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.
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.
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.
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.
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.
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.
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.
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
.
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.
When you get a chance, write more Java to free your mind of such distractions. Your C++ will improve.
See an illustration of some of these patterns in [ ../code/cpp_prototype/ ] .
Bill Harlan, 1998
Return to parent directory.