Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Combining composition & inheritance

Of course, you can use the two together. The following example shows the creation of a more complex class, using both inheritance and composition.

//: C14:Combined.cpp
// Inheritance & composition

class A {
  int i;
public:
  A(int ii) : i(ii) {}
  ~A() {}
  void f() const {}
};

class B {
  int i;
public:
  B(int ii) : i(ii) {}
  ~B() {}
  void f() const {}
};

class C : public B {
  A a;
public:
  C(int ii) : B(ii), a(ii) {}
  ~C() {} // Calls ~A() and ~B()
  void f() const {  // Redefinition
    a.f();
    B::f();
  }
};

int main() {
  C c(47);
} ///:~

C inherits from B and has a member object (“is composed of”) A. You can see the constructor initializer list contains calls to both the base-class constructor and the member-object constructor.

The function C::f( ) redefines B::f( ) that it inherits, and also calls the base-class version. In addition, it calls a.f( ). Notice that the only time you can talk about redefinition of functions is during inheritance; with a member object you can only manipulate the public interface of the object, not redefine it. In addition, calling f( ) for an object of class C would not call a.f( ) if C::f( ) had not been defined, whereas it would call B::f( ).

Automatic destructor calls

Although you are often required to make explicit constructor calls in the initializer list, you never need to make explicit destructor calls because there’s only one destructor for any class, and it doesn’t take any arguments. However, the compiler still ensures that all destructors are called, and that means all the destructors in the entire hierarchy, starting with the most-derived destructor and working back to the root.

It’s worth emphasizing that constructors and destructors are quite unusual in that every one in the hierarchy is called, whereas with a normal member function only that function is called, but not any of the base-class versions. If you also want to call the base-class version of a normal member function that you’re overriding, you must do it explicitly.

Order of constructor & destructor calls

It’s interesting to know the order of constructor and destructor calls when an object has many subobjects. The following example shows exactly how it works:

//: C14:Order.cpp
// Constructor/destructor order
#include <fstream>
using namespace std;
ofstream out("order.out");

#define CLASS(ID) class ID { \
public: \
  ID(int) { out << #ID " constructor\n"; } \
  ~ID() { out << #ID " destructor\n"; } \
};

CLASS(Base1);
CLASS(Member1);
CLASS(Member2);
CLASS(Member3);
CLASS(Member4);

class Derived1 : public Base1 {
  Member1 m1;
  Member2 m2;
public:
  Derived1(int) : m2(1), m1(2), Base1(3) {
    out << "Derived1 constructor\n";
  }
  ~Derived1() {
    out << "Derived1 destructor\n";
  }
};

class Derived2 : public Derived1 {
  Member3 m3;
  Member4 m4;
public:
  Derived2() : m3(1), Derived1(2), m4(3) {
    out << "Derived2 constructor\n";
  }
  ~Derived2() {
    out << "Derived2 destructor\n";
  }
};

int main() {
  Derived2 d2;
} ///:~

First, an ofstream object is created to send all the output to a file. Then, to save some typing and demonstrate a macro technique that will be replaced by a much improved technique in Chapter XX, a macro is created to build some of the classes, which are then used in inheritance and composition. Each of the constructors and destructors report themselves to the trace file. Note that the constructors are not default constructors; they each have an int argument. The argument itself has no identifier; its only job is to force you to explicitly call the constructors in the initializer list. (Eliminating the identifier prevents compiler warning messages.)

The output of this program is

Base1 constructor
Member1 constructor
Member2 constructor
Derived1 constructor
Member3 constructor
Member4 constructor
Derived2 constructor
Derived2 destructor
Member4 destructor
Member3 destructor
Derived1 destructor
Member2 destructor
Member1 destructor
Base1 destructor

You can see that construction starts at the very root of the class hierarchy, and that at each level the base class constructor is called first, followed by the member object constructors. The destructors are called in exactly the reverse order of the constructors – this is important because of potential dependencies.

It’s also interesting that the order of constructor calls for member objects is completely unaffected by the order of the calls in the constructor initializer list. The order is determined by the order that the member objects are declared in the class. If you could change the order of constructor calls via the constructor initializer list, you could have two different call sequences in two different constructors, but the poor destructor wouldn’t know how to properly reverse the order of the calls for destruction, and you could end up with a dependency problem.

Name hiding

If a base class has a function name that’s overloaded several times, redefining that function name in the derived class will hide all the base-class versions. That is, they become unavailable in the derived class:

//: C14:Hide.cpp
// Name hiding during inheritance

class Homer {
public:
  int doh(int) const { return 1; }
  char doh(char) const { return 'd';}
  float doh(float) const { return 1.0; }
};

class Bart : public Homer {
public:
  class Milhouse {};
  void doh(Milhouse) const {}
};

int main() {
  Bart b;
//! b.doh(1); // Error
//! b.doh('x'); // Error
//! b.doh(1.0); // Error
} ///:~

Because Bart redefines doh( ), none of the base-class versions can be called for a Bart object. In each case, the compiler attempts to convert the argument into a Milhouse object and complains because it can’t find a conversion.

As you’ll see in the next chapter, it’s far more common to redefine functions using exactly the same signature and return type as in the base class.

Functions that don’t automatically inherit

Not all functions are automatically inherited from the base class into the derived class. Constructors and destructors deal with the creation and destruction of an object, and they can know what to do with the aspects of the object only for their particular level, so all the constructors and destructors in the entire hierarchy must be called. Thus, constructors and destructors don’t inherit.

In addition, the operator= doesn’t inherit because it performs a constructor-like activity. That is, just because you know how to initialize all the members of an object on the left-hand side of the = from an object on the right-hand side doesn’t mean that initialization will still have meaning after inheritance.

In lieu of inheritance, these functions are synthesized by the compiler if you don’t create them yourself. (With constructors, you can’t create any constructors for the default constructor and the copy-constructor to be automatically created.) This was briefly described in Chapter XX. The synthesized constructors use memberwise initialization and the synthesized operator= uses memberwise assignment. Here’s an example of the functions that are created by the compiler rather than inherited:

//: C14:Ninherit.cpp
// Non-inherited functions
#include <fstream>
using namespace std;
ofstream out("ninherit.out");

class Root {
public:
  Root() { out << "Root()\n"; }
  Root(Root&) { out << "Root(Root&)\n"; }
  Root(int) { out << "Root(int)\n"; }
  Root& operator=(const Root&) {
    out << "Root::operator=()\n";
    return *this;
  }
  class Other {};
  operator Other() const {
    out << "Root::operator Other()\n";
    return Other();
  }
  ~Root() { out << "~Root()\n"; }
};

class Derived : public Root {};

void f(Root::Other) {}

int main() {
  Derived d1;  // Default constructor
  Derived d2 = d1; // Copy-constructor
//! Derived d3(1); // Error: no int constructor
  d1 = d2; // Operator= not inherited
  f(d1); // Type-conversion IS inherited
} ///:~

All the constructors and the operator= announce themselves so you can see when they’re used by the compiler. In addition, the operator Other( ) performs automatic type conversion from a Root object to an object of the nested class Other. The class Derived simply inherits from Root and creates no functions (to see how the compiler responds). The function f( ) takes an Other object to test the automatic type conversion function.

In main( ), the default constructor and copy-constructor are created and the Root versions are called as part of the constructor-call hierarchy. Even though it looks like inheritance, new constructors are actually created. As you might expect, no constructors with arguments are automatically created because that’s too much for the compiler to intuit.

The operator= is also synthesized as a new function in Derived using memberwise assignment because that function was not explicitly written in the new class.

Because of all these rules about rewriting functions that handle object creation, it may seem a little strange at first that the automatic type conversion operator is inherited. But it’s not too unreasonable – if there are enough pieces in Root to make an Other object, those pieces are still there in anything derived from Root and the type conversion operator is still valid (even though you may in fact want to redefine it).

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]