Dan Saks is the founder and principal of Saks & Associates, which offers consulting and training in C++ and C. He is secretary of the ANSI and ISO C++ committees, and contributing editor for the Windows/DOS Developer's Journal. Dan is coauthor of C++ Programming Guidelines, and codeveloper of the Plum Hall Validation Suite for C++ (both with Thomas Plum). You can reach him at 393 Leander Dr., Springfield OH, 45504,4906, by phone at (513)324-3601, or electronically at dsaks@wittenberg.edu.
Although they don't always agree on the exact meaning of the terms, most people who know something about object-oriented programming agree that it employs at least three techniques:
Thus far in my column, I've only covered C++ features that support data abstraction namely, classes, access specifiers, constructors and destructors, and features such as references and operator overloading that help you build more intuitive abstractions. In this article, I'll introduce inheritance.
- Data abstraction
- Inheritance
- Polymorphism
Some of you may be wondering why I've waited so long to deal with inheritance. I think inheritance is useful, but not nearly as useful as classes and access specifiers. Many programmers overrate its value and consequently misuse it. I use inheritance sparingly in my own work.
The other reason I've postponed discussing inheritance is that inheritance is a technique for defining new classes from existing classes. As such, understanding inheritance requires an understanding of all of those features I listed above. Now that I've covered them, I'll take on inheritance.
The Basics
Inheritance is a technique for creating a new class, called the derived class, from an existing class, called the base class. The derived class definition looks like any other base class definition, except for the presence of a base class specifier appearing immediately after the derived class name. For example, the definition
class D : public B { //... };includes the base class specifier : public B, so that class D is derived from (a previously defined) base class B. The keyword public after the colon (:) indicates that B is a public base class of D, or conversely, that D is publicly-derived from B. Base classes can also be private or protected. For the moment, I will only consider public base classes.The derived class inherits nearly all the members (data and functions) of the base class even though the derived class definition doesn't even mention the inherited members. You add more members to a derived class by simply declaring them inside the class body. For example, if you define class B as
class B { public: int f(); void g(int); private: int i, j; };then
class D : public B { public: double h(double); private: double x; };defines class D inheriting the members of B aing a public member function h(double) and a private data member x. The inherited public members functions f() and g(int) become public members of D. The inherited private members data members i and j become part of class D, but remain private to the B part of D. A D member function, like h(double), can only access i and j via public members (and friends, if any) of B. You may be surprised by this restriction, but if derived classes could directly access private base class members, then anyone could violate the encapsulation of a class by simply deriving another class from it.A derived class can be a base class for further derived classes. For example,
class F : public D { public: char *p; };derives class F from D, adding a public data member p to F.
Is-A Relationships
Inheritance provides a simple, explicit notation for creating specialized versions of broader classes. Inheritance defines an Is-A relationship (some people prefer Is-A-Kind-Of for added clarity): an object of a derived class is an object of the base class. A derived class object has everything that a base class object has, and usually more. It never has less.For example, Stroustrup (1991) introduces inheritance by sketching an example dealing with employees and their managers. Class employee defines the representation for each employee, with data such as name, age, and salary. Since a manager is an employee (with additional powers and responsibilities), Stroustrup derives class manager from class employee:
class manager : public employee { // additional members that // distinguish managers from // other employees };Since a manager is an employee, any function f with a formal parameter of type employee or employee & will accept an actual argument of type manager. For example, given
void f(employee &e); manager m;the call f(m) binds the formal parameter e to actual argument m. Inside f, e appears to refer to an employee, which it does. It just happens that in this particular call, e refers to an employee that is also a manager. But f can't tell that e actually refers to a manager; it can only access the employee part of the object.As a general rule, whenever class B is a public base of class D, you can:
1. Convert a D * to a B * (a pointer to a derived object to a pointer to a base object)
2. Convert a D & to a B & (a reference to a derived object to a reference to a base object)
3. Initialize a B & to refer to a D
These conversions are transitive. That is, if class D is a public base of class F, then you can convert an F * to either a D *or a B *, or an F & to either a D & or a B &. For example, given
class B {...}; class D : public B {...}; class F : public D {...}; B b; D d; F f;then all of the following operations are valid:
B *pb = &d; // ok, a D* is a B* D *pd = &f; // ok, an F* is a D* pb = &f; // ok, an F* is a D*, which is a B* D &rd = f; // ok, an F is a D B &rb1 = rd; // ok, a D is a B B &rb2 = f; // ok, an F is a D, which is a BAlthough an object of a derived class is an object of its base class, the opposite is not true. Thus, you cannot convert a pointer (or reference) to a base object to a pointer (or reference) to a derived object, unless you use a cast. For example, given the immediately preceding declarations, then
pd = pb; // error, a B* is not a D* pd = (D *)pb; // ok, but suspect F &f = b; // error, a B is not an FIn general, C++ also lets you initialize a base class object with a derived class object, as in
D d; //... B b(d);or simply assign a derived class object to a base class object, as in
b = (d);(I detailed the differences between initialization and assignment in "Initialization vs. Assignment," CUJ, September 1992.) Both the initialization and the assignment copy only the inherited B members from d to b.The ARM (Ellis & Stroustrup 1990) doesn't specifically permit conversion from a derived class object to a base class object, but it falls out from the reference conversions. When you write
B b(d);C++ translates this to a call to B's copy constructor, typically declared as
B::B(const B &br);The constructor call binds formal parameter br (a reference to a base object) to actual argument d (a derived class object), as permitted by in rule 3 previously mentioned. Similarly, when you write the assignment
b = d;C++ translates this to a call to B's assignment operator:
B &B::operator=(const B &br);Again, calling this assignment operator binds br to d, employing the same reference conversion.
How It Works
Some insight into how C++ implements inheritance may help you understand and remember the conversion rules a bit better. Although C++ imposes some restrictions on the storage layout for derived class objects, it doesn't require that an implementation use any particular layout strategy.Figure 1 shows a simple base class B and a typical storage layout for an object of class B. Notice that only the data fields occupy storage in the object. C++ resolves B's member function calls at translation time, so it need not store any information about the member functions in the object.
Figure 2 shows a typical layout for a class D derived from class B in Figure 1. The B sub-object (the B part) occupies the beginning (the lowest addresses) of a D object. Thus, converting a pointer (or reference) to a D into a pointer (or reference) to a B doesn't require any generated code; it's strictly a compile-time transformation.
Calling a function,
void f(B &br);with an actual argument d of type D binds br to the B part of d. Typical generated code simply assigns the address of d to the pointer that implements br. No pointer arithmetic or indirection is needed.The body of the function can't tell whether br is bound to a B or to an object of a class derived from B. But, since all classes derived from B have at least everything that B has, f can safely access all members of br. That there might be more members beyond the fringes of B is not a concern.
This model also illustrates why you generally can't convert in the opposite direction, that is, from pointer (or reference) to base to pointer (or reference) to derived. For example, calling a function,
void g(D &dr);with an actual argument b of type B attempts to bind dr to b. But, referring to Figure 1 and Figure 2, a B object doesn't have an x member, so accessing dr.x inside g would reach beyond the end of b into uncharted territory. Thus, converting from base to derived violates the type safety rules in C++. You can't make the call without casting the actual argument b to type D.
Overriding
A derived class can redefine a function inherited from a base case, as shown in Listing 1. Here, the base class B defines two functions, f and g. The derived class D inherits both functions from B, but then replaces g with a definition of its own. This replacement is called overriding.Figure 3 shows the output from the program in Listing 1. Calling d.f calls the f inherited from B. The translator need not generate any new code for D's f; it can simply invoke the code already generated for B's f, passing the address of d's B sub-object as the value of this. However, since D's g overrides the g inherited from B, calling d.g calls a different function than calling b.g.
When a derived class overrides an inherited public function, that function is hidden, but not completely inaccessible. The derived class can still access the hidden member by using the scope resolution operator, ::, and explicitly qualifying the member name with the base class name, as shown in Listing 2. In this example, derived class D overrides inherited functions and g and h. The call to B::g inside D's g calls B's g, as shown in the program output in Figure 4. Without the qualifier B::, a call to g inside D's g would be a recursive call to D's g.
The body of D::h in Listing 2 shows another technique for calling an overridden member function using a cast. Remember that, inside the body of a member function, a call to a member function like h is actually a call to this->h. In a D member function, the type of is D const *. By casting this to B * (which is a valid conversion from pointer to derived to pointer to base) inside D::h, I forced the translator to look for h in the scope of B, and bypass looking in the scope of B. It works, but I recommend avoiding the cast and using the scope resolution operator as I did in D::g.
A derived class can override inherited data members as well as function members. For example, consider
class B { public: int n; // ... }; class D : public B { public: long n; void f(); // ... };An object d of class D has storage for both an int n and a long n. Inside D::f, an unqualified reference to n refers to D::n (the long); B::n refers to the inherited int n.A derived class cannot delete inherited members.
An Example
In my last two articles I described and implemented several versions of class float_array, an array of float for which you can set the number of elements at runtime. (See "Dynamic Arrays," CUJ, November 1992, and "The Function operator[]", CUJ, January 1993.) Listing 3 shows the class definition for the climactic version, which grows automatically to keep subscript references in bounds.float_arrays, like all other arrays in C and C++, have the lowest subscript fixed at zero. This is less than ideal for some applications. Programming languages like Pascal, and its descendents Modula-2 and Ada, let you declare arrays with low bounds other than zero.
In C++, you can fill the need by creating a class that I'll call float_vector, for which you specify not the number of elements but the low and high bounds of the subscript. For example,
float_vector fv(1, 10);declares fv as an array of float whose subscript range is 1 to 10, inclusive. float_array already embodies much of the functionality for float_vector, so let's consider implementing float_vector by deriving it from float_array.Listing 4 shows the class definition for float_vector. float_vector adds a new private data member, _low, that records the vector's low bound. A float_vector need not store the high bound as a data member because it can determine the high bound from the low bound and length (inherited from float_array). float_vector defines a constructor, float_vector(int lo, int hi), that builds a vector with subscripts from lo to hi, inclusive. It also adds two query functions, low and high, that return the values of the current low and high subscripts, respectively.
The float_vector constructor is extremely terse, so I defined it as an inline function in the header, as
inline float_vector::float_vector(int lo, int hi) : _low(lo), float_array(hi - lo + 1) { }The constructor's member-initializers do all the work. The first initializer, _low(lo), fills in the private data member _low. The second initializer, float_array(hi - lo + 1), invokes the base class constructor to initialize the inherited data members array and len.hi - lo + 1 is the number of elements in a float_vector whose subscript range is from lo to hi.Remember, inherited private members are not directly accessible in the derived class. The derived class must use the public interface provided by the base class, which in this case, is a public constructor. Unlike member initializers that I've used in the past, the leading identifier in the initializer float_array(hi - lo + 1) is the name of a type, not the name of a data member. There's no named member for the float_array sub-object in the float_vector, so you must refer to it using its type name.
float_vector overrides both inherited operator[] functions with new implementations that work when the low subscript bound is nonzero. Notice that the formal parameters for both the const and non-const float_vector::operator[] are int, and not size_t, as they are in class float_array, because a float_vector's low bound may be negative.
Listing 5 contains the non-inline float_vector member functions, namely, both forms of operator[]. The function bodies are identical; they both rely on the inherited (overridden) versions of operator[] to do most of the work. The expression i - low shifts the subscript i into a subscript range whose low bound is zero. The statement
return float_array::operator[] (i - low);calls the inherited operator[] to select the desired element from the inherited float_array sub-object, and extend the array if necessary.Listing 6 shows a test program for float_vectors. Notice that the display function (brought over from my previous two articles) still accepts a second argument of type const float_array &. I did not change it to const float_vector &. I also left a float_array in the test program to show that the display function accepts arguments of both the base and derived types.
A sample output from the program appears in Figure 5. The abnormal termination is intentional. I planted a subscripting error just to show that the assertions work.
Food for Thought
I was forced to make a simplifying assumption in my float_vector class, namely, that you can only extend the high bound of a vector. The low bound must remain fixed. That's why both float_vector::operator[] functions include the assertion
assert(i >= low());The problem is that my design for float_arrays did not anticipate that I might want to extend the arrays in both directions. I'm not sure that I want to extend the low bound, but given this design, it's out of the question.Inheritance is often advertised as a wonderful technique for reusing existing code. But the reuse doesn't always work out the way you want. The reality is that you must decide for each class that you build whether you intend to use it as a base class for further derivation, and if so, how you or others might wish to use it.
My design also raises another question. I said earlier that public inheritance defines Is-A relationships. You might reasonably ask if float_vector really is a float_array, or if I just used a convenient implementation trick.
I'll ponder this and other questions in the next part of this series.
References
Ellis, Margaret A. and Bjarne Stroustrup. 1990. The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley.Stroustrup, Bjarne. 1991. The C++ Programming Language, 2nd. ed. Reading, MA: Addison-Wesley.