Saturday, March 24, 2007

Managing C++ Objects

http://billharlan.com/pub/papers/Managing_Cpp_Objects.html

Managing C++ Objects

Here are some guidelines I have found useful for writing C++ classes. There are many good books on the subject, but they have not been sufficient to keep me out of trouble. The first time I returned to writing C++ after a year of writing Java, I was appalled at how much my design was constrained by managing the lifetime of objects. When C++ classes share objects, then they must negotiate who owns the object. Garbage collection is not available, and smart pointers often fall short.
§ Simple constructors

If your preferred constructor takes arguments, then define a default constructor (no arguments) and make it protected. Derived classes will require this method.

Define protected initialization methods void init(...) with arguments, and call them from your preferred constructors. Each initialization method should set all member variables to a valid state, without relying on constructor initialization blocks. Use these initialization methods from your public copy constructor and assignment operator, if required.

Remove everything from constructor initialization blocks except the simplest constructor of a superclass (preferably a default constructor). Call protected superclass initialization methods from the subclass initialization methods. Make initialization methods non-virtual to avoid hiding by derived classes (since the name will always be init(...).)

All init(...) methods should first call the init() equivalent of a default constructor, to initialize all member pointers, perhaps to nulls. If your constructor fails and throws an exception, then the destructor can be called safely.

Your constructors will now be much more flexible and robust. Derived class constructors can manipulate their arguments before initializing the superclass. (Superclass constructors can only be called in initialization blocks.) Within a single class, you can share more initialization between alternative constructors. You need not worry about the order of initialization blocks.
§ Implement "The Big Three"

Always define a copy constructor and an assignment operator. Don't let anyone use the default implementations. If your class contains pointers to objects which your class does not plan to delete, then just make these two methods private, without an implementation. Do not implement versions that make shallow copies. You do not want a user to accidentally make copies on the stack if required to call a non-copy constructor or clone method instead. Making copies of objects should be a very deliberate step Conversion operators (single-argument constructors) can be dangerous for the same reason.

Define a virtual destructor unless you never want anyone to derive from your class. Define a protected non-virtual void dispose() method that deletes the object's resources, then call this method from your destructor. (This is the destructor equivalent of an init() method.) You can use this method in assignment operators, initialization, and derived classes.
§ No references as members

A class member should never be a reference, whether const or non-const. A member's object reference can only be set in the initialization block of a constructor. You will not be able to set a reference member in an initialization method. A reference permanently prevents your class from replacing the object dynamically.
§ Optional ownership

If a constructor or initialization method takes a non-const object as an argument, then you must decide whether this wrapper class will assume ownership of this object. The destructor of a Bridge or Decorator class might need to delete the contained object. Or maybe not. If you have any doubt, then the constructor should allow the user to choose.
§ No pointers as arguments

Pass all objects to class methods and constructors as references. There is absolutely no advantage to passing objects as pointers. This rule is equally valid whether the objects are const or not.

I've already recommended that all class members be saved as pointers. You can easily take the address of an argument reference (with an ampersand) and assign it to your member pointer. Some C++ programmers do not seem to realize that the address of a reference is the same as the address of the original object. So they pass pointers when they want to save the argument, and references when they do not. This is a poor form of documentation, based on a misunderstanding.

If an object is passed to a constructor or initialization method, the user can expect the class to hang onto it. If a method saves an object from an argument, choose an appropriate name, like setColor(Color&) or addInterpolator(Interpolator&).

The worst excuse for using a pointer as an argument is that you want to give it a default value of null (0). You still have to document what a null object is supposed to mean. Worse, the user may overlook that the argument exists or is optional. Declare a separate method that lacks the extra argument. The effort is negligible.
§ Returning objects

One can always return objects from class methods by reference, either const or non-const. A user can take the address of the reference, if necessary, to save the object. But there are no drawbacks to returning objects always as pointers. Consistency is preferable, and most API's return pointers.

If you return an object allocated on the heap (with a new), then be clear who has ownership of the object--your class, the recipient, or a third party.

Think about whether you are breaking encapsulation of member data in a way that will prevent modification later.

Never return a reference to a class member allocated on the stack in the header file. If your class replaces the value, then the user may be left with an invalid reference, even though your object still exists. (Other reasons: Your class will never be able to remove the object as a member. A user may manipulate the logic of your class in unexpected ways.)

A method should modify an object constructed by the user by accepting it as a non-const reference. Returning the same object would be redundant and confusing.
§ Clean header files

A header file ideally includes only the header file of super-classes or of standard C++ libraries. All other classes can be forward declared, like class ClassName; or template class ClassName;. Forward declarations will greatly simplify your "make" dependencies and speed your builds. Repairs will be easier.

Member variables that are saved by value require your header to include another header file. Consider allocating such members on the heap, even if you must delete them in the destructor. Save member objects by value only when the default constructor creates a lightweight object with a useful state.

If you have reasons to put your entire implementation in the header file, then of course you cannot take advantage of forward declarations.
§ Write more Java

When you get a chance, write more Java to free your mind of such distractions. Your C++ will improve.
§ Examples

See an illustration of some of these patterns in [ ../code/cpp_prototype ] .

Bill Harlan

1998

Revision: 1.21 2004/09/14 18:01:26 harlan Exp $

Return to parent directory.