C++ Core Guidelines: The Remaining Rules about Class Hierarchies

I needed three posts to present the 20 rules for class hierarchies in the C++ core guidelines. Here are the seven remaining rules.

 To get the great picture. These are all special rules for class hierarchies.

hierarchy

 Let’s continue with rule C.134. 

C.134: Ensure all non-const data members have the same access level

The previous rule C.133, stated that you should avoid protected data. This means all your non-const data members should be either public or private. An object can have data members that do not prescribe the object’s invariants. Non-const data members that do not prescribe the invariants of an object should be public. In contrast, non-const private data members are used for the object invariants. To remind you: a data member having an invariant cannot have all values of the underlying type.

If you think about class design more generally, you will recognize two kinds of classes.

  • All public: classes with only public data members because the data members have no invariant. Honestly, you should use a struct.
  • All private: classes with only private or const data members that established the invariant.

Based on this observation, all your non-const data members should be public or private.

Imagine if you have a class with public and non-constant invariants. This means that you must maintain the data members’ invariance through the whole class hierarchy. This is quite error-prone because you can not easily control the invariants of your class. Or to say it differently. You break encapsulation.

 

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.

     

    C.135: Use multiple inheritance to represent multiple distinct interfaces

    It is a good idea that your interfaces will only support one aspect of your design. What does that mean? A concrete class must implement all functions if you provide a pure interface consisting only of pure virtual functions. In particular, in the case of a too-rich interface, the class has to implement functions it doesn’t need or make no sense.

    Examples of two distinct interfaces are istream and ostream from the input and output streams library.

    class iostream : public istream, public ostream {   // very simplified
        // ...
    };
    

     

    By combining both interfaces istream for input operations and ostream for output operations, we can quite easily create a new interface.

    C.136: Use multiple inheritance to represent the union of implementation attributes, C.137: Use virtual bases to avoid overly general base classes

    Both rules are pretty special. Therefore I will skip them. The guidelines said that C.137 is relatively seldom used and that C.138 is similar to C. 129: When designing a class hierarchy, distinguish between implementation inheritance and interface inheritance.

    C.138: Create an overload set for a derived class and its bases with using

    This rule is quite apparent and holds for virtual and non-virtual functions. If you don’t use the using-declaration, member functions in the derived class hide the entire overload set. Sometimes this process is called shadowing. Breaking these rules is often quite confusing.

    An example from the guidelines makes this rule quite clear.

    class B {
    public:
        virtual int f(int i) { std::cout << "f(int): "; return i; }
        virtual double f(double d) { std::cout << "f(double): "; return d; }
    };
    class D: public B {
    public:
        int f(int i) override { std::cout << "f(int): "; return i + 1; }
    };
    int main()
    {
        D d;
        std::cout << d.f(2) << '\n';   // prints "f(int): 3"
        std::cout << d.f(2.3) << '\n'; // prints "f(int): 3"
    }
    

     

    Look at the last line. d.f(2.3) with a double argument is called, but the int overload of class D is used; therefore, a narrowing conversion from double to int happens. That is, most of the time, not the behavior you want. To use the double overload of class B, you must introduce it in the scope of D.

     

    class D: public B {
    public:
        int f(int i) override { std::cout << "f(int): "; return i + 1; }
        using B::f; // exposes f(double)
    };
    

     

    C.139: Use final sparingly

     final is a new feature with C++11. You can use it for a class or a virtual function.

    • If you derive a class My_widget final from a class Widget, you cannot further derive a class from My_widget.

    class Widget { /* ... */ };
    
    // nobody will ever want to improve My_widget (or so you thought)
    class My_widget final : public Widget { /* ... */ };
    
    class My_improved_widget : public My_widget { /* ... */ };  // error: can't do that
    

     

    • You can declare a virtual function as final. That means you can not override the function in derived classes.
    struct Base
    {
        virtual void foo();
    };
     
    struct A : Base
    {
        void foo() final; // A::foo is overridden and it is the final override
    };
     
    struct B final : A // struct B is final
    {
        void foo() override; // Error: foo cannot be overridden as it's final in A
    };
    

     

    If you use final, you seal your class hierarchy on a class base or a virtual function base. Often that has consequences you can not oversee. The potential performance benefit of using final should be your second thought.

    C.140: Do not provide different default arguments for a virtual function and an overrider

    Not following this rule can confuse. Have a look.

    // overrider.cpp
    
    #include <iostream>
    
    class Base {
    public:
        virtual int multiply(int value, int factor = 2) = 0;
    };
    
    class Derived : public Base {
    public:
      int multiply(int value, int factor = 10) override {
        return factor * value;
      }
    };
    
    int main(){
    
      std::cout << std::endl;
    
      Derived d;
      Base& b = d;
    
      std::cout << "b.multiply(10): " << b.multiply(10) << std::endl; 
      std::cout << "d.multiply(10): " << d.multiply(10) << std::endl;  
    
      std::cout << std::endl;
    
    }
    

     

     Here is the quite surprising output of the program.

    overrider

    What’s happening? Both objects b and d call the same function because the function is virtual and, therefore, late binding happens. This will not hold for the data such as the default arguments. They are statically bound, and early binding happens.

    What’s next?

    Now we are done with the design of class hierarchies. The question remains: who can we access the objects in the class hierarchy? Of course, I will answer this question in the next post.

     

     

     

    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,and Matt Godbolt.

    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 *