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 17, 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 10. 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).
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru