Don't know C++ (2) (inheritance and composition)

Chong Wang Lv6

My gaps in knowledge have become unbearable. I need to review my understanding of several concepts seriously, from the beginning.

Until I make these ideas clear, I cannot work on other things with peace of mind. Why am I so weak at this?

Chapter 14 of Thinking in C++ opens with this idea:

One of C++’s most important features is code reuse. To go further, however, you need more than copying and modifying code.

This chapter introduces two ways to do that. The first is direct: create an object of an existing class inside a new class. Because the new class is composed of existing-class objects, this approach is called composition.

The second approach is more complex. Create a new class as a type of an existing class. Rather than modifying the existing class, take its form and add code to it. This is called inheritance.

1. Composition syntax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class X
{
int i;
public:
X(){i=0;}
void set(int ii){i=ii;}
int read()const {return i;};
int permute(){return i=i+47;}
};

class Y
{
int i;
public:
X x;
Y(){i=0;}
void f(int ii){i=ii;}
int g()const {return i;}
};

int main()
{
Y y;
y.f(47);
y.x.set(37);
return 0;
}

More commonly, the embedded object is made private so it becomes part of the implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Y
{
int i;
public:
X x;
Y(){i=0;}
void f(int ii){i=ii;x.set(ii);}
int g()const {return i*x.read();}
void permute(){x.permute();};
};

int main()
{
Y y;
y.f(47);
y.permute();
return 0;
}

2. Inheritance syntax

In code, use the class name as before, but add a colon and the base-class name before the opening brace. For multiple inheritance, list multiple base classes separated by commas. Doing so gives the derived class the base class’s data members and member functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Y:public X
{
int i;
public:
Y(){i=0;}
int change()
{
i = permute();
return i;
}
void set(int ii)
{
i = ii;
X::set(ii);
}
};

int main()
{
cout << "sizeof(X) = " << sizeof(X) << endl;
cout << "sizeof(Y) = " << sizeof(Y) << endl;

Y D;
D.change();
D.read();
D.permute();
D.set(12);
return 0;
}

Y inherits from X, which means Y contains all of X’s data members and member functions.

All private members of X remain private in Y. Inheritance does not let Y bypass access protection. X’s private members still occupy storage, but cannot be accessed directly.

Also, inheritance is private by default. Without public before the base class, all of the base class’s public members become private in the derived class. That is usually not what we want; use the public keyword to keep them public.

After redefining a function, you may still want to call the base-class version. Calling set() directly invokes the local version and recurses; to call the base class’s set(), explicitly name the base class with the scope-resolution operator.

3. Constructor initializer lists

C++ provides dedicated syntax for calling constructors of subobjects: a constructor initializer list. Its form resembles inheritance syntax. In inheritance, the base class is placed after a colon before the class body. In an initializer list, calls to subobject constructors appear after the constructor’s parameter list and colon, before the function body. For a class MyType derived from Bar, where Bar has a single int constructor parameter:

1
MyType::MyType(int i):Bar(i){}
3.1 Initializing member objects

For composition, use the same syntax for member objects, except that you give the object name rather than a class name. Separate multiple constructor calls in an initializer list with commas.

1
MyType2::MyType2(int i):Bar(i),m(i+1){}
3.2 Built-in types in initializer lists

For syntactic consistency, you can regard a built-in type as a type with a one-argument constructor whose argument has the same type as the variable being initialized. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class X
{
int i;
float f;
char c;
char *s;
public:
X():i(7),f(1.4),c('x'),s("howdy"){}
};

int main()
{
int i(100);
int *ip=new int(47);
return 0;
}

This makes built-in types behave a little like objects. Remember, though, that these are not real constructors. In particular, initialization does not occur unless you explicitly use this pseudo-constructor form.

4. Combining composition and inheritance

Although initializer lists often require explicit constructor calls, explicit destructor calls are unnecessary: every type has only one destructor, and it takes no arguments. The compiler still ensures that every destructor is called. Across the hierarchy, destruction begins at the lowest derived layer and continues back to the root.

4.1 Constructor and destructor call order
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define CLASS(ID) class ID{\
public:\
ID(int){ cout << #ID " constructor\n";}\
~ID(){ cout << #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)
{
cout << "Derived1 constructor\n";
}
~Derived1()
{
cout << "Derived1 destructor\n";
}
};

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

int main()
{
Derived2 d2;
return 0;
}

The program outputs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

As shown, construction starts at the root of the class hierarchy. At each level, the base-class constructor runs first, followed by member-object constructors. Destructors run in exactly the reverse order. This matters because of dependencies: constructors and destructors of a derived class must assume that the base-class subobject is available and has either already been constructed or has not yet been destroyed.

Another important detail: for member objects, construction order is not affected by their order in the initializer list. It is determined by their declaration order in the class. Otherwise, different constructors could establish different construction orders and a destructor would not know which order to reverse, causing dependency problems.

5. Name hiding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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:
int f()const
{
cout << "Derived2::f()\n";
return 2;
}
};

class Dervied3:public Base
{
public:
void f()const
{
cout << "Dervied3::f()\n";
}
};

class Dervied4:public Base
{
public:
int f(int)const
{
cout << "Dervied4::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);

Dervied3 d3;
d3.f();
//! x = d3.f();

Dervied4 d4;
//! x = d4.f();
x = d4.f(1);

//system("pause");
return 0;
}

Base has overloads of f(). Derived1 does not change f(), so both base-class overloads remain available. Derived2 redefines one version of f() but not the other, so the other overload cannot be used. Derived3 hides both base-class versions by changing the return type, and Derived4 does so by changing the parameter list. In general, when a derived class redefines one overloaded base-class function, every other base-class overload with that name is automatically hidden. The next post explains how virtual affects this.

6. Functions that are not inherited automatically

Not every function is inherited automatically. Constructors and destructors manage object creation and destruction, but each only knows what to do for objects at its own level. Therefore constructors and destructors at every layer must be called: they cannot be inherited and must be created separately for every derived class.

Likewise, operator= cannot be inherited because it performs work analogous to a constructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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;
public:
Game(){cout << "Game()\n";}
Game(const Game &g):gb(g.gb)
{
cout << "Game(const Game&)";
}
Game(int){cout << "Game(int)\n";}
Game& operator=(const Game& g)
{
gb = g.gb;
cout << "Game::operator=()\n";
return *this;
}
class Other
{
public:
Other(){cout << "AC!" << endl;}
};
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:
Checkers(){cout << "Checkers()\n";}
Checkers(const Checkers &c):Game(c)
{
cout << "Checkers(const Checkers &c)\n";
}
Checkers& operator=(const Checkers &c)
{
Game::operator=(c);
cout << "Checkers::operator=()\n";
return *this;
}
};

int main()
{
Chess d1;
Chess d2(d1);
d1=d2;
f(d1);
Game::Other go;
Checkers c1,c2(c1);
c1=c2;
//system("pause");
return 0;
}

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GameBoard()
Game()
GameBoard(const GameBoard&)
Game(const Game&)GameBoard::operator=()
Game::operator=()
Game::operator Other()
AC!
AC!
GameBoard()
Game()
Checkers()
GameBoard(const GameBoard&)
Game(const Game&)Checkers(const Checkers &c)
GameBoard::operator=()
Game::operator=()
Checkers::operator=()
~Game()
~GameBoard()
~Game()
~GameBoard()
~Game()
~GameBoard()
~Game()
~GameBoard()

Several points are worth noting:

operator Other() performs an automatic conversion from a Game object to an object of the embedded Other class.

Checkers explicitly defines its default constructor, copy constructor, and assignment operator. In the default constructor, the default base-class constructor is called automatically, which is what we want. But once you write your own copy constructor or assignment operator, the compiler assumes you know what you are doing and no longer calls the base-class version for you. If you want it, you must call it explicitly. In Checkers’ copy constructor, this happens in the initializer list:

1
Checkers(const Checker &c):Game(c){}

For the Checkers assignment operator, the base-class call is on the first line of the function body:

1
Game::operator=(c);

7. Subtype setup

Suppose you want a class around an ifstream object that both opens a file and keeps its filename. You can use composition and embed both ifstream and string in the new class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FName1
{
ifstream file;
string fileName;
bool named;
public:
FName1():named(false){}
FName1(const string& fname):fileName(fname),file(fname.c_str())
{
named = true;
}
string name()const{return fileName;}
void name(const string& newName)
{
if(named)return;
fileName = newName;
named = true;
}
operator ifstream &()
{
return file;
}
};
int main()
{
FName1 file("FName1.cpp");
cout << file.name() <<endl;
return 0;
}

Here ifstream & is an overloaded type conversion. Reference

Declaration: operator XX(); has no return type; XX is the target type.

Usage: XX a = (XX)obj;

There is a problem: we may want to use an FName1 object anywhere an ifstream is used by providing an automatic conversion from FName1 to ifstream &, but in main:

1
file.close();

One option is to add close() to FName1:

1
void close(){file.close();}

This works if you need only a few functions from ifstream. In that case, composition is appropriate because you use only part of the class. If you need every function it provides, use inheritance instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FName2:public ifstream
{
string fileName;
bool named;
public:
FName2():named(false){}
FName2(const string &fname):ifstream(fname.c_str()),fileName(fname)
{
named=true;
}
string name()const{return fileName;}
void name(const string &newName)
{
if(named)return;
fileName = newName;
named = true;
}
};

int main()
{
FName2 file("FName1.cpp");
cout << file.name() <<endl;
string s;
getline(file,s);
file.seekg(-200,ios::end);
file.close();
return 0;
}

Now every member function that works with an ifstream also works with an FName2. Non-member functions that require an ifstream, such as getline(), can use FName2 as well.

On this page
Don't know C++ (2) (inheritance and composition)