DekGenius.com
Team LiB   Previous Section   Next Section

6.4 Inheritance

A class can inherit from zero or more base classes. A class with at least one base class is said to be a derived class. A derived class inherits all the data members and member functions of all of its base classes and all of their base classes, and so on. A class's immediate base classes are called direct base classes. Their base classes are indirect base classes. The complete set of direct and indirect base classes is sometimes called the ancestor classes.

A class can derive directly from any number of base classes. The base-class names follow a colon and are separated by commas. Each class name can be prefaced by an access specifier (described later in this chapter). The same class cannot be listed more than once as a direct base class, but it can appear more than once in the inheritance graph. For example, derived3 in the following code has base2 twice in its inheritance tree, once as a direct base class, and once as an indirect base class (through derived2):

class base1 { ... };
class derived1 : public base1 { ... };
class base2 { ... }
class derived2 : public derived1, public base2 { ... }
class derived3 : protected derived2, private base2 { ... }

A derived class can access the members that it inherits from an ancestor class, provided the members are not private. (See Section 6.5 later in this chapter for details.) To look up a name in class scope, the compiler looks first in the class itself, then in direct base classes, then in their direct base classes, and so on. See Chapter 2 for more information about name lookup.

To resolve overloaded functions, the compiler finds the first class with a matching name and then searches for overloads in that class. Chapter 5 lists the complete rules for overload resolution.

An object with a derived-class type can usually be converted to a base class, in which case the object is sliced. The members of the derived class are removed, and only the base class members remain:

struct file {
  std::string name;
};
struct directory : file {
  std::vector<file*> entries;
};
directory d;
file f;
f = d; // Only d.name is copied to f; entries are lost.

Slicing usually arises from a programming error. Instead, you should probably use a pointer or reference to cast from a derived class to a base class. In that case, the derived-class identity and members are preserved. This distinction is crucial when using virtual functions, as described in the next section. For example:

directory* dp = new directory;
file* fp;
fp = dp; // Keeps entries and identity as a directory object

As you can see in the previous examples, the compiler implicitly converts a derived class to a base class. You can also use an explicit cast operator, but if the base class is virtual, you must use dynamic_cast<>, not static_cast<>. See Chapter 3 for more information about cast expressions.

If a base class is ambiguous (see Section 6.4.5 later in this chapter) or inaccessible (see Section 6.5 later in this chapter), you cannot slice a derived class to the base class, nor can you cast a pointer or reference to the inaccessible base class (an old-style cast allows it but such use is not recommended).

6.4.1 Virtual Functions

A nonstatic member function can be declared with the virtual function specifier, and is then known as a virtual function. A virtual function can be overridden in a derived class. To override a virtual function, declare it in a derived class with the same name and parameter types. The return type is usually the same but does not have to be identical (as described in the next section, Section 6.4.2). The virtual specifier is optional in the derived class but is recommended as a hint to the human reader. A constructor cannot be virtual, but a destructor can be. A virtual function cannot be static.

A class that has at least one virtual function is polymorphic. This form of polymorphism is more precisely known as type polymorphism. (C++ also supports parametric polymorphism with templates; see Chapter 7.) Most programmers mean type polymorphism when they talk about object-oriented programming.

When calling virtual functions, you must distinguish between the declared type of the object, pointer, or reference and the actual type at runtime. The declared type is often called the static type, and the actual type is the dynamic type. For example:

struct base {
  virtual void func(  );
};
struct derived : base {
  virtual void func(  ); // Overload
};
base* b = new derived;   // Static type of b is base*.
                         // Dynamic type is derived*.
b->func(  );             // Calls dynamic::func(  )

When any function is called, the compiler uses the static type to pick a function signature. If the function is virtual, the compiler generates a virtual function call. Then, at runtime, the object's dynamic type determines which function is actually called—namely, the function in the most-derived class that overrides the virtual function. This is known as a polymorphic function call.

Dispatching Virtual Functions

Virtual functions are most commonly implemented using virtual function tables, or vtables. Each class that declares at least one virtual function has a hidden data member (e.g., _ _vtbl). The _ _vtbl member points to an array of function pointers. Every derived class has a copy of the table. Every instance of a class shares a common table. Each entry in the table points to a function in a base class, or if the function is overridden, the entry points to the derived class function. Any new virtual functions that the derived class declares are added at the end of the table.

When an object is created, the compiler sets its _ _vtbl pointer to the vtable for its dynamic class. A call to a virtual function is compiled into an index into the table and into a call to the function at that index. Note that the dynamic_cast<> operator can use the same mechanism to identify the dynamic type of the object.

Multiple inheritance complicates matters slightly, yet the basic concept remains the same: indirection through a table of pointers.

Compilers do not have to use vtables, but they are used so widely, the term "vtable" has entered the common parlance of many C++ programmers.

An object's dynamic type can differ from its static type only if the object is accessed via a pointer or reference. Thus, to call a virtual function, you typically access the target object via a pointer (e.g., ptr->func( )). Inside a member function, if you call a virtual member function using its unqualified name, that is the same as calling the function via this->, so the function is called virtually. If a nonpointer, nonreference object calls a virtual function, the compiler knows that the static type and dynamic type always match, so it can save itself the lookup time and call the function in a nonvirtual manner.

Example 6-16 shows a variety of virtual functions for implementing a simple calculator. A parser constructs a parse tree of expr nodes, in which each node can be a literal value or an operator. The operator nodes point to operand nodes, and an operand node, in turn, can be any kind of expr node. The virtual evaluate function evaluates the expression in the parse tree, returning a double result. Each kind of node knows how to evaluate itself. For example, a node can return a literal value or add the values that result from evaluating two operands.

Example 6-16. Declaring and using virtual functions
class expr {
public:
  virtual ~expr(  ) {}
  virtual double evaluate(  ) const = 0;
  std::string as_string(  ) const {
    std::ostringstream out;
    print(out);
    return out.str(  );
  }
  virtual void print(std::ostream& out) const {}
  virtual int precedence(  ) const = 0;
  template<typename charT, typename traits>
  static std::auto_ptr<expr> parse(
    std::basic_istream<charT,traits>& in);
};

// cout << *expr prints any kind of expression because expr->print(  ) is virtual.
template<typename charT, typename traits>
std::basic_ostream<charT,traits>& 
  operator<<(std::basic_ostream<charT,traits>& out, const expr& e)
{
  e.print(out);
  return out;
}

class literal : public expr {
public:
  literal(double value) : value_(value) {}
  virtual double evaluate(  ) const { return value_; }
  virtual void print(std::ostream& out) const {
    out << value_;
  }
  virtual int precedence(  ) const { return 1; }
private:
  double value_;
};

// Abstract base class for all binary operators
class binop : public expr {
public:
  binop(std::auto_ptr<expr> left, std::auto_ptr<expr> right)
  : left_(left), right_(right) {}
  virtual double evaluate(  ) const {
    return eval(left_->evaluate(  ), right_->evaluate(  ));
  }
  virtual void print(std::ostream& out) const {
    if (left_->precedence(  ) > precedence(  ))
      out << '(' << *left_ << ')';
    else
      out << *left_;

    out << op(  );

    if (right_->precedence(  ) > precedence(  ))
      out << '(' << *right_ << ')';
    else
      out << *right_;
  }
  // Reminder that derived classes must override precedence
  virtual int precedence(  ) const = 0;
protected:
  virtual double eval(double left, double right) const = 0;
  virtual const char* op(  ) const = 0;
private:
  // No copying allowed (to avoid messing up auto_ptr<>s)
  binop(const binop&);
  void operator=(const binop&);
  std::auto_ptr<expr> left_;
  std::auto_ptr<expr> right_;
};

// Example binary operator.
class plus : public binop {
public:
  plus(std::auto_ptr<expr> left, std::auto_ptr<expr> right)
  : binop(left, right) {}
  virtual int precedence(  ) const { return 3; }
protected:
  virtual double eval(double left, double right) const {
    return left + right;
  }
  virtual const char* op(  ) const { return "+"; }
};

int main(  )
{
  while(std::cin) {
    std::auto_ptr<expr> e(expr::parse(std::cin));
    std::cout << *e << '\n';
    std::cout << e->evaluate(  ) << '\n';
  }
}

Sometimes you do not want to take advantage of the virtualness of a function. Instead, you may want to call the function as it is defined in a specific base class. In such a case, qualify the function name with the base-class name, which tells the compiler to call that class's definition of the function:

literal* e(new literal(2.0));
e->print(std::cout);        // Calls literal::print
e->expr::print(std::cout);  // Calls expr::print

A class that has at least one virtual function should also have a virtual destructor. If a delete expression (see Chapter 3) deletes a polymorphic pointer (for which the dynamic type does not match the static type), the static class must have a virtual destructor. Otherwise, the behavior is undefined.

6.4.2 Covariant Return Types

The return type of an overriding virtual function must be the same as that of the base function, or it must be covariant. In a derived class, a covariant return type is a pointer or reference to a class type that derives from the return type used in the base class. Note that the return type classes do not necessarily have to match the classes that contain the functions, but they often do. The return type in the derived class can have additional const or volatile qualifiers that are not present in the base-class return type.

In a function call, the actual return value is implicitly cast to the static type used in the function call. Example 6-17 shows one typical use of covariant types.

Example 6-17. Covariant return types
struct shape {
  virtual shape* clone(  ) = 0;
};
struct circle : shape {
  virtual circle* clone(  ) {
    return new circle(*this);
  }
  double radius(  ) const { return radius_; }
  void radius(double r) { radius_ = r; }
private:
  double radius_;
  point center_;
};
struct square : shape {
  virtual square* clone(  ) {
    return new square(*this);
  }
private:
  double size_;
  point corners_[4];
};

circle unit_circle;

circle* big_circle(double r)
{
  circle* result = unit_circle.clone(  );
  result->radius(r);
  return result;
}

int main(  )
{
  shape* s = big_circle(42.0);
  shape* t = s->clone(  );
  delete t;
  delete s;
}

6.4.3 Pure Virtual Functions

A virtual function can be declared with the pure specifier (=0) after the function header. Such a function is a pure virtual function (sometimes called an abstract function). The syntax for a pure specifier requires the symbols = 0. You cannot use an expression that evaluates to 0.

Even though a function is declared pure, you can still provide a function definition (but not in the class definition). A definition for a pure virtual function allows a derived class to call the inherited function without forcing the programmer to know which functions are pure. A pure destructor must have a definition because a derived-class destructor always calls the base-class destructor.

A derived class can override a pure virtual function and provide a body for it, override it and declare it pure again, or simply inherit the pure function. See the next section, Section 6.4.4.

Example 6-18 shows some typical uses of pure virtual functions. A base class, shape, defines several pure virtual functions (clone, debug, draw, and num_sides). The shape class has no behavior of its own, so its functions are pure virtual.

Example 6-18. Pure virtual functions
class shape {
public:
  virtual ~shape(  );
  virtual void draw(graphics* context)       = 0;
  virtual size_t num_sides(  )           const = 0;
  virtual shape* clone(  )               const = 0;
  virtual void debug(ostream& out)       const = 0;
};

class circle : public shape {
public:
  circle(double r) : radius_(r) {}
  virtual void draw(graphics* context);
  virtual size_t num_sides(  )    const { return 0; }
  virtual circle* clone(  )       const { return new circle(radius(  )); }
  virtual void debug(ostream& out) const {
    shape::debug(out);
    out << "radius=" << radius_ << '\n';
  }
  double radius(  ) const { return radius_; }
private:
  double radius_;
};

class filled_circle : public circle {
public:
  filled_circle(double r, ::color c) : circle(r), color_(c) {}
  virtual filled_circle* clone(  ) const {
    return new filled_circle (radius(  ), color(  ));
  }
  virtual void draw(graphics* context);
  virtual void debug(ostream& out) const {
    circle::debug(out);
    out << "color=" << color_ << '\n';
  }
  ::color color(  ) const { return color_;}
private:
  color color_;
};

void shape::debug(ostream& out)
const
{}

Even though shape::debug is pure, it has a function body. Derived classes must override shape::debug, but they can also call it, which permits uniform implementation of the various debug functions. In other words, every implementation of debug starts by calling the base class debug. Classes that inherit directly from shape do not need to implement debug differently from classes that inherit indirectly.

6.4.4 Abstract Classes

An abstract class declares at least one pure virtual function or inherits a pure virtual function without overriding it. A concrete class has no pure virtual functions (or all inherited pure functions are overridden). You cannot create an object whose type is an abstract class. Instead, you must create objects of concrete type. In other words, a concrete class that inherits from an abstract class must override every pure virtual function.

Abstract classes can be used to define a pure interface class, that is, a class with all pure virtual functions and no nonstatic data members. Java and Delphi programmers recognize this style of programming because it is the only way these languages support multiple inheritance. Example 6-19 shows how interface classes might be used in C++.

Example 6-19. Using abstract classes as an interface specification
struct Runnable {
  virtual void run(  ) = 0;
};

struct Hashable {
  virtual size_t hash(  ) = 0;
};

class Thread : public Runnable, public Hashable {
public:
  Thread(  )                       { start_thread(*this); }
  Thread(const Runnable& thread)   { start_thread(thread); }
  virtual void run(  );
  virtual size_t hash(  ) const { return thread_id(  ); }
  size_t thread_id(  )    const;
  ...
private:
  static void start_thread(const Runnable&);
};

// Derived classes can override run to do something useful.
void Thread::run(  )
{}

6.4.5 Multiple Inheritance

A class can derive from more than one base class. You cannot name the same class more than once as a direct base class, but a class can be used more than once as an indirect base class, or once as a direct base class and one or more times as an indirect base class. Some programmers speak of inheritance trees or hierarchies, but with multiple base classes, the organization of inheritance is a directed acyclic graph, not a tree. Thus, C++ programmers sometimes speak of inheritance graphs.

If multiple base classes declare the same name, the derived class must qualify references to the name or else the compiler reports an ambiguity error:

struct base1 { int n; };
struct base2 { int n; };
struct derived : base1, base2 {
  int get_n(  ) { return base1::n; } // Plain n is an error.
};

Objects of a derived-class type contain separate subobjects for each instance of every base class to store the base class's nonstatic data members. To refer to a member of a particular subobject, qualify its name with the name of its base class. Static members, nested types, and enumerators are shared among all instances of a repeated base class, so they can be used without qualification (unless the derived class hides a name with its own declaration), as shown in Example 6-20.

Example 6-20. Multiple inheritance
struct base1 {
  int n;
};
struct base2 {
  enum color { black, red };
  int n;
};
struct base3 : base2 {
  int n; // Hides base2::n
};
struct derived : base1, base2, base3 {
  color get_color(  ); // OK: unambiguous use of base2::color
  int get_n(  )  { return n; } // Error: ambiguous
  int get_n1(  ) { return base2::n; } // Error: which base2?
  int get_n2(  ) { return base3::n; } // OK
  int get_n3(  ) {        // OK: another way to get to a specific member n
    base3& b3 = *this;
    base2& b2 = b3;
    return b2.n;
  }
};

A well-designed inheritance graph avoids problems with ambiguities by ensuring that names are unique throughout the graph, and that a derived class inherits from each base class no more than once. Sometimes, however, a base class must be repeated in an inheritance graph. Figure 6-1 illustrates the organization of multiple base classes, modeled after the standard I/O stream classes. Because basic_iostream derives from basic_istream and from basic_ostream, it inherits two sets of flags, two sets of buffers, and so on, even though it should have only one set of each. This problem is solved by virtual base classes, as explained in the next section.

Figure 6-1. Deriving from multiple base classes
figs/cppn_0601.gif

6.4.6 Virtual Inheritance

A base class can be declared with the virtual specifier, which is important when a class has multiple base classes. Ordinarily, each time a class is named in an inheritance graph, the most-derived class is organized so that an object has a distinct subobject for each occurrence of each base class. Virtual base classes, however, are combined into a single subobject. That is, each nonvirtual base class results in a distinct subobject, and each virtual base class gets a single subobject, no matter how many times that base class appears in the inheritance graph.

Figure 6-2 illustrates virtual base classes as they are used in the standard I/O streams. Because basic_istream and basic_ostream derive virtually from basic_ios, the basic_iostream class inherits a single copy of basic_ios, with its single copy of flags, buffer, and so on.

Figure 6-2. Virtual base classes
figs/cppn_0602.gif

If a base class is used as a virtual and nonvirtual base class in an inheritance graph, all the virtual instances are shared, and all the nonvirtual instances are separate from the virtual instances. This situation is rare, and usually indicates a programming error.

When constructing a class that has virtual base classes, the most-derived class's constructors must initialize all the virtual base classes. If a constructor does not explicitly initialize a virtual base class, the virtual base class's default constructor is used. Initializers for the virtual base classes are ignored when all base class constructors run.

You should design your virtual base classes so they require only the default constructor. Otherwise, you impose a burden on every derived class to initialize the virtual base class properly. Any change to the parameters of the virtual base class constructor necessitates a corresponding change to every constructor for every class that derives, directly or indirectly, from the virtual base class.

Example 6-21 shows the skeletons of the declarations for the standard I/O streams, as illustrated in Figure 6-2.

Example 6-21. Virtual inheritance in the standard I/O classes
class ios_base;

template<typename charT, typename traits>
class basic_ios : public ios_base;

template<typename charT, typename traits>
class basic_istream : public virtual basic_ios<charT,traits>;

template<typename charT, typename traits>
class basic_ostream : public virtual basic_ios<charT,traits>;

template<typename charT, typename traits>
class basic_iostream : public basic_istream<charT,traits>,
                       public basic_ostream<charT,traits>;

6.4.7 Polymorphic Constructors and Destructors

When an object is constructed, the class of the object being created is called the most-derived class. When a constructor body runs, the class identity and virtual functions are those of the class whose constructor is running, which is not necessarily the most-derived class. If you call a pure virtual function, the behavior is undefined.

Destructors run in the opposite order of constructors. In the body of a destructor, the class identity and virtual functions are those of the destructor's class, not the most-derived class. As mentioned earlier, any class that has a virtual function should also have a virtual destructor. Example 6-22 illustrates how an object's identity changes as it is being constructed.

Example 6-22. Constructing and destroying an object
#include <iostream>
#include <ostream>

struct base {
  base(  )  { whoami(  ); }
  ~base(  ) { whoami(  ); }
  virtual void whoami(  ) { std::cout << "I am base\n"; }
};

struct derived : base {
  derived(  )  { whoami(  ); }
  ~derived(  ) { whoami(  ); }
  virtual void whoami(  ) { std::cout << "I am derived\n"; }
};

struct most_derived : virtual derived {
  most_derived(  )  { whoami(  ); }
  ~most_derived(  ) { whoami(  ); }
  virtual void whoami(  )
  {
    std::cout << "I am most_derived\n";
  }
};

int main(  )
{
  most_derived md;
  // Prints:
  // I am base
  // I am derived
  // I am most_derived
  // I am most_derived
  // I am derived
  // I am base
}
    Team LiB   Previous Section   Next Section