Inapoi
Inainte
Cuprins
Combining composition &
inheritance
Of course, you can use composition &
inheritance together. The following example shows the creation of a more complex
class using both of them.
//: 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”) of type 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( ), which 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 of 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 16, 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 reason for existence
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 (in the derived-class
constructor or destructor, you must be able to assume that the base-class
subobject is still available for use, and has already been constructed –
or not destroyed yet).
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 you inherit a class and provide a new
definition for one of its member functions, there are two possibilities. The
first is that you provide the exact signature and return type in the derived
class definition as in the base class definition. This is called
redefining for ordinary member functions and
overriding when the base class member function is
a virtual function
(virtual functions are the normal case, and will be covered in detail in
Chapter 15). But what happens if you change the member function argument list or
return type in the derived class? Here’s an example:
//: C14:NameHiding.cpp
// Hiding overloaded names during inheritance
#include <iostream>
#include <string>
using namespace std;
class Base {
public:
int f() const {
cout << "Base::f()\n";
return 1;
}
int f(string) const { return 1; }
void g() {}
};
class Derived1 : public Base {
public:
void g() const {}
};
class Derived2 : public Base {
public:
// Redefinition:
int f() const {
cout << "Derived2::f()\n";
return 2;
}
};
class Derived3 : public Base {
public:
// Change return type:
void f() const { cout << "Derived3::f()\n"; }
};
class Derived4 : public Base {
public:
// Change argument list:
int f(int) const {
cout << "Derived4::f()\n";
return 4;
}
};
int main() {
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
//! d2.f(s); // string version hidden
Derived3 d3;
//! x = d3.f(); // return int version hidden
Derived4 d4;
//! x = d4.f(); // f() version hidden
x = d4.f(1);
} ///:~
In Base you see an overloaded
function f( ), and Derived1 doesn’t make any changes to
f( ) but it does redefine g( ). In main( ),
you can see that both overloaded versions of f( ) are available in
Derived1. However, Derived2 redefines one overloaded version of
f( ) but not the other, and the result is that the second overloaded
form is unavailable. In Derived3, changing the return type hides both the
base class versions, and Derived4 shows that changing the argument list
also hides both the base class versions. In general, we can say that anytime you
redefine an overloaded function name from the base class, all the other versions
are automatically hidden in the new class. In Chapter 15, you’ll see that
the addition of the virtual keyword affects function overloading a bit
more.
If you change the interface of the base
class by modifying the
signature
and/or
return
type of a member function from the base class, then you’re using the class
in a different way than inheritance is normally intended to support. It
doesn’t necessarily mean you’re doing it wrong, it’s just that
the ultimate goal of inheritance is to support
polymorphism, and if you change the function
signature or return type then you are actually changing the interface of the
base class. If this is what you have intended to do then you are using
inheritance primarily to reuse code, and not to maintain the common interface of
the base class (which is an essential aspect of polymorphism). In general, when
you use inheritance this way it means you’re taking a general-purpose
class and specializing it for a particular need – which is usually, but
not always, considered the realm of
composition.
For example, consider the Stack
class from Chapter 9. One of the problems with that class is that you had to
perform a cast every time you fetched a pointer from the container. This is not
only tedious, it’s unsafe – you could cast the pointer to anything
you want.
An approach that seems better at first
glance is to specialize the general Stack class using inheritance.
Here’s an example that uses the class from Chapter 9:
//: C14:InheritStack.cpp
// Specializing the Stack class
#include "../C09/Stack4.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class StringStack : public Stack {
public:
void push(string* str) {
Stack::push(str);
}
string* peek() const {
return (string*)Stack::peek();
}
string* pop() {
return (string*)Stack::pop();
}
~StringStack() {
string* top = pop();
while(top) {
delete top;
top = pop();
}
}
};
int main() {
ifstream in("InheritStack.cpp");
assure(in, "InheritStack.cpp");
string line;
StringStack textlines;
while(getline(in, line))
textlines.push(new string(line));
string* s;
while((s = textlines.pop()) != 0) { // No cast!
cout << *s << endl;
delete s;
}
} ///:~
Since all of the member functions in
Stack4.h are inlines, nothing needs to be linked.
StringStack specializes
Stack so that push( ) will accept only String
pointers. Before, Stack would accept void pointers, so the user
had no type checking to make sure the proper pointers were inserted. In
addition, peek( ) and pop( ) now return String
pointers instead of void pointers, so no cast is necessary to use the
pointer.
Amazingly enough, this extra
type-checking safety is free in push( ), peek( ), and
pop( )! The compiler is being given extra type information that it
uses at compile-time, but the functions are inlined and no extra code is
generated.
Name hiding comes into play here because,
in particular, the push( ) function has a different signature: the
argument list is different. If you had two versions of push( ) in
the same class, that would be overloading, but in this case overloading is
not what we want because that would still allow you to pass any kind of
pointer into push( ) as a void*. Fortunately, C++ hides the
push(void*) version in the base class in favor of the new version
that’s defined in the derived class, and therefore it only allows us to
push( ) string pointers onto the StringStack.
Because we can now guarantee that we know
exactly what kind of objects are in the container, the destructor works
correctly and the ownership problem is solved – or
at least, one approach to the ownership problem. Here, if you
push( ) a string pointer onto the StringStack, then
(according to the semantics of the StringStack) you’re also
passing ownership of that pointer to the StringStack. If you
pop( ) the pointer, you not only get the pointer, but you also get
ownership of that pointer. Any pointers that are left on the StringStack
when its destructor is called are then deleted by that destructor. And since
these are always string pointers and the delete statement is
working on string pointers instead of void pointers, the proper
destruction happens and everything works correctly.
There is a drawback: this class works
only for string pointers. If you want a Stack that works
with some other kind of object, you must write a new version of the class so
that it works only with your new kind of object. This rapidly becomes tedious,
and is finally solved using templates, as you will see in Chapter
16.
We can make an additional observation
about this example: it changes the interface of the Stack in the process
of inheritance. If the interface is different, then a StringStack really
isn’t a Stack, and you will never be able to correctly use a
StringStack as a Stack. This makes the use of inheritance
questionable here; if you’re not creating a StringStack that
is-a type of Stack, then why are you
inheriting? A more appropriate version of StringStack will be shown later
in this
chapter.
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 class,
so all the constructors and destructors
in the hierarchy below them must be called. Thus,
constructors and destructors don’t inherit and must be created specially
for each derived class.
In addition, the operator=
doesn’t inherit because it performs a
constructor-like activity. That is, just because you know how to assign 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 assignment will still have the same
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 in order for the compiler to synthesize the default constructor
and the copy-constructor.) This was briefly described in Chapter 6. The
synthesized constructors use
memberwise
initialization and the synthesized operator= uses
memberwise
assignment. Here’s an example of the functions that are synthesized by the
compiler:
//: C14:SynthesizedFunctions.cpp
// Functions that are synthesized by the compiler
#include <iostream>
using namespace std;
class GameBoard {
public:
GameBoard() { cout << "GameBoard()\n"; }
GameBoard(const GameBoard&) {
cout << "GameBoard(const GameBoard&)\n";
}
GameBoard& operator=(const GameBoard&) {
cout << "GameBoard::operator=()\n";
return *this;
}
~GameBoard() { cout << "~GameBoard()\n"; }
};
class Game {
GameBoard gb; // Composition
public:
// Default GameBoard constructor called:
Game() { cout << "Game()\n"; }
// You must explicitly call the GameBoard
// copy-constructor or the default constructor
// is automatically called instead:
Game(const Game& g) : gb(g.gb) {
cout << "Game(const Game&)\n";
}
Game(int) { cout << "Game(int)\n"; }
Game& operator=(const Game& g) {
// You must explicitly call the GameBoard
// assignment operator or no assignment at
// all happens for gb!
gb = g.gb;
cout << "Game::operator=()\n";
return *this;
}
class Other {}; // Nested class
// Automatic type conversion:
operator Other() const {
cout << "Game::operator Other()\n";
return Other();
}
~Game() { cout << "~Game()\n"; }
};
class Chess : public Game {};
void f(Game::Other) {}
class Checkers : public Game {
public:
// Default base-class constructor called:
Checkers() { cout << "Checkers()\n"; }
// You must explicitly call the base-class
// copy constructor or the default constructor
// will be automatically called instead:
Checkers(const Checkers& c) : Game(c) {
cout << "Checkers(const Checkers& c)\n";
}
Checkers& operator=(const Checkers& c) {
// You must explicitly call the base-class
// version of operator=() or no base-class
// assignment will happen:
Game::operator=(c);
cout << "Checkers::operator=()\n";
return *this;
}
};
int main() {
Chess d1; // Default constructor
Chess d2(d1); // Copy-constructor
//! Chess d3(1); // Error: no int constructor
d1 = d2; // Operator= synthesized
f(d1); // Type-conversion IS inherited
Game::Other go;
//! d1 = go; // Operator= not synthesized
// for differing types
Checkers c1, c2(c1);
c1 = c2;
} ///:~
The constructors and the operator=
for GameBoard and Game announce themselves so you can see when
they’re used by the compiler. In addition, the operator
Other( ) performs automatic type conversion from a Game object
to an object of the nested class Other. The class Chess simply
inherits from Game 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 synthesized
default constructor and copy-constructor for the derived class Chess are
called. The Game versions of these constructors are called as part of the
constructor-call hierarchy. Even though it looks like inheritance, new
constructors are actually synthesized by the compiler. 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 Chess using memberwise assignment (thus, the
base-class version is called) because that function was not explicitly written
in the new class. And of course the destructor was automatically synthesized by
the compiler.
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
Game to make an Other object, those pieces are still there in
anything derived from Game and the type conversion operator is still
valid (even though you may in fact want to redefine it).
operator= is synthesized
only for assigning objects of the same type. If you want to assign one
type to another you must always write that operator=
yourself.
If you look more closely at Game,
you’ll see that the copy-constructor and assignment operators have
explicit calls to the member object copy-constructor and assignment operator.
You will normally want to do this because otherwise, in the case of the
copy-constructor, the default member object constructor will be used instead,
and in the case of the assignment operator, no assignment at all will be
done for the member objects!
Lastly, look at Checkers, which
explicitly writes out the default constructor, copy-constructor, and assignment
operators. In the case of the default constructor, the default base-class
constructor is automatically called, and that’s typically what you want.
But, and this is an important point, as soon as you decide to write your own
copy-constructor and assignment operator, the compiler assumes that you know
what you’re doing and does not automatically call the base-class
versions, as it does in the synthesized functions. If you want the base class
versions called (and you typically do) then you must explicitly call them
yourself. In the Checkers copy-constructor, this call appears in the
constructor initializer list:
Checkers(const Checkers& c) : Game(c) {
In the Checkers assignment
operator, the base class call is the first line in the function
body:
Game::operator=(c);
These calls should be part of the
canonical form that you use whenever you inherit a
class.
Inheritance and static member
functions
static member functions act the
same as non-static member functions:
- They inherit into the
derived class.
- If
you redefine a static member, all the other overloaded functions in the base
class are hidden.
- If
you change the signature of a function in the base class, all the base class
versions with that function name are hidden (this is really a variation of the
previous point).
However,
static member functions cannot be virtual (a topic covered
thoroughly in Chapter 15).
 |
|