You are here:  OakRoadSystems > Programming > C++ Practices > 4/Ctors/Dtors
 
  Home
  Site Map
  Shareware Utils
  Articles
      Programming
      Internet
      Mathematics
      General
  Search

4. Constructors, Destructors, Inheritance

revised 15 Feb 2001
Copyright © 1998-2002 Stan Brown, Oak Road Systems

 

This page continues the class design topic. See the next page for class implementation.


 

I'll follow the "rule of 3".

Details
"A class with any of {destructor, assignment operator, copy constructor} generally needs all three." [UPDATED! 2000-06-21 -- to next update]
(Cline 2000 [25.9], "Are there any lint-like guidelines for C++?")

[UPDATED! 2000-06-16 -- to next update]
Eckel 1995 page 774 writes that any class with pointers as data members will need all three. Martin Aupperle (private communication) adds the important proviso, "if the class maintains resources (is the owner of resources)".

A class that doesn't own dynamically allocated resources probably doesn't need a copy constructor, assignment operator, or destructor (but see below).


 

When it's needed, I'll write a proper assignment operator.

Details
operator=() needs to

The function signature should be

    T& operator=(const T& rhs);
and the function itself should follow this pattern:
    T& T::operator=(const T& rhs) {
        if (this == &rhs)
            return *this;
        // do the assignment stuff
        return *this;
    }
[UPDATED! 2000-06-16 -- to next update]
The purpose of checking for self assignment is to prevent a nasty and subtle bug when the object owns resources. If possible, it is better still to write the assignment operator so that self assignment does no harm; see Cline and Sutter in the references below.
 
See also
Meyers 1997 pages 71-76 and 64; Stroustrup 1997 page 246; Cline 2000 [25.9], "Are there any lint-like guidelines for C++?", and [12.3], "OK, OK, already; I'll handle self-assignment. How do I do it?" Herb Sutter, Sutter 1999 pages 43-48, presents an exception-safe alternative to the above copy assignment by using a "swap" helper function; on pages 159-163 he talks about the above check for self assignment.

 

In constructors, I'll prefer initializer lists over assignments.

Details
Not this:
    Rational::Rational(int top=0, int bottom=1) {
        top_ = top;
        bottom_ = bottom;
    }
but this:
    Rational::Rational(int top=0, int bottom=1)
        : top_(top), bottom_(bottom) {
    }
I'll remember not to count on having the items in an initializer list processed in any particular order. (Actually, they're initialized in the order of the data members in the class definition. But it's not safe to rely on that, since I might later rearrange the data members. As Gollum said, "Tricksy! Tricksy!")

There's a related rule for general code.
 

Rationale
Initializer lists are more efficient when the data members are not built-in types -- by a factor of 3, according to [UPDATED! 2000-06-21 -- to next update]
Cline 2000 [25.9], "Are there any lint-like guidelines for C++?"

They're also safer: if the constructor has any actual code it knows that the data members are all initialized before the opening brace, so that it can confidently call their member functions if it needs to.

Initializer lists may look odd at first, especially to old hands at C. But I think the gain in efficiency is enough to outweigh that inconvenience.
 

See also
Eckel 1995 pages 289-290 and 502-505; Meyers 1997 pages 53-58

 

I'll handle exceptions properly in constructors.

Details
[UPDATED! 2000-06-16 -- to next update]
If my class has pointers as data members, and the object is responsible for managing resources, the constructor must catch its own exceptions and delete any objects that it created.

The standard auto_ptr provides one way to get those members deallocated automatically.
 

See also
Meyers 1996 pages 50-58

 

I'll give every base class a virtual destructor.

Rationale
[UPDATED! 2000-06-16 -- to next update]
Otherwise, program behavior may be undefined, for instance if a derived class object is deleted through a base class pointer. Stroustrup has a short and clear example, beginning with the second paragraph under "Why are destructors not virtual by default?"
 
Details
"Many people summarize the situation this way: declare a virtual destructor in a class if and only if that class contains at least one [other] virtual function. This is a good rule, one that works most of the time, but unfortunately, it is possible to get bitten by the nonvirtual destructor problem even in the absence of virtual functions." (Meyers 1997 page 62)

[UPDATED! 2000-06-16 -- to next update]
This does not mean that all classes need non-default destructors or that all destructors should be declared virtual, just those for classes from which other classes may be derived. However, Meyers 1996 devotes twelve pages (257-270) to an argument that every non-leaf class should be abstract. I'm not certain I'm ready to go that far, but he does make some compelling points.

FUTURE This is one area where I need to do some more investigation.
 

See also
Meyers 1997 pages 59-64i, 254-256; Buchheit 1994 page 33

 

I'll respect the LSP in public inheritance.

Details
[NEW! 2000-06-26 -- to next update]
The Liskov Substitution Principle (quoted in Stroustrup 1997) says that if I have
    class D : public B { ... };
then an object of type D should be usable wherever an object of type B is usable (or expected). In other words, D must extend B, not extend B "with some exceptions".

Francis Glassborow said it well in "Re: Design Mistake?" in comp.std.c++ on 15 Jun 2000: "If a derived class cannot always substitute for the base class it is a design fault to treat it as if it could be. This despite many books to the contrary. OO in C++ is not OO in Smalltalk."
 

Rationale
Violating the LSP for public inheritance strikes at the whole root of polymorphism. If client code is doing something with a B*, it ought to be able to do anything pointer-to-B can do, without worrying about whether the underlying object is actually some crippled subclass of B. Otherwise the code is fragile and too hard to use.
 
See also
Cline 2000 has an entire chapter on this point: "[21] Inheritance -- proper inheritance and substitutability", especially from "[21.6] Is a Circle a kind-of an Ellipse?" to the end. Sutter 1999 makes similar points, but more briefly, on pages 95-96.

 

I'll distinguish between "is-a" and "has-a".

Details
[NEW! 2000-06-26 -- to next update]
"Is-a" relationships are usually modeled by public inheritance. "Has-a" relationships are usually modeled by containment. "Has-a" is also a special case of "is-implemented-in-terms-of", which may be modeled by private or protected inheritance.

For example, if I have a Building and a Room class, I would not inherit Building from Room but would contain a Room object (or pointer or reference) in the Building class. This example makes Stroustrup 1997's point (page 741) almost absurdly obvious: if D can have more than one B, you want to contain B in D and not derive D from B. Cline 2000 has a less silly example at "[24.2] How are 'private inheritance' and 'composition' similar?"

To implement the "has-a" or "is-implemented-in-terms-of" relationship, authors like Sutter 1999 (page 90) and Cline 2000 prefer containing B in D over inheriting D from B, except when you need:


 
See also
Sutter 1999 pages 88-96; Stroustrup 1997 pages 740-745; Cline 2000: "[24.2] How are 'private inheritance' and 'composition' similar?" and "[24.3] Which should I prefer: composition or private inheritance?"

previous   contents     C++ Practices
 contact info  |  site map
Valid HTML 4.0! this page: http://oakroadsystems.com/codeprac/inher.htm