hierarchy 73335 640

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; therefore, I have a lot to discuss.

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

The first three lines are more general, or to say it differently: they summarize 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 quite obvious. You should use a hierarchy if you model something in the code with an inherently hierarchical structure. 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 that consisted of a lot of subsystems. For example, one subsystem was the user interface. 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 subsystems was inherently hierarchical; therefore, I modeled it hierarchically. The great benefit was that the software was relatively easy to explain top-down because there was this natural match between the actual hardware and the software.

But, of course, the classic example of using a hierarchy in the design of a graphical user interface (GUI). This is the example the C++ core guidelines are 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 hierarchically. Have a look here.

Rainer D 6 P2 500x500

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    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 terrible? You have only to read the comments. The class template Container consists of pure virtual functions for modeling a list, a vector, and a tree. If you use Container as an interface, you must 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 a derived class must implement if that class should not be abstract.

    Only for completeness reasons. An abstract class can provide implementations of pure virtual functions. A derived 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 terrible 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 harmful effect is that the 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 the separation of interface and implementation. 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 patterns, I often call this rule the meta-design pattern that is the base for a lot of the design patterns 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 have 15 of them.

    Today I write about the first three.

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

    An abstract class typically has no data and needs no constructor to initialize them.

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

    A class with a virtual function is mainly used via a pointer or a reference to the base. Suppose you explicitly delete the derived class via a pointer, a reference to the base, or indirectly via a smart pointer. In that case, you want to be sure that the derived class’s destructor is also called. This rule is similar to rule C.121, which discusses pure virtual functions.

    Another way to solve the destruction issue is to have a protected and non-virtual base class destructor. This destructor guarantees 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 overridden 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 override. Use final only when declaring a final override.”

    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 Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *