In this chapter you’ll learn:
To use const_cast
to temporarily treat a const object as a non-const
object.
To use namespaces
.
To use operator keywords.
To use mutable
members in const
objects.
To use class-member pointer operators .*
and ->*
.
To use multiple inheritance.
The role of virtual
base classes in multiple inheritance.
What’s in a name? that which we call a rose By any other name would smell as sweet.
—William Shakespeare
O Diamond! Diamond! thou little knowest the mischief done!
—Sir Isaac Newton
We now consider several advanced C++ features. First, you’ll learn about the const_cast
operator, which allows programmers to add or remove the const
qualification of a variable. Next, we discuss namespaces
, which can be used to ensure that every identifier in a program has a unique name and can help resolve naming conflicts caused by using libraries that have the same variable, function or class names. We then present several operator keywords that are useful for programmers who have keyboards that do not support certain characters used in operator symbols, such as !, &, ^, ~ and |. We continue our discussion with the mutable
storage-class specifier, which enables a programmer to indicate that a data member should always be modifiable, even when it appears in an object that is currently being treated as a const
object by the program. Next we introduce two special operators that we can use with pointers to class members to access a data member or member function without knowing its name in advance. Finally, we introduce multiple inheritance, which enables a derived class to inherit the members of several base classes. As part of this introduction, we discuss potential problems with multiple inheritance and how virtual
inheritance can be used to solve those problems.
C++ provides the const_cast
operator for casting away const or volatile qualification. You declare a variable with the volatile
qualifier when you expect the variable to be modified by hardware or other programs not known to the compiler. Declaring a variable volatile
indicates that the compiler should not optimize the use of that variable because doing so could affect the ability of those other programs to access and modify the volatile
variable.
In general, it is dangerous to use the const_cast
operator, because it allows a program to modify a variable that was declared const, and thus was not supposed to be modifiable. There are cases in which it is desirable, or even necessary, to cast away const
-ness. For example, older C and C++ libraries might provide functions that have non-const
parameters and that do not modify their parameters. If you wish to pass const data to such a function, you would need to cast away the data’s const-ness; otherwise, the compiler would report error messages.
Similarly, you could pass non-const
data to a function that treats the data as if it were constant, then returns that data as a constant. In such cases, you might need to cast away the const-ness of the returned data, as we demonstrate in Fig. 22.1.
Fig. 22.1 Demonstrating operator const_cast
.
1 // Fig. 22.1: fig22_01.cpp
2 // Demonstrating const_cast.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 #include <cstring> // contains prototypes for functions strcmp and strlen
8 #include <cctype> // contains prototype for function toupper
9
10 // returns the larger of two C-style strings
11 const char *maximum( const char *first, const char *second )
12 {
13 return ( strcmp( first, second ) >= 0 ? first : second );
14 } // end function maximum
15
16 int main()
17 {
18 char s1[] = "hello"; // modifiable array of characters
19 char s2[] = "goodbye"; // modifiable array of characters
20
21 // const_cast required to allow the const char * returned by maximum
22 // to be assigned to the char * variable maxPtr
23 char *maxPtr = const_cast< char * >( maximum( s1, s2 ) );
24
25 cout << "The larger string is: " << maxPtr << endl;
26
27 for ( size_t i = 0; i < strlen( maxPtr ); i++ )
28 maxPtr[ i ] = toupper( maxPtr[ i ] );
29
30 cout << "The larger string capitalized is: " << maxPtr << endl;
31 return 0;
32 } // end main
The larger string is: hello
The larger string capitalized is: HELLO
In this program, function maximum
(lines 11–14) receives two C-style strings as const char *
parameters and returns a const char *
that points to the larger of the two strings. Function main
declares the two C-style strings as non-const char
arrays (lines 18–19); thus, these arrays are modifiable. In main, we wish to output the larger of the two C-style strings, then modify that C-style string by converting it to uppercase letters.
Function maximum
’s two parameters are of type const char *
, so the function’s return type also must be declared as const char *
. If the return type is specified as only char *
, the compiler issues an error message indicating that the value being returned cannot be converted from const char *
to char *
—a dangerous conversion, because it attempts to treat data that the function believes to be const
as if it were non-const
data.
Even though function maximum
believes the data to be constant, we know that the original arrays in main
do not contain constant data. Therefore, main
should be able to modify the contents of those arrays as necessary. Since we know these arrays are modifiable, we use const_cast
(line 23) to cast away the const-ness of the pointer returned by maximum, so we can then modify the data in the array representing the larger of the two C-style strings. We can then use the pointer as the name of a character array in the for
statement (lines 27–28) to convert the contents of the larger string to uppercase letters. Without the const_cast
in line 23, this program will not compile, because you are not allowed to assign a pointer of type const char *
to a pointer of type char *
.
A program includes many identifiers defined in different scopes. Sometimes a variable of one scope will “overlap” (i.e., collide) with a variable of the same name in a different scope, possibly creating a naming conflict. Such overlapping can occur at many levels. Identifier overlapping occurs frequently in third-party libraries that happen to use the same names for global identifiers (such as functions). This can cause compiler errors.
Avoid identifiers that begin with the underscore character, as these can lead to linker errors. Many code libraries use names that begin with underscores.
The C++ standard solves this problem with namespaces
. Each namespace
defines a scope in which identifiers and variables are placed. To use a namespace
member, either the member’s name must be qualified with the namespace name
and the binary scope resolution operator (::)
, as in
MyNameSpace::member
or a using
declaration or using
directive must appear before the name is used in the program. Typically, such using
statements are placed at the beginning of the file in which members of the namespace
are used. For example, placing the following using
directive at the beginning of a source-code file
using namespace MyNameSpace;
specifies that members of namespace
MyNameSpace can be used in the file without preceding each member with MyNameSpace and the scope resolution operator (::)
.
A using
declaration (e.g., using std::cout;
) brings one name into the scope where the declaration appears. A using
directive (e.g., using namespace std;
) brings all the names from the specified namespace into the scope where the directive appears.
Ideally, in large programs, every entity should be declared in a class, function, block or namespace
. This helps clarify every entity’s role.
Precede a member with its namespace
name and the scope resolution operator (::
) if the possibility exists of a naming conflict.
Not all namespaces
are guaranteed to be unique. Two third-party vendors might inadvertently use the same identifiers for their namespace
names. Figure 22.2 demonstrates the use of namespaces
.
Fig. 22.2 Demonstrating the use of namespaces
.
1 // Fig. 22.2: fig22_02.cpp
2 // Demonstrating namespaces.
3 #include <iostream>
4 using namespace std; // use std namespace
5
6 int integer1 = 98; // global variable
7
8 // create namespace Example
9 namespace Example
10 {
11 // declare two constants and one variable
12 const double PI = 3.14159;
13 const double E = 2.71828;
14 int integer1 = 8;
15
16 void printValues(); // prototype
17
18 // nested namespace
19 namespace Inner
20 {
21 // define enumeration
22 enum Years { FISCAL1 = 1990, FISCAL2, FISCAL3 };
23 } // end Inner namespace
24 } // end Example namespace
25
26 // create unnamed namespace
27 namespace
28 {
29 double doubleInUnnamed = 88.22; // declare variable
30 } // end unnamed namespace
31
32 int main()
33 {
34 // output value doubleInUnnamed of unnamed namespace
35 cout << "doubleInUnnamed = " << doubleInUnnamed;
36
37 // output global variable
38 cout << "
(global) integer1 = " << integer1;
39
40 // output values of Example namespace
41 cout << "
PI = " << Example::PI << "
E = " << Example::E
42 << "
integer1 = " << Example::integer1 << "
FISCAL3 = "
43 << Example::Inner::FISCAL3 << endl;
44
45 Example::printValues(); // invoke printValues function
46 return 0;
47 } // end main
48
49 // display variable and constant values
50 void Example::printValues()
51 {
52 cout << "
In printValues:
integer1 = " << integer1 << "
PI = "
53 << PI << "
E = " << E << "
doubleInUnnamed = "
54 << doubleInUnnamed << "
(global) integer1 = " << ::integer1
55 << "
FISCAL3 = " << Inner::FISCAL3 << endl;
56 } // end printValues
doubleInUnnamed = 88.22
(global) integer1 = 98
PI = 3.14159
E = 2.71828
integer1 = 8
FISCAL3 = 1992
In printValues:
integer1 = 8
PI = 3.14159
E = 2.71828
doubleInUnnamed = 88.22
(global) integer1 = 98
FISCAL3 = 1992
Line 4 informs the compiler that namespace std
is being used. The contents of header file <iostream>
are all defined as part of namespace std. [Note: Most C++ programmers consider it poor practice to write a using
directive such as line 4 because the entire contents of the namespace are included, thus increasing the likelihood of a naming conflict.]
The using namespace
directive specifies that the members of a namespace will be used frequently throughout a program. This allows you to access all the members of the namespace and to write more concise statements such as
cout << "double1 = " << double1;
rather than
std::cout << "double1 = " << double1;
Without line 4, either every cout
and endl
in Fig. 22.2 would have to be qualified with std::, or individual using declarations must be included for cout
and endl
as in:
using std::cout;
using std::endl;
The using namespace
directive can be used for predefined namespaces (e.g., std
) or programmer-defined namespaces.
Lines 9–24 use the keyword namespace
to define namespace Example
. The body of a namespace is delimited by braces ({}
). Namespace Example
’s members consist of two constants (PI
and E
in lines 12–13), an int
(integer1
in line 14), a function (printValues
in line 16) and a nested namespace (Inner
in lines 19–23). Notice that member integer1
has the same name as global variable integer1
(line 6). Variables that have the same name must have different scopes—otherwise compilation errors occur. A namespace can contain constants, data, classes, nested namespaces, functions, etc. Definitions of namespaces must occupy the global scope or be nested within other namespaces.
Lines 27–30 create an unnamed namespace containing the member doubleInUnnamed
. The unnamed namespace has an implicit using
directive, so its members appear to occupy the global namespace, are accessible directly and do not have to be qualified with a namespace name. Global variables are also part of the global namespace and are accessible in all scopes following the declaration in the file.
Line 35 outputs the value of variable doubleInUnnamed
, which is directly accessible as part of the unnamed namespace. Line 38 outputs the value of global variable integer1
. For both of these variables, the compiler first attempts to locate a local declaration of the variables in main
. Since there are no local declarations, the compiler assumes those variables are in the global namespace.
Lines 41–43 output the values of PI, E, integer1
and FISCAL3
from namespace Example
. Notice that each must be qualified with Example::
because the program does not provide any using
directive or declarations indicating that it will use members of namespace Example. In addition, member integer1
must be qualified, because a global variable has the same name. Otherwise, the global variable’s value is output. Notice that FISCAL3
is a member of nested namespace Inner, so it must be qualified with Example::Inner::
.
Function printValues
(defined in lines 50–56) is a member of Example
, so it can access other members of the Example
namespace directly without using a namespace qualifier. The output statement in lines 52–55 outputs integer1, PI, E, doubleInUnnamed
, global variable integer1
and FISCAL3
. Notice that PI
and E
are not qualified with Example
. Variable doubleInUnnamed
is still accessible, because it is in the unnamed namespace and the variable name does not conflict with any other members of namespace Example. The global version of integer1
must be qualified with the unary scope resolution operator (::
), because its name conflicts with a member of namespace Example
. Also, FISCAL3
must be qualified with Inner::
. When accessing members of a nested namespace, the members must be qualified with the namespace name (unless the member is being used inside the nested namespace).
Namespaces can be aliased. For example the statement
namespace CPPFP = CPlusPlusForProgrammers;
creates the alias CPPFP
for CPlusPlusForProgrammers
.
The C++ standard provides operator keywords (Fig. 22.3) that can be used in place of several C++ operators. Operator keywords are useful for programmers who have keyboards that do not support certain characters such as !
, &
, ^
, ~
, |
, etc.
Fig. 22.3 Operator keyword alternatives to operator symbols.
Operator |
Operator keyword |
Description |
---|---|---|
Logical operator keywords |
||
|
and |
logical AND |
|
or |
logical OR |
|
not |
logical NOT |
Inequality operator keyword |
||
|
not_eq |
inequality |
Bitwise operator keywords |
||
|
bitand |
bitwise AND |
|
bitor |
bitwise inclusive OR |
|
xor |
bitwise exclusive OR |
|
compl |
bitwise complement |
Bitwise assignment operator keywords |
||
|
and_eq |
bitwise AND assignment |
|
or_eq |
bitwise inclusive OR assignment |
|
xor_eq |
bitwise exclusive OR assignment |
Figure 22.4 demonstrates the operator keywords. This program was compiled with Microsoft Visual C++ 2005, which requires the header file <iso646.h>
(line 8) to use the operator keywords. In GNU C++, line 8 should be removed and the program should be compiled as follows:
g++ -foperator-names fig22_04.cpp -o fig22_04
The compiler option -foperator-names
indicates that the compiler should enable use of the operator keywords in Fig. 22.3. Other compilers may not require you to include a header file or to use a compiler option to enable support for these keywords. For example, the Borland C++ 5.6.4 compiler implicitly permits these keywords.
Fig. 22.4 Demonstrating the operator keywords.
1 // Fig. 22.4: fig22_04.cpp
2 // Demonstrating operator keywords.
3 #include <iostream>
4 using std::boolalpha;
5 using std::cout;
6 using std::endl;
7
8 #include <iso646.h> // enables operator keywords in Microsoft Visual C++
9
10 int main()
11 {
12 bool a = true;
13 bool b = false;
14 int c = 2;
15 int d = 3;
16
17 // sticky setting that causes bool values to display as true or false
18 cout << boolalpha;
19
20 cout << "a = " << a << "; b = " << b
21 << "; c = " << c << "; d = " << d;
22
23 cout << "
Logical operator keywords:";
24 cout << "
a and a: " << ( a and a );
25 cout << "
a and b: " << ( a and b );
26 cout << "
a or a: " << ( a or a );
27 cout << "
a or b: " << ( a or b );
28 cout << "
not a: " << ( not a );
29 cout << "
not b: " << ( not b );
30 cout << "
a not_eq b: " << ( a not_eq b );
31
32 cout << "
Bitwise operator keywords:";
33 cout << "
c bitand d: " << ( c bitand d );
34 cout << "
c bit_or d: " << ( c bitor d );
35 cout << "
c xor d: " << ( c xor d );
36 cout << "
compl c: " << ( compl c );
37 cout << "
c and_eq d: " << ( c and_eq d );
38 cout << "
c or_eq d: " << ( c or_eq d );
39 cout << "
c xor_eq d: " << ( c xor_eq d ) << endl;
40 return 0;
41 } // end main
The program declares and initializes two bool
variables and two integer variables (lines 12–15). Logical operations (lines 24–30) are performed with bool
variables a
and b
using the various logical operator keywords. Bitwise operations (lines 33–39) are performed with the int
variables c
and d
using the various bitwise operator keywords. The result of each operation is output.
In Section 22.2, we introduced the const_cast
operator, which allowed us to remove the “const-ness” of a type. A const_cast
operation can also be applied to a data member of a const
object from the body of a const
member function of that object’s class. This enables the const
member function to modify the data member, even though the object is considered to be const
in the body of that function. Such an operation might be performed when most of an object’s data members should be considered const
, but a particular data member still needs to be modified.
As an example, consider a linked list that maintains its contents in sorted order. Searching through the linked list does not require modifications to the data of the linked list, so the search function could be a const
member function of the linked-list class. However, it is conceivable that a linked-list object, in an effort to make future searches more efficient, might keep track of the location of the last successful match. If the next search operation attempts to locate an item that appears later in the list, the search could begin from the location of the last successful match, rather than from the beginning of the list. To do this, the const
member function that performs the search must be able to modify the data member that keeps track of the last successful search.
If a data member such as the one described above should always be modifiable, C++ provides the storage-class specifier mutable
as an alternative to const_cast.
A mutable data member is always modifiable, even in a const
member function or const
object. This reduces the need to cast away “const
-ness.”
The effect of attempting to modify an object that was defined as constant, regardless of whether that modification was made possible by a const_cast
or C-style cast, varies among compilers.
mutable
and const_cast
are used in different contexts. For a const
object with no mutable
data members, operator const_cast
must be used every time a member is to be modified. This greatly reduces the chance of a member being accidentally modified because the member is not permanently modifiable. Operations involving const_cast
are typically hidden in a member function’s implementation. The user of a class might not be aware that a member is being modified.
mutable
members are useful in classes that have “secret” implementation details that do not contribute to the logical value of an object.
Figure 22.5 demonstrates using a mutable
member. The program defines class TestMutable
(lines 8–22), which contains a constructor, function getValue
and a private
data member value
that is declared mutable
. Lines 16–19 define function getValue
as a const
member function that returns a copy of value
. Notice that the function increments mutable
data member value
in the return statement. Normally, a const member function cannot modify data members unless the object on which the function operates—i.e., the one to which this
points—is cast (using const_cast
) to a non-const
type. Because value
is mutable
, this const
function is able to modify the data.
Fig. 22.5 Demonstrating a mutable
data member.
1 // Fig. 22.5: fig22_05.cpp
2 // Demonstrating storage-class specifier mutable.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 // class TestMutable definition
8 class TestMutable
9 {
10 public:
11 TestMutable( int v = 0 )
12 {
13 value = v;
14 } // end TestMutable constructor
15
16 int getValue() const
17 {
18 return value++; // increments value
19 } // end function getValue
20 private:
21 mutable int value; // mutable member
22 }; // end class TestMutable
23
24 int main()
25 {
26 const TestMutable test( 99 );
27
28 cout << "Initial value: " << test.getValue();
29 cout << "
Modified value: " << test.getValue() << endl;
30 return 0;
31 } // end main
Initial value: 99
Modified value: 100
Line 26 declares const TestMutable
object test
and initializes it to 99
. Line 28 calls the const
member function getValue
, which adds one to value
and returns its previous contents. Notice that the compiler allows the call to member function getValue
on the object test
because it is a const
object and getValue
is a const
member function. However, getValue
modifies variable value. Thus, when line 29 invokes getValue
again, the new value (100)
is output to prove that the mutable
data member was indeed modified.
C++ provides the .*
and ->*
operators for accessing class members via pointers. This is a rarely used capability that is used primarily by advanced C++ programmers. We provide only a mechanical example of using pointers to class members here. Figure 22.6 demonstrates the pointer-to-class-member operators.
Fig. 22.6 Demonstrating the .*
and ->*
operators.
1 // Fig. 22.6: fig22_06.cpp
2 // Demonstrating operators .* and ->*.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 // class Test definition
8 class Test
9 {
10 public:
11 void test()
12 {
13 cout << "In test function
";
14 } // end function test
15
16 int value; // public data member
17 }; // end class Test
18
19 void arrowStar( Test * ); // prototype
20 void dotStar( Test * ); // prototype
21
22 int main()
23 {
24 Test test;
25 test.value = 8; // assign value 8
26 arrowStar( &test ); // pass address to arrowStar
27 dotStar( &test ); // pass address to dotStar
28 return 0;
29 } // end main
30
31 // access member function of Test object using ->*
32 void arrowStar( Test *testPtr )
33 {
34 void ( Test::*memPtr )() = &Test::test; // declare function pointer
35 ( testPtr->*memPtr )(); // invoke function indirectly
36 } // end arrowStar
37
38 // access members of Test object data member using .*
39 void dotStar( Test *testPtr2 )
40 {
41 int Test::*vPtr = &Test::value; // declare pointer
42 cout << ( *testPtr2 ).*vPtr << endl; // access value
43 } // end dotStar
In test function
8
The program declares class Test
(lines 8–17), which provides public
member function test
and public
data member value
. Lines 19–20 provide prototypes for the functions arrowStar
(defined in lines 32–36) and dotStar
(defined in lines 39–43), which demonstrate the ->*
and .*
operators, respectively. Lines 24 creates object test
, and line 25 assigns 8 to its data member value
. Lines 26–27 call functions arrowStar
and dotStar
with the address of the object test
.
Line 34 in function arrowStar
declares and initializes variable memPtr
as a pointer to a member function. In this declaration, Test::*
indicates that the variable memPtr
is a pointer to a member of class Test
. To declare a pointer to a function, enclose the pointer name preceded by *
in parentheses, as in ( Test::*memPtr
). A pointer to a function must specify, as part of its type, both the return type of the function it points to and the parameter list of that function. The function’s return type appears to the left of the left parenthesis and the parameter list appears in a separate set of parentheses to the right of the pointer declaration. In this case, the function has a void
return type and no parameters. The pointer memPtr
is initialized with the address of class Test
’s member function named test
. Note that the header of the function must match the function pointer’s declaration—i.e., function test
must have a void
return type and no parameters. Notice that the right side of the assignment uses the address operator (&) to get the address of the member function test
. Also, notice that neither the left side nor the right side of the assignment in line 34 refers to a specific object of class Test
. Only the class name is used with the binary scope resolution operator (::
). Line 35 invokes the member function stored in memPtr
(i.e., test
), using the ->*
operator. Because memPtr
is a pointer to a member of a class, the ->*
operator must be used rather than the ->
operator to invoke the function.
Line 41 declares and initializes vPtr
as a pointer to an int
data member of class Test
. The right side of the assignment specifies the address of the data member value. Line 42 dereferences the pointer testPtr2
, then uses the .*
operator to access the member to which vPtr
points. Note that the client code can create pointers to class members for only those class members that are accessible to the client code. In this example, both member function test
and data member value
are publicly accessible.
Declaring a member-function pointer without enclosing the pointer name in parentheses is a syntax error.
Declaring a member-function pointer without preceding the pointer name with a class name followed by the scope resolution operator (::
) is a syntax error.
Attempting to use the ->
or *
operator with a pointer to a class member generates syntax errors.
In Chapters 12 and 13, we discussed single inheritance, in which each class is derived from exactly one base class. In C++, a class may be derived from more than one base class—a technique known as multiple inheritance in which a derived class inherits the members of two or more base classes. This powerful capability encourages interesting forms of software reuse but can cause a variety of ambiguity problems. Multiple inheritance is a difficult concept that should be used only by experienced programmers. In fact, some of the problems associated with multiple inheritance are so subtle that newer programming languages, such as Java and C#
, do not enable a class to derive from more than one base class.
Multiple inheritance is a powerful capability when used properly. Multiple inheritance should be used when an is-a relationship exists between a new type and two or more existing types (i.e., type A is a type B and type A is a type C).
Multiple inheritance can introduce complexity into a system. Great care is required in the design of a system to use multiple inheritance properly; it should not be used when single inheritance and/or composition will do the job.
A common problem with multiple inheritance is that each of the base classes might contain data members or member functions that have the same name. This can lead to ambiguity problems when you attempt to compile. Consider the multiple-inheritance example (Fig. 22.7, Fig. 22.8, Fig. 22.9, Fig. 22.10, Fig. 22.11). Class Base1
(Fig. 22.7) contains one protected int
data member—value
(line 20), a constructor (lines 10–13) that sets value
and public
member function getData
(lines 15–18) that returns value.
Fig. 22.7 Demonstrating multiple inheritance—Base1.h
.
1 // Fig. 22.7: Base1.h
2 // Definition of class Base1
3 #ifndef BASE1_H
4 #define BASE1_H
5
6 // class Base1 definition
7 class Base1
8 {
9 public:
10 Base1( int parameterValue )
11 {
12 value = parameterValue;
13 } // end Base1 constructor
14
15 int getData() const
16 {
17 return value;
18 } // end function getData
19 protected: // accessible to derived classes
20 int value; // inherited by derived class
21 }; // end class Base1
22
23 #endif // BASE1_H
Class Base2
(Fig. 22.8) is similar to class Base1
, except that its protected
data is a char
named letter
(line 20). Like class Base1, Base2
has a public
member function getData
, but this function returns the value of char
data member letter
.
Fig. 22.8 Demonstrating multiple inheritance—Base2.h.
1 // Fig. 22.8: Base2.h
2 // Definition of class Base2
3 #ifndef BASE2_H
4 #define BASE2_H
5
6 // class Base2 definition
7 class Base2
8 {
9 public:
10 Base2( char characterData )
11 {
12 letter = characterData;
13 } // end Base2 constructor
14
15 char getData() const
16 {
17 return letter;
18 } // end function getData
19 protected: // accessible to derived classes
20 char letter; // inherited by derived class
21 }; // end class Base2
22
23 #endif // BASE2_H
Class Derived
(Figs. 22.9–22.10) inherits from both class Base1
and class Base2
through multiple inheritance. Class Derived
has a private
data member of type double
named real
(line 21), a constructor to initialize all the data of class Derived
and a public
member function getReal
that returns the value of double
variable real
.
Fig. 22.9 Demonstrating multiple inheritance—Derived.h.
1 // Fig. 22.9: Derived.h
2 // Definition of class Derived which inherits
3 // multiple base classes (Base1 and Base2).
4 #ifndef DERIVED_H
5 #define DERIVED_H
6
7 #include <iostream>
8 using std::ostream;
9
10 #include "Base1.h"
11 #include "Base2.h"
12
13 // class Derived definition
14 class Derived : public Base1, public Base2
15 {
16 friend ostream &operator<<( ostream &, const Derived & );
17 public:
18 Derived( int, char, double );
19 double getReal() const;
20 private:
21 double real; // derived class's private data
22 }; // end class Derived
23
24 #endif // DERIVED_H
To indicate multiple inheritance we follow the colon (:)
after class Derived
with a comma-separated list of base classes (line 14). In Fig. 22.10, notice that constructor Derived
explicitly calls base-class constructors for each of its base classes—Base1
and Base2
—using the member-initializer syntax (line 9). The base-class constructors are called in the order that the inheritance is specified, not in the order in which their constructors are mentioned; also, if the base-class constructors are not explicitly called in the member-initializer list, their default constructors will be called implicitly.
Fig. 22.10 Demonstrating multiple inheritance—Derived.cpp
.
1 // Fig. 22.10: Derived.cpp
2 // Member-function definitions for class Derived
3 #include "Derived.h"
4
5 // constructor for Derived calls constructors for
6 // class Base1 and class Base2.
7 // use member initializers to call base-class constructors
8 Derived::Derived( int integer, char character, double double1 )
9 : Base1( integer ), Base2( character ), real( double1 ) { }
10
11 // return real
12 double Derived::getReal() const
13 {
14 return real;
15 } // end function getReal
16
17 // display all data members of Derived
18 ostream &operator<<( ostream &output, const Derived &derived )
19 {
20 output << " Integer: " << derived.value << "
Character: "
21 << derived.letter << "
Real number: " << derived.real;
22 return output; // enables cascaded calls
23 } // end operator<<
The overloaded stream insertion operator (Fig. 22.10, lines 18–23) uses its second parameter—a reference to a Derived
object—to display a Derived
object’s data. This operator function is a friend
of Derived
, so operator<<
can directly access all of class Derived
’s protected
and private
members, including the protected
data member value
(inherited from class Base1
), protected
data member letter
(inherited from class Base2
) and private
data member real
(declared in class Derived
).
Now let us examine the main
function (Fig. 22.11) that tests the classes in Figs. 22.7–22.10. Line 13 creates Base1
object base1
and initializes it to the int
value 10
, then creates the pointer base1Ptr
and initializes it to the null pointer (i.e., 0). Line 14 creates Base2
object base2
and initializes it to the char
value 'Z'
, then creates the pointer base2Ptr
and initializes it to the null pointer. Line 15 creates Derived
object derived
and initializes it to contain the int
value 7
, the char
value 'A'
and the double
value 3.5
.
Fig. 22.11 Demonstrating multiple inheritance.
1 // Fig. 22.11: fig22_11.cpp
2 // Driver for multiple-inheritance example.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 #include "Base1.h"
8 #include "Base2.h"
9 #include "Derived.h"
10
11 int main()
12 {
13 Base1 base1( 10 ), *base1Ptr = 0; // create Base1 object
14 Base2 base2( 'Z' ), *base2Ptr = 0; // create Base2 object
15 Derived derived( 7, 'A', 3.5 ); // create Derived object
16
17 // print data members of base-class objects
18 cout << "Object base1 contains integer " << base1.getData()
19 << "
Object base2 contains character " << base2.getData()
20 << "
Object derived contains:
" << derived << "
";
21
22 // print data members of derived-class object
23 // scope resolution operator resolves getData ambiguity
24 cout << "Data members of Derived can be accessed individually:"
25 << "
Integer: " << derived.Base1::getData()
26 << "
Character: " << derived.Base2::getData()
27 << "
Real number: " << derived.getReal() << "
";
28 cout << "Derived can be treated as an object of either base class:
";
29
30 // treat Derived as a Base1 object
31 base1Ptr = &derived;
32 cout << "base1Ptr->getData() yields " << base1Ptr->getData() << '
';
33
34 // treat Derived as a Base2 object
35 base2Ptr = &derived;
36 cout << "base2Ptr->getData() yields " << base2Ptr->getData() << endl;
37 return 0;
38 } // end main
Object base1 contains integer 10
Object base2 contains character Z
Object derived contains:
Integer: 7
Character: A
Real number: 3.5
Data members of Derived can be accessed individually:
Integer: 7
Character: A
Real number: 3.5
Derived can be treated as an object of either base class:
base1Ptr->getData() yields 7
base2Ptr->getData() yields A
Lines 18–20 display each object’s data values. For objects base1
and base2
, we invoke each object’s getData
member function. Even though there are two getData
functions in this example, the calls are not ambiguous. In line 18, the compiler knows that base1
is an object of class Base1
, so class Base1
’s getData
is called. In line 19, the compiler knows that base2
is an object of class Base2
, so class Base2
’s getData
is called. Line 20 displays the contents of object derived
using the overloaded stream insertion operator.
Lines 24–27 output the contents of object derived
again by using the get member functions of class Derived
. However, there is an ambiguity problem, because this object contains two getData
functions, one inherited from class Base1
and one inherited from class Base2
. This problem is easy to solve by using the binary scope resolution operator. The expression derived.Base1::getData()
gets the value of the variable inherited from class Base1
(i.e., the int
variable named value
) and derived.Base2::getData()
gets the value of the variable inherited from class Base2
(i.e., the char
variable named letter
). The double
value in real
is printed without ambiguity with the call derived.getReal()
—there are no other member functions with that name in the hierarchy.
The is-a relationships of single inheritance also apply in multiple-inheritance relationships. To demonstrate this, line 31 assigns the address of object derived
to the Base1
pointer base1Ptr
. This is allowed because an object of class Derived
is an object of class Base1
. Line 32 invokes Base1
member function getData
via base1Ptr
to obtain the value of only the Base1
part of the object derived
. Line 35 assigns the address of object derived
to the Base2
pointer base2Ptr
. This is allowed because an object of class Derived
is an object of class Base2
. Line 36 invokes Base2
member function getData
via base2Ptr
to obtain the value of only the Base2
part of the object derived
.
In Section 22.7, we discussed multiple inheritance, the process by which one class inherits from two or more classes. Multiple inheritance is used, for example, in the C++ standard library to form class basic_iostream
(Fig. 22.12).
Class basic_ios
is the base class for both basic_istream
and basic_ostream
, each of which is formed with single inheritance. Class basic_iostream
inherits from both basic_istream
and basic_ostream
. This enables class basic_iostream
objects to provide the functionality of basic_istream
s and basic_ostream
s. In multiple-inheritance hierarchies, the situation described in Fig. 22.12 is referred to as diamond inheritance.
Because classes basic_istream
and basic_ostream
each inherit from basic_ios
, a potential problem exists for basic_iostream
. Class basic_iostream
could contain two copies of the members of class basic_ios
—one inherited via class basic_istream
and one inherited via class basic_ostream
). Such a situation would be ambiguous and would result in a compilation error, because the compiler would not know which version of the members from class basic_ios
to use. Of course, basic_iostream
does not really suffer from the problem we mentioned. In this section, you’ll see how using virtual
base classes solves the problem of inheriting duplicate copies of an indirect base class.
Figure 22.13 demonstrates the ambiguity that can occur in diamond inheritance. The program defines class Base
(lines 9–13), which contains pure virtual
function print
(line 12). Classes DerivedOne
(lines 16–24) and DerivedTwo
(lines 27–35) each publicly inherit from class Base
and override the print
function. Class DerivedOne
and class DerivedTwo
each contain what the C++ standard refers to as a base-class subobject—i.e., the members of class Base
in this example.
Fig. 22.13 Attempting to call a multiply inherited function polymorphically.
1 // Fig. 22.13: fig22_13.cpp
2 // Attempting to polymorphically call a function that is
3 // multiply inherited from two base classes.
4 #include <iostream>
5 using std::cout;
6 using std::endl;
7
8 // class Base definition
9 class Base
10 {
11 public:
12 virtual void print() const = 0; // pure virtual
13 }; // end class Base
14
15 // class DerivedOne definition
16 class DerivedOne : public Base
17 {
18 public:
19 // override print function
20 void print() const
21 {
22 cout << "DerivedOne
";
23 } // end function print
24 }; // end class DerivedOne
25
26 // class DerivedTwo definition
27 class DerivedTwo : public Base
28 {
29 public:
30 // override print function
31 void print() const
32 {
33 cout << "DerivedTwo
";
34 } // end function print
35 }; // end class DerivedTwo
36
37 // class Multiple definition
38 class Multiple : public DerivedOne, public DerivedTwo
39 {
40 public:
41 // qualify which version of function print
42 void print() const
43 {
44 DerivedTwo::print();
45 } // end function print
46 }; // end class Multiple
47
48 int main()
49 {
50 Multiple both; // instantiate Multiple object
51 DerivedOne one; // instantiate DerivedOne object
52 DerivedTwo two; // instantiate DerivedTwo object
53 Base *array[ 3 ]; // create array of base-class pointers
54
55 array[ 0 ] = &both; // ERROR--ambiguous
56 array[ 1 ] = &one;
57 array[ 2 ] = &two;
58
59 // polymorphically invoke print
60 for ( int i = 0; i < 3; i++ )
61 array[ i ] -> print();
62
63 return 0;
64 } // end main
Class Multiple
(lines 38–46) inherits from both classes DerivedOne
and DerivedTwo
. In class Multiple
, function print
is overridden to call DerivedTwo
’s print
(line 44). Notice that we must qualify the print
call with the class name DerivedTwo
to specify which version of print
to call.
Function main
(lines 48–64) declares objects of classes Multiple
(line 50), DerivedOne
(line 51) and DerivedTwo
(line 52). Line 53 declares an array of Base *
pointers. Each array element is initialized with the address of an object (lines 55–57). An error occurs when the address of both
—an object of class Multiple
—is assigned to array[ 0 ]
. The object both
actually contains two subobjects of type Base
, so the compiler does not know which subobject the pointer array[ 0 ]
should point to, and it generates a compilation error indicating an ambiguous conversion.
The problem of duplicate subobjects is resolved with virtual
inheritance. When a base class is inherited as virtual
, only one subobject will appear in the derived class—a process called virtual
base-class inheritance. Figure 22.14 revises the program of Fig. 22.13 to use a virtual
base class.
Fig. 22.14 Using virtual
base classes.
1 // Fig. 22.14: fig22_14.cpp
2 // Using virtual base classes.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 // class Base definition
8 class Base
9 {
10 public:
11 virtual void print() const = 0; // pure virtual
12 }; // end class Base
13
14 // class DerivedOne definition
15 class DerivedOne : virtual public Base
16 {
17 public:
18 // override print function
19 void print() const
20 {
21 cout << "DerivedOne
";
22 } // end function print
23 }; // end DerivedOne class
24
25 // class DerivedTwo definition
26 class DerivedTwo : virtual public Base
27 {
28 public:
29 // override print function
30 void print() const
31 {
32 cout << "DerivedTwo
";
33 } // end function print
34 }; // end DerivedTwo class
35
36 // class Multiple definition
37 class Multiple : public DerivedOne, public DerivedTwo
38 {
39 public:
40 // qualify which version of function print
41 void print() const
42 {
43 DerivedTwo::print();
44 } // end function print
45 }; // end Multiple class
46
47 int main()
48 {
49 Multiple both; // instantiate Multiple object
50 DerivedOne one; // instantiate DerivedOne object
51 DerivedTwo two; // instantiate DerivedTwo object
52
53 // declare array of base-class pointers and initialize
54 // each element to a derived-class type
55 Base *array[ 3 ];
56 array[ 0 ] = &both;
57 array[ 1 ] = &one;
58 array[ 2 ] = &two;
59
60 // polymorphically invoke function print
61 for ( int i = 0; i < 3; i++ )
62 array[ i ]->print();
63
64 return 0;
65 } // end main
DerivedTwo
DerivedOne
DerivedTwo
The key change in the program is that classes DerivedOne
(line 15) and DerivedTwo
(line 26) each inherit from class Base
by specifying virtual public Base
. Since both of these classes inherit from Base
, they each contain a Base
subobject. The benefit of virtual
inheritance is not clear until class Multiple
inherits from both DerivedOne
and DerivedTwo
(line 37). Since each of the base classes used virtual
inheritance to inherit class Base
’s members, the compiler ensures that only one subobject of type Base
is inherited into class Multiple
. This eliminates the ambiguity error generated by the compiler in Fig. 22.13. The compiler now allows the implicit conversion of the derived-class pointer (&both
) to the base-class pointer array[ 0 ]
in line 56 in main
. The for
statement in lines 61–62 polymorphically calls print
for each object.
Implementing hierarchies with virtual
base classes is simpler if default constructors are used for the base classes. The examples in Figs. 22.13 and 22.14 use compiler-generated default constructors. If a virtual
base class provides a constructor that requires arguments, the implementation of the derived classes becomes more complicated, because the most derived class must explicitly invoke the virtual base class’s constructor to initialize the members inherited from the virtual
base class.
Multiple inheritance is a complex topic typically covered in more advanced C++ texts. For more information on multiple inheritance, please visit our C++ Resource Center at
In the C++ Multiple Inheritance category, you’ll find links to several articles and resources, including a multiple inheritance FAQ and tips for using multiple inheritance.
In this chapter, you learned how to use the const_cast
operator to remove the const
qualification of a variable. We then showed how to use namespace
s to ensure that every identifier in a program has a unique name and explained how they can help resolve naming conflicts. You saw several operator keywords for programmers whose keyboards do not support certain characters used in operator symbols, such as !
, &
, ^
, ~
and |
. Next, we showed how the mutable
storage-class specifier enables a programmer to indicate that a data member should always be modifiable, even when it appears in an object that is currently being treated as a const
. We also showed the mechanics of using pointers to class members and the ->*
and .*
operators. Finally, we introduced multiple inheritance and discussed problems associated with allowing a derived class to inherit the members of several base classes. As part of this discussion, we demonstrated how virtual
inheritance can be used to solve those problems.