[Effective-cpp] Item 33: Make non-leaf classes abstract.

Tarjei Knapstad tarjeik at chemcon.no
Wed Jun 4 06:35:49 EDT 2003


Sorry about the delay, but here it is. (forgot all about it/caught up in
work/<your favourite excuse here>)



Item 33. Make non-leaf classes abstract.

As with my other items I've tried to summarize the item as-is. I've
added a few comments of my own throughout inside [].

Summary:

In this item Scott sets out to explain to us why Animal should be an
abstract class in the following hierarchy:

		Animal
                /    \
               /      \
           Lizard    Chicken

If we consider the following declaration of the assignment operator
for the classes above...:

class Animal {
public:
  Animal& operator=(const Animal& rhs);
};

class Lizard: public Animal {
public:
  Lizard& operator=(const Lizard& rhs);
};

class Chicken: public Animal {
public:
  Chicken& operator=(const Chicken& rhs);
};

...Scott points out that partial assignment will occur when trying to 
assign two Lizard objects through Animal pointers, and [far more
important I think] that people tend to write code like this without
hesitating.

Scotts first approach for a remedy is to make the assignment
op. virtual, thereby changing the argument to 'const Animal& rhs' in
all declarations above. This allows for mixed type assignments though
which is usually not desired behaviour. To solve the problem one can
do a 'dynamic_cast' to the type of 'this' inside the implementation
which will throw std::bad_cast if mixed assignments are
attempted. To avoid the overhead of dynamic_cast for "normal"
assignments one can add a second, non-virtual conventional assignment 
operator for the Lizard and Chicken classes.

Scott criticises the dynamic_cast solution on the following points:

1. Some compilers lack support for dynamic_cast so code may not be
   portable [this is clearly dated, are there still compilers out there
   not supporting dynamic_cast and RTTI?]
2. It requires client code to handle std::bad_cast and there aren't
   many programmers willing to do that [I highly disagree with this,
   the assignemnt operator should claim the std::bad_cast exception 
   to make it obvious to clients that they must handle the possibility 
   of this kind of exception or suffer the evil consequences. This
   doesn't mean the main point to be made here is not legit, I just
   don't see this as valid criticism.]

Scotts next step is to declare Animal's assignment operator private to
avoid partial and mixed-type assignments. This blocks mis-/abuse from
the client code, but makes it impossible to assign Animal objects and
also makes it impossible for the derived classes to do proper
assignment by blocking access to the base classes assignment
operator. The latter can be resolved by making the assignment operator
protected instead of private. 

The final solution is a redesing where an AbstractAnimal class is
introduced and the three classes Lizard, Animal and Chicken inherit
from this abstract class. This design allows for homogenous assignment
and eliminates the above mentioned pitfalls and problems. Scott notes
that if you do this sort of refactoring and can't readily identify a
pure virtual function in the base, you should make the destructor pure
virtual (just don't forget to implement it).

It is also pointed out that if you have two concrete classes C1 and C2
and you want C2 to publicly inherit from C1, you should identify their
commonalities and put those into a common abstract base class A. This
transformation forces you to identify what the classes have in common
and turn this into a formal abstraction in a base class. 

Scott defines a "useful abstraction" as an abstraction that is useful 
in more than one context. The first time a concept is needed, creation
of an abstract and a concrete class is not justified, but when it's
needed in more than context you can justify your design decision. Note
that this is one possible way to identify abstractions - not by any
means the only.

Finally Scott presents a few possible solutions (all with their quirks
and drawbacks) when you need to extend concrete classes in third party 
libraries:

1. Derive from the concrete class and handle the problems discussed.

2. Identify and inherit from an abstract class further up the
   hierarchy if there is one. This may lead to reimplementation of
   functionality allready present in the concrete class.

3. Use containment instead of inheritance. This will result in a fair
   bit of wrapping code which will need to be updated as the third
   party lib evolves. It also restricts polymorphism.

4. Use the concrete class as-is. Modify your own code to work with it
   and implement any needed functionality in non-member functions.


Regards,
--
Tarjei




More information about the Effective-cpp mailing list