C++ Core Guidelines: Class Hierarchies

Let's talk in this post about rules for class hierarchies in general and in particular. The C++ core guidelines have about thirty rules in total; therefore, I have a lot to talk about.

At first, what is a class hierarchy? The C++ core guidelines gives a clear answer. Let me rephrase it. A class hierarchy represents a set of hierarchically organized concepts. Base classes act typically as interfaces. They are two uses for interfaces. One is called implementation inheritance and the other interface inheritance.

The first three line are more general or to say it differently: they are a summary of the more detailed rules.

hierarchy 73335 640

Class hierarchy rule summary:

C.120: Use class hierarchies to represent concepts with inherent hierarchical structure (only)

This is a quite obvious. If you model something in the code which has an inherently hierarchical structure you should us a hierarchy. For me, the easiest way to reason about my code is if I have a natural match between the code and the world.

For example, I had to model a complex system. This system was a family of defibrillators which consist of a lot of subsystems. For example, one subsystem was the user interfaces. The requirement was that the defibrillators should use different user interfaces such as a keyboard, a touch screen, or a few buttons. This system of subsystem was inherently hierarchical; therefore, I modelled it a hierarchical way. The great benefit was that the software was quite easy to explain in a top-down fashion because there was this natural match between the real hardware and the software.

But of course, the classic example for using a hierarchy is the design of graphical user interface (GUI). This is the example the C++ core guidelines is using.

class DrawableUIElement {
public:
  virtual void render() const = 0;
// ...
};
class AbstractButton : public DrawableUIElement {
public:
  virtual void onClick() = 0;
// ...
};
class PushButton : public AbstractButton {
  virtual void render() const override;
  virtual void onClick() override;
// ...
};
class Checkbox : public AbstractButton {
// ...
};

 

If something is not inherently hierarchical, you should not model it in a hierarchical way. Have a look here.

template<typename T>
class Container {
public:
    // list operations:
    virtual T& get() = 0;
    virtual void put(T&) = 0;
    virtual void insert(Position) = 0;
    // ...
    // vector operations:
    virtual T& operator[](int) = 0;
    virtual void sort() = 0;
    // ...
    // tree operations:
    virtual void balance() = 0;
    // ...
};

 

Why is the example bad? You have only to read the comments. The class template Container consists of pure virtual functions for modelling a list, a vector, and a tree. That means if you use Container as an interface you have to implement three disjunct concepts.

C.121: If a base class is used as an interface, make it a pure abstract class

An abstract class is a class that has at least one pure virtual function. A pure virtual function (virtual void function() = 0 ) is a function that must be implemented by a derived class if that class should not be abstract.

Only for completeness reasons. An abstract class can provide implementations of pure virtual functions. A dervived class can, therefore, use these implementations.

Interfaces should usually consist of public pure virtual functions and a default/empty virtual destructor (virtual ~My_interface() = default).  If you don't follow the rule, something bad may happen.

class Goof {
public:
// ...only pure virtual functions here ...
// no virtual destructor
};
class Derived : public Goof {
string s;
// ...
};
void use()
{
  unique_ptr<Goof> p {new Derived{"here we go"}};
  f(p.get()); // use Derived through the Goof interface 
} // leak

 

If p goes out of scope, it will be destroyed. But Goof has no virtual destructor; therefore, the destructor of Goof and not Derived is called. The bad effect is that destructor of the string s is not called.

C.122: Use abstract classes as interfaces when complete separation of interface and implementation is needed

Abstract classes are about separation of interface and implementation. The effect is that you can use a different implementation of Device in the following example during runtime because you only depend on the interface.

struct Device {
  virtual void write(span<const char> outbuf) = 0;
  virtual void read(span<char> inbuf) = 0;
};
class D1 : public Device {
// ... data ...
void write(span<const char> outbuf) override;
  void read(span<char> inbuf) override;
};
class D2 : public Device {
// ... different data ...
  void write(span<const char> outbuf) override;
  void read(span<char> inbuf) override;
};

 

In my seminars to design pattern, I often call this rule the meta-design pattern that is the base for a lot of the design pattern from the most influential software book: Design Patterns: Elements of Reusable Object-Oriented Software.

Designing rules for classes in a hierarchy summary:

Here are the more detailed rules in summary. The guidelines has 15 of them.

Today I write about the first three.

C.126: An abstract class typically doesn’t need a constructor

An abstract class has typically not data and, therefore, needs not constructor to initialise them.

C.127: A class with a virtual function should have a virtual or protected destructor

A class with a virtual function is most of the times used via a pointer or a reference to the base. If you explicitly delete the derived class via a pointer or a reference to the base or indirectly via a smart pointer, you want to be sure, that also the destructor of the derived class is called. This rule is quite similar to the rule C.121 which talks about pure virtual functions.

Another way to solve the destruction issue is to have a protected and non-virtual base class destructor. This destructor guarantees that you can not delete a derived object via a pointer or reference to the base.

C.128: Virtual functions should specify exactly one of virtual, override, or final

 In C++11 we have three keywords to deal with overriding.

  • virtual: declares a function that can be overwritten in derived classes
  • override: ensures that the function is virtual and overwrites a virtual function of a base class
  • final: ensures that the function is virtual and cannot be overriden by a derived class

According to the guidelines, the rules for the usage of the three keywords are straightforward: "Use virtual only when declaring a new virtual function. Use override only when declaring an overrider. Use final only when declaring a final overrider."

struct Base{
    virtual void testGood(){}
    virtual void testBad(){}
};

struct Derived: Base{
    void testGood() final {}
    virtual void testBad() final override {}
};

int main(){
    Derived d;
}

 

The method testBad() in the class Derived has a lot of redundant information.

  • You should only use final or override, if the function is virtual. Skip virtualvoid testBad() final override{}
  • Using the keyword final without the virtual keyword is only valid if the function is already virtual; therefore, the function must override a virtual function of a base class. Skip override: void testBad() final {}

What's next?

The remaining twelve rules for class hierarchies are missing. My next post will close this gap.

 

 

Thanks a lot to my Patreon Supporter: Eric Pederson.

 

Get your e-book at leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11 and C++14 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 500 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Add comment


My Newest E-Books

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 378

All 459291

Currently are 150 guests and no members online