Columns


Stepping Up To C++

Inheritance, Part 2

Dan Saks


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 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.

In my last column, I introduced inheritance as a technique for creating new (derived) classes from existing (base) classes. (See "Inheritance, Part 1", CUJ, March 1993). I considered only the simplest and most common form of inheritance — public inheritance. In this article, I'll explore other forms of inheritance and offer some guidance that may help you decide when to use, and when not to use, inheritance.

In part one, I derived a float_vector class from a float_array class. The float_array class is a dynamic array of float elements. That is, it is an array whose number of elements can vary at runtime. The lower subscript bound for all float_arrays is always zero. The float_array class definition appears in Listing 1.

The float_vector class is a dynamic array of float whose lower subscript can be an integer value other than zero. The float_vector class definition appears in Listing 2. (When I presented this listing last time, I accidentally omitted the int return type from the inline function definitions for low and high.) The non-inline float_vector member function definitions appear in Listing 3. My test program for float_vectors and float_arrays appears as Listing 4.

I concluded last time by questioning the merits of deriving float_vector from float_array. Although public inheritance offers a compact notation for creating float_vector from float_array, it's not the only way to do it. Let's look at an alternative.

Aggregation

If all you want to do is simply create float_vector from float_array without rewriting much code, you can do that very straightforwardly using aggregation. Aggregation (also known as composition) is simply embedding an object of one type as a member of another. That is, rather than derive float_vector from float_array:

class float_vector :
             public float_array
   {
public:
   // public member functions ...
private:
   int_low;
   };
you implement float_vector as a class with a float_array member:

class float_vector
   {
public:
   // public member functions ...
private:
   float_array fa;
   int _low;
   };
Then, you implement each float_vector member function by calling the corresponding float_array member. For example:

size_t float_vector::length() const
   {
   return fa.length();
   }
Here, the length of a float_vector is simply the length of its float_array member.

Listing 5 shows a definition for class float_vector implemented with a float_array member. It differs from the class definition in Listing 2 in only two ways:

The inline member functions in Listing 5 also reflect these subtle changes:

inline float_vector::float_vector(int lo, int hi)
   : _low(lo), fa(hi - lo + 1)
   {}
the constructor initializer for the float_array subobject refers to the float_array by its name, fa. In Listing 2, the float_array subobject has no name, so the initializer refers to the subobject by its type.

Listing 6 shows the non-inline member functions for the floor_vector class defined (by aggregation) in Listing 5. These functions are identical to those in Listing 3, except for the calls to float_array::operator[]. In Listing 3, each float_vector::operator[] calls the operator[] for the inherited float_array subobject using the explicitly-qualified function name, as in

return float_array::operator[] (i - low());
In Listing 6, each float_vector::operator[] calls operator[] for the float_array member object using the usual member function call notation. I could have written the calls as

return fa.operator[](i - low());
but I took advantage of the overloaded operator notation and wrote the calls as simply

return fa[i - low()];
In retrospect, I could have used the operator notation in the derived float_vector: :operator[] as well, as shown in Listing 7. Notice that the local variable fa is a float_array * in the non-const float_vector::operator[] function, and const float_array *in the const function.

Aggregation vs. Inheritance

Using aggregation, class float_vector has nearly all the functionality as it did when implemented by inheritance. But there are some critical differences. When you compile the test program in Listing 4, the compiler balks at the first call to display inside main, namely

display("fa", fa);
When you derive float_vector from float_array, this call works fine. Even though display expects its second argument to be a float_array, it accepts float_vector fa because a float_vector IS A float_array. But when using aggregation, a float_vector is not a float_array. Rather, a float_vector HAS A float_array, or equivalently, a float_vector IS IMPLEMENTED BY A float_array, so a float_vector does not convert implicitly to float_array.

If you try writing an additional display function for float_vectors as

void display(const char *s, const float_vector &fv)
   {
   cout << S << " = " << fv << endl;
   }
then the compiler complains that there's no operator<< for float_vectors. Once again, even though there's an operator<< for float_arrays (declared in Listing 1) , it doesn't apply to float_vectors built by aggregation.

You can cure that problem by defining

inline ostream &operator<<
   (ostream &os, const float_vector &fv)
   {
   return os << fv.fa;
   }
(Note that this is not a member function, but it must reference private data in fv, so class float_vector must declare it as a friend function.) This clears up all the problems in calling display, but the program still doesn't compile, because the compiler rejects the later declaration

float_array fc = fa;
Disregarding the possibility that the compiler might introduce temporary objects, this declaration compiles as if it were

float_array fc (fa);
The compiler wants to call a float_array constructor that accepts a float_vector argument. (Remember, fa is a float_vector here.) When a float_vector IS A float_array, the compiler readily binds a float_vector to a float_array &, so the previous declaration calls the float_array copy constructor

float_array(const float_array &);
But, when a float_vector HAS A float_array, the compiler refuses to do the conversion.

Of course, you could go back to the float_array class and write a constructor that accepts a float_vector argument, but you shouldn't. Adding this constructor forces all float_array users to also accept this float_vector class. It introduces an implicit conversion into an existing class that could break code using that class. Furthermore, you cannot assume that all users wishing to extend a class have access and can alter the source for that class. Thus, I've been looking at ways to reuse an existing class without rewriting it.

Adding a conversion operator to class float_vector is a better approach:

float_vector::operator float_array()
   {
   return fa;
   }
This operator converts a float_vector to a float_array by returning a copy of the float_vector's float_array member. With this addition, you can compile the test program (Listing 4) using the float_vector class implemented by aggregation.

Once you add a conversion operator, you no longer need versions of the display function and operator<< that accept a float_vector as their second argument. If the only declaration for display is

void display(const char *s, const float_array &fa);
but you have declared a conversion operator

float_vector::operator float_array();
then the call

display("fa", fa);
(where fa is a float_vector), translates into

display("fa", float_array(fa));
That is, the compiler obtains a float_array by applying the conversion operator to float_vector fa.

This conversion operator doesn't really let you treat float_vectors as if they were float_arrays. The conversion operator returns a float_array by value. That is, it returns a temporary copy of the float_array object inside a float_vector, not a reference to the float_array subobject.

For example, suppose you write

float_vector fv;
...
const float_array &r = fv;
If you had derived float_vector from float_array, then the reference declaration binds r directly to the float_array subobject of fv. But, if you had declared float_vector with a float_array member, then the reference declaration binds cr to a temporary float_array object created by copying the float_array member of fv.

Now, suppose you write

float_array &r = fv;
If you had derived float_vector from float_array, then this also binds r directly to the float_array subobject of fv. But, if you had declared float_vector with a float_array member, then the compiler will reject the reference declaration because you cannot bind a non-const reference to a temporary object.

The error should go away if you declare an overloaded pair of conversion operators

float_vector::operator float_array &();
float_vector::operator const float_array &() const;
I say it should go away because I believe this is valid C++. Unfortunately, many compilers complain that this pair of conversions causes ambiguities.

Listing 5 shows the class and inline function definitions for class float_vector implemented with a float_array member. Listing 6 contains the corresponding non-inline member function definitions. If the overloaded pair of conversions to (const and non-const) float_array & gives your compiler fits, replace them both with

float_vector::operator float_array()
   {
   return fa;
   }
and your compiler should accept the test program.

Choosing the Right Design

If inheritance and aggregation are so similar, how do you choose which one to use? It's largely an matter of judgment based on experience. But by and large, you will find that these general rules apply:

Both Cargill (1992) and Meyers (1992) offer additional rationale for and examples of these guidelines. Meyers calls aggregation layering.

Sometimes it's easy to tell IS-A from HAS-A. For example, a circle is a shape and a triangle is a shape, but shapes have attributes like color. Therefore, you should publicly derive classes circle and triangle from class shape, and include a member of type color in class shape.

Other times, the distinctions are very subtle. For instance, is a float_vector a float_array? It all depends on how you define the concepts. If you define float_array as a dynamic array of N elements and float_vector as a dynamic array of N elements with a user-definable lower subscript bound, then you could argue that a float_vector is a float_array. But, if you define float_array as a dynamic array of elements subscripted 0 through N-l, you'll have a harder time arguing that a float_vector is a float_array.

If you have trouble designing a particular class relationship, I suggest you write down the reasoning behind your design in clear and precise prose, using complete sentences. If possible, have your peers review it too. If you can't make a convincing case for an IS-A relationship, then don't use public inheritance.

Protected Members

In addition to public and private, C++ offers a third access specifier — protected. Protected access is similar to private access, except that members and friends of derived classes can access protected base class members. As always, derived class members and friends cannot access private base class members. For example, Listing 8 shows a base class B with protected member j and private member k. Member function foo of class D derived from B can access j but not k.

In the first part of this article, I said I was forced to make a simplifying assumption in my float_vector class, namely, that you only extend a vector's high bound. The word "forced" was a bit misleading. You could write the non-const float_vector:: operator[] to extend the vector's low bound as well. But, because the float_vector class can't access its inherited array member (the pointer to the dynamically-allocated elements), extending the low bound would have been more complicated and much slower than extending the high bound.

Plum and Saks (1992) recommend that if the original float_array class had declared its private data members (array and len) as protected, then the derived float_vector class could access those members directly. A float_vector::operator[] could then extend a vector's low bound more efficiently. But this efficiency comes at a price.

You should resist the urge to hedge against inflexible class designs by declaring members that should be private as protected. Protected members increase coupling between a base and its derived classes. The protected members are part of the base's interface. Any application program can gain access to protected base class members by simply deriving a class.

If you must use protected members, use protected member functions rather than data. Protected data members don't protect themselves. The author of a derived class can easily corrupt its base class subobject by misusing inherited protected data. Protected member functions supplied by the base class can do a better job of keeping inherited private data in a consistent state.

Non-Public Inheritance

In addition to public inheritance, C+ + also supports private and protected inheritance. For example,

class D : private B { ... };
In fact, private is the default access specifier for base classes, which is why you must explicitly specify public if that is what you want.

Here's what the different base class access specifiers mean:

Last time, I said that whenever B is a public base class of D, then a D is a B. Specifically, you can:

In fact, these conversions apply whenever B is an accessible base class of D. The ARM (Ellis and Stroustrup, 1990) and C++ draft (Shopiro 1992) both state that "a base class is accessible if its public members are accessible." I believe this really means that a base class B is accessible with respect to a derived class D if the members of D inherited from public members of B are accessible as members of D.

A public base class is always accessible with respect to its derived class, so those IS-A conversions, above, always apply. But, for protected and private inheritance, a base class is accessible only in certain contexts, as illustrated by Listing 9. Here, B is a protected base class of T. Inside a member (or friend) of T, like T::foo, B is accessible with respect to T, so the IS-A conversions apply. But, outside T's members and friends, as in main, B is not an accessible base of T, so the IS-A conversions do not apply.

In many ways, private inheritance is like aggregation; it provides an alternative technique for defining IS-IMPLEMENTED-BY-A. In fact, the choice between aggregation and private inheritance is often a matter of personal preference. Both Cargill (1992) and Meyers (1992) suggest favoring aggregation over private inheritance. I agree. Meyers also provides an example to illustrate a exceptional case where you might prefer private inheritance.

I have yet to see a use for protected inheritance. I would certainly be interested to see one.

References

Cargill, Tom. 1992. C++ Programming Style. Reading, MA: Addison-Wesley.

Meyers, Scott. 1992. Effective C++. Reading, MA: Addison-Wesley.

Plum, Thomas and Dan Saks. 1992. C++ Programming Guidelines, Cardiff, NJ: Plum Hall Books.