Effective C++ item 34: Differentiate Between Inheritance of Interface and Inheritance of Implementation

When you design public inheritance classes, you will mainly use three type of inheritance features.

  • Pure virtual functions specify inheritance of interface only.
  • Simple (impure) virtual functions specify inheritance of interface plus inheritance of a default implementation.
  • Non-virtual functions specify inheritance of interface plus inheritance of a mandatory implementation.

Let me give an example to explain these in plain English

1
2
3
4
5
6
7
8
9
class Shape {
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
...
};
class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };
  • Pure virtual function draw means both Rectangle and Ellipse have to define their own implementation of draw.
  • Simple (impure) virtual function error means both Rectangle and Ellipse have the option to define error function. If they don’t, the default implementation in base class Shape will be used.
  • Non-virtual function objectID means both Rectangle and Ellipse should not redefine this method and will use the implementation in Shape base class.

An interesting problem for simple virtual function is it specifies both interface and a optional default implementation. Sometimes such feature will cause problem because new derived class is added but forget to implement such function. Consider following class

1
2
3
4
5
6
7
8
9
class Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{
// default code for flying an airplane to the given destination
}

What if someone add ModelX which derives from Airplane, but ModelX requires a special implementation of fly in order for it to work correctly, but programmer forgets to implement such. Compiler will happily compile because the default implementation will be used. The poor programmer will only realize the mistake by running the program, hopefully not in production environment which may cost life in this case.
How to provide a default implementation as well as giving compilation error if someone forgets about it? There are two common approaches to this problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
// default code for flying an airplane to the given destination
}
class ModelY: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
class ModelX: public Airplane {
public:
virtual void fly(const Airport& destination)
{ // ModelX's special implementation }
...
};

By making fly pure virtual, programmer will get error if they forget to implement such function in derived class. If they want to use the default behavior, they can simply call defaultFly. By doing so, we separated the interface fly and default implementation defaultFly in base class.

Another solution is to implement fly pure virtual function in base class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination)
{
// default code for flying an airplane to the given destination
}
class ModelY: public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelX: public Airplane {
public:
virtual void fly(const Airport& destination)
{ // ModelX's special implementation }
...
};

Don’t be surprised to see that you can actually provide an implementation for pure virtual class. This example is the exact reason compiler supports such feature. By doing so, we basically achieved making fly mandatory and providing a default implementation.

The only slight difference between these two solutions is that defaultFly can be protected method while implementation of pure fly has to be public. In addition, if we provide default implementation for pure virtual function, people may not aware of such unless dig into cpp file, which they may assume to be empty for pure interface base class.

Reference:
“Effective C++” Third Edition by Scott Meyers.