The general rule: non-leaf classes should be abstract. This will yields dividends in the form of increased reliability, robustness, comprehensibility, and extensibility throughout our software.
Redesign concrete base classes to abstract ones
If we have two concrete classes C1 and C2 and we’d like C2 to publicly inherit from C1, we should transform that two-class hierarchy into a three-class hierarchy by creating a new class A and having both C1 and C2 publicly inherit from it:
initial idea | the transformed hierarchy
┌─────────┐ | ┌─────┐
│ C1 │ | │ A │
└─────────┘ | └─────┘
↑ | public inheritance ↗ ↖ public inheritance
┌─────────┐ | ┌────┐ ┌────┐
│ C1 │ | │ C1 │ │ C2 │
└─────────┘ | └────┘ └────┘
For example, we create a software dealing with animals, with two kinds of animals - lizards and chickens - require special handling:
┌──────────┐
│ Animal │
└──────────┘
public inheritance ↗ ↖ public inheritance
┌────────┐ ┌─────────┐
│ Lizard │ │ Chicken │
└────────┘ └─────────┘
The Animal
class embodies the features shared by all the creatures, and the Lizerd
and Chicken
classes specialize Animal
in their own ways:
|
|
Now consider what happens for assignment operation:
|
|
The two problems here:
- partial assignment: only
Animal
members inliz1
get updated fromliz2
, while theliz1
’s Lizard members remain unchanged. - it’s not uncommon for programmers to make assignments to objects via pointers.
Solution 1: virtual functions
|
|
We can customize the return value of the virtual assignment operators here, but the rules of C++ force us to declare identical parameter types for a virtual function in every class in which it is declared, leading to the problem that the assignment operator for the Lizard
and Chicken
must be prepared to accept any kind of Animal
object on the right-hand side of an assignment:
|
|
By making Animal
’s assignment operator virtual, we opened the door to such mixed-type operations. To only allow the same type assignment in virtual assignment operation, we have to make distinctions the types at runtime:
|
|
In this case, we have to worry about std::bad_cast
exceptions thrown by dynamic_cast
when rhs
is not a Lizard
, while paying for extra runtime check cost for valid assignment cases, as well as the harder to maintain code.
Solution 2: adding another function
If we don’t want to pay for the complexity or cost of a dynamic_cast
in the case of valid assignment, we add to Lizard
the conventional assignment operator:
|
|
|
|
Still, clients of Lizard
and Chicken
have to be prepared to catch bad_cast
exceptions and do something sensible with them each time they perform an assignment, which most programmers are unwilling to do.
Solution 3: making partial assignment illegal
The easiest way to prevent partial assignments is to make Animal::operator=
private so that *pAni1 = *pAni2;
is illegal (which calls private Animal::operator=
), but this naive solution has 2 problems:
-
Animal
is a concrete class. A privateoperator=
makes also it illegal to make assignments betweenAnimal
objects:animal1 = animal2;
-
Assignment operator in derived classes are responsible for calling assignment operators in their base classes, but a private
Animal::operator=
makes it impossible to implement theLizard::operator=
andChicken::operator=
correctly to assign theAnimal
part of*this
:1 2 3 4 5 6
Lizard& Lizard::operator=(const Lizard& rhs) { if (this == &rhs) return *this; Animal::operator=(rhs); // can't call private Animal::operator= ... }
Declaring Animal::operator=
as protected
will solve the latter problem, but the first one still remains.
Solution 4: redesign the inheritance hierarchy
Because our orignimal design for the system presupposed that Animal
objects were necessary, we can not abstract Animal
class. Instead, we create a new class - AbstractAnimal
that consists of the common features of Animal
, Lizard
, and Chicken
, and we make that class abstract by making its destructor a pure virtual function1:
|
|
This design gives us everything:
- homogeneous assignments ar allowed for lizards, chickens, and animals;
- partial assignments and heterogeneous assignments are prohibited
- derived class assignment operators may call the assignment operator in the base class
- non of the code written in terms of the
Animal
,Lizard
, orChicken
requires modification - they behave as they did beforeAbstractAnimal
was introduced - though the code does need to be recompiled
In reality when facing constraints
If we want wot create a concrete class that inherits from a concrete class in a thirt-party libraries to which we have only read access, what are we to do?
Then there are only unappealing options:
-
Derive the concrete class from the existing concrete class, and put up with the assignment-related problems, and watch out for the array-related pitfalls (MECpp item 3).
-
Try to find an abstract class higher in the library hierarchy that does most of what we need, then inherit from that class.
-
Implement the new class in terms of the library class we’d like to inherit from: having an object of the library class as a data member, then reimplement the library class’s interface in the new class - this requires to update the class each time the library vendor updates our dependent library classes, and we also give up the ability to redefine virtual functions declared in the library class (we can’t redefine virtual functions unless we inherit them):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
class Window { public: virtual void resize(int newWidth, int newHeight); virtual void repaint() const; int width() const; int height() const; }; class SpecialWindow { // class we wanted to have inherit from Window public: ... int width() const { return w.width(); } // pass through nonvirtual functions int height() const { return w.height(); } virtual void resize(int newWidth, int newHeight); // new impl. of "inherited" virtual functions virtual void repaint() const; private: Window w; };
-
Use the concrete class that’s in the library and modify the software so that the class suffices. Write non-member functions to proved the extra functionality we’d like to add to the class, but can’t - the result may not be as clear, as efficient, as maintainable, or as extensible as we’d like.
-
Declaring a function pure virtual doesn’t mean it has no implementation, it means: 1. the current class is abstract, and 2 any concrete class inheriting from the current class must declare the function as a “normal” virtual function (i.e., without the “=0”). ↩︎