MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Choosing composition vs. inheritance

Both composition and inheritance place subobjects inside your new class. Both use the constructor initializer list to construct these subobjects. You may now be wondering what the difference is between the two, and when to choose one over the other.

Composition is generally used when you want the features of an existing class inside your new class, but not its interface. That is, you embed an object that you’re planning on using to implement features of your new class, but the user of your new class sees the interface you’ve defined rather than the interface from the original class. For this effect, you embed private objects of existing classes inside your new class.

Sometimes it makes sense to allow the class user to directly access the composition of your new class, that is, to make the member objects public. The member objects use implementation hiding themselves, so this is a safe thing to do and when the user knows you’re assembling a bunch of parts, it makes the interface easier to understand. A car object is a good example:

//: C14:Car.cpp
// Public composition

class Engine {
public:
  void start() const {}
  void rev() const {}
  void stop() const {}
};

class Wheel {
public:
  void inflate(int psi) const {}
};

class Window {
public:
  void rollup() const {}
  void rolldown() const {}
};

class Door {
public:
  Window window;
  void open() const {}
  void close() const {}
};

class Car {
public:
  Engine engine;
  Wheel wheel[4];
  Door left, right; // 2-door
};

int main() {
  Car car;
  car.left.window.rollup();
  car.wheel[0].inflate(72);
} ///:~ 

Because the composition of a car is part of the analysis of the problem (and not simply part of the underlying design), making the members public assists the client programmer’s understanding of how to use the class and requires less code complexity for the creator of the class.

With a little thought, you’ll also see that it would make no sense to compose a car using a vehicle object – a car doesn’t contain a vehicle, it is a vehicle. The is-a relationship is expressed with inheritance, and the has-a relationship is expressed with composition.

Subtyping

Now suppose you want to create a type of ifstream object that not only opens a file but also keeps track of the name of the file. You can use composition and embed both an ifstream and a strstream into the new class:

//: C14:FName1.cpp
// An fstream with a file name
#include <iostream>
#include <fstream>
#include <strstream>
#include "../require.h"
using namespace std;

class FName1 {
  ifstream File;
  enum { bsize = 100 };
  char buf[bsize];
  ostrstream Name;
  int nameset;
public:
  FName1() : Name(buf, bsize), nameset(0) {}
  FName1(const char* filename)
    : File(filename), Name(buf, bsize) {
      assure(File, filename);
      Name << filename << ends;
      nameset = 1;
  }
  const char* name() const { return buf; }
  void name(const char* newname) {
    if(nameset) return; // Don't overwrite
    Name << newname << ends;
    nameset = 1;
  }
  operator ifstream&() { return File; }
};

int main() {
  FName1 file("FName1.cpp");
  cout << file.name() << endl;
  // Error: rdbuf() not a member:
//!  cout << file.rdbuf() << endl;
} ///:~ 

There’s a problem here, however. An attempt is made to allow the use of the FName1 object anywhere an ifstream object is used, by including an automatic type conversion operator from FName1 to an ifstream&. But in main, the line

cout << file.rdbuf() << endl;

will not compile because automatic type conversion happens only in function calls, not during member selection. So this approach won’t work.

A second approach is to add the definition of rdbuf( ) to FName1:

filebuf* rdbuf() { return File.rdbuf(); }

This will work if there are only a few functions you want to bring through from the ifstream class. In that case you’re only using part of the class, and composition is appropriate.

But what if you want everything in the class to come through? This is called subtyping because you’re making a new type from an existing type, and you want your new type to have exactly the same interface as the existing type (plus any other member functions you want to add), so you can use it everywhere you’d use the existing type. This is where inheritance is essential. You can see that subtyping solves the problem in the preceding example perfectly:

//: C14:FName2.cpp
// Subtyping solves the problem
#include <iostream>
#include <fstream>
#include <strstream>
#include "../require.h"
using namespace std;

class FName2 : public ifstream {
  enum { bsize = 100 };
  char buf[bsize];
  ostrstream fname;
  int nameset;
public:
  FName2() : fname(buf, bsize), nameset(0) {}
  FName2(const char* filename)
    : ifstream(filename), fname(buf, bsize) {
      assure(*this, filename);
      fname << filename << ends;
      nameset = 1;
  }
  const char* name() const { return buf; }
  void name(const char* newname) {
    if(nameset) return; // Don't overwrite
    fname << newname << ends;
    nameset = 1;
  }
};

int main() {
  FName2 file("FName2.cpp");
  assure(file, "FName2.cpp");
  cout << "name: " << file.name() << endl;
  const int bsize = 100;
  char buf[bsize];
  file.getline(buf, bsize); // This works too!
  file.seekg(-200, ios::end);
  cout << file.rdbuf() << endl;
} ///:~ 

Now any member function that works with an ifstream object also works with an FName2 object. That’s because an FName2 is a type of ifstream; it doesn’t simply contain one. This is a very important issue that will be explored at the end of this chapter and in the next one.

Specialization

When you inherit, you take an existing class and make a special version of it. Generally, this means you’re taking a general-purpose class and specializing it for a particular need.

For example, consider the Stack class from the previous chapter. 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 the previous chapter:

//: C14:Inhstak.cpp
//{L} ../C13/Stack11
// Specializing the Stack class
#include <iostream>
#include <fstream>
#include <string>
#include "../require.h"
#include "../C13/Stack11.h"
using namespace std;

class StringList : public Stack {
public:
  void push(string* str) {
    Stack::push(str);
  }
  string* peek() const {
    return (string*)Stack::peek();
  }
  string* pop() {
    return (string*)Stack::pop();
  }
};

int main() {
  ifstream file("Inhstak.cpp");
  assure(file, "Inhstak.cpp");
  string line;
  StringList textlines;
  while(getline(file,line))
    textlines.push(new string(line));
  string* s;
  while((s = textlines.pop()) != 0) // No cast!
    cout << *s << endl;
} ///:~ 

The Stack11.h header file is brought in from Chapter 13. (The Stack11 object file must be linked in as well.)

Stringlist 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 rather than void pointers, so no cast is necessary to use the pointer.

Amazingly enough, this extra type-checking safety is free! The compiler is being given extra type information, that it uses at compile-time, but the functions are inline and no extra code is generated.

Unfortunately, inheritance doesn’t solve all the problems with this container class. The destructor still causes trouble. You’ll remember from Chapter 11 that the Stack::~Stack( ) destructor moves through the list and calls delete for all the pointers. The problem is, delete is called for void pointers, which only releases the memory and doesn’t call the destructors (because void* has no type information). If a Stringlist::~Stringlist( ) destructor is created to move through the list and call delete for all the String pointers in the list, the problem is solved if

  1. The Stack data members are made protected so the Stringlist destructor can access them. ( protected is described a bit later in the chapter.)
  2. The Stack base class destructor is removed so the memory isn’t released twice.
  3. No more inheritance is performed, because you’d end up with the same dilemma again: multiple destructor calls versus an incorrect destructor call (to a String object rather than what the class derived from Stringlist might contain).
This issue will be revisited in the next chapter, but will not be fully solved until templates are introduced in Chapter XX.

A more important observation to make about this example is that it changes the interface of the Stack in the process of inheritance. If the interface is different, then a Stringlist really isn’t a Stack, and you will never be able to correctly use a Stringlist as a Stack. This questions the use of inheritance here: if you’re not creating a Stringlist that is-a type of Stack, then why are you inheriting? A more appropriate version of Stringlist will be shown later in the chapter.

private inheritance

You can inherit a base class privately by leaving off the public in the base-class list, or by explicitly saying private (probably a better policy because it is clear to the user that you mean it). When you inherit privately, you’re “implementing in terms of”; that is, you’re creating a new class that has all the data and functionality of the base class, but that functionality is hidden, so it’s only part of the underlying implementation. The class user has no access to the underlying functionality, and an object cannot be treated as a member of the base class (as it was in FName2.cpp).

You may wonder what the purpose of private inheritance is, because the alternative of creating a private object in the new class seems more appropriate. private inheritance is included in the language for completeness, but if for no other reason than to reduce confusion, you’ll usually want to use a private member rather than private inheritance. However, there may occasionally be situations where you want to produce part of the same interface as the base class and disallow the treatment of the object as if it were a base-class object. private inheritance provides this ability.

Publicizing privately inherited members

When you inherit privately, all the public members of the base class become private. If you want any of them to be visible, just say their names (no arguments or return values) in the public section of the derived class:

//: C14:Privinh.cpp
// Private inheritance

class Base1 {
public:
  char f() const { return 'a'; }
  int g() const { return 2; }
  float h() const { return 3.0; }
};

class Derived : Base1 { // Private inheritance
public:
  Base1::f; // Name publicizes member
  Base1::h;
};

int main() {
  Derived d;
  d.f();
  d.h();
//! d.g(); // Error -- private function
} ///:~ 

Thus, private inheritance is useful if you want to hide part of the functionality of the base class.

You should think carefully before using private inheritance instead of member objects; private inheritance has particular complications when combined with run-time type identification (the subject of Chapter 17).

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru