Supports C Virtual Constructor

C ++ Core Guidelines: The remaining rules for class hierarchies

Three articles were necessary to introduce the 20 rules for class hierarchies in the C ++ Core Guidelines. This article concludes the miniseries with the remaining seven rules.

In the familiar manner, there is first of all the big picture. Here are the special rules for class hierarchies.

Let's start with rule C.134:

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

The previous rule C.133 was that you should not use "protected" data. It states that your non-constant data should either be all public or private. An object can have attributes that determine the invariance of the object or not. Non-constant data that does not determine the invariance of attributes should be public. In contrast, non-constants and private attributes define the invariance of the object. As a reminder, a data attribute that is invariant has a limited scope.

If we look more broadly at class design, two types of classes can be identified.

  • Everything public: Classes that only have public attributes, as there are no restrictions for the attributes. Here should be a struct come into use.
  • Everything private: Classes that only have private and constant attributes that define the restrictions for the concrete objects.

Based on this observation, all non-constant attributes of the class should be either public or private.

Imagine you have a class with nonconstants and public attributes. This means that the restrictions for these attributes must be maintained in the entire class hierarchy. This is of course very error-prone, because the restrictions cannot be easily controlled. In other words, class design breaks one of the fundamental rules of object-oriented design: encapsulation.

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

It is a very good idea if an interface supports only one aspect of class design. What exactly does that mean? If you are designing a pure interface that consists only of pure virtual functions, concrete classes must implement all functions. In particular, if the interface is too powerful, this means that the concrete class has to implement functions that it neither needs nor make sense for it.

An example of two separate interfaces are the istream- and ostream-Interfaces of the input and output streams.

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

By combining the interfaces istream for input and ostream A new interface can easily be designed for the output operations.

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 very special. So I won't go into them. The guidelines say that rule C.137 is used relatively seldom and rule C.138 is very similar to rule 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 pretty obvious and applies to virtual and non-virtual functions. If you do not use the using declaration, then the derived class hides the functions of the same name in the base classes. In the English-language literature, the functions of the same name in the base class are referred to as overload set referred to. The term is also popular for this process of hiding the functions of the base class of the same name shadowing used. The potential for surprise is very great if you do not observe this rule.

This is exactly what an example from the guidelines shows.

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) will be with a doubleArgument called. Still it comes for int overloaded function of the class D. for use. This also leads to a narrowing conversion of double on int takes place. It is very likely that this does not correspond to the intention of the author. To the doubleOverloading the class B. to be used, it must be entered in the Scope for D. to be introduced.

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 classes or virtual functions.

  • If a class My_widget final is derived from the Widget class can be derived from the My_widget cannot be further derived.
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
  • A virtual function can be used as a final be declared. This means that the function can no longer be overwritten.
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 final is used, you prevent the expansion of the class or its virtual functions. This often has consequences that only become apparent much later. A potential, small performance advantage through the use of final should not sacrifice the extensibility of the class hierarchy.

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

If you ignore this rule, nasty surprises can occur.

// overrider.cpp

#include

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's the nasty surprise. The program does not have the expected result.

What's happening? Both objects b and d call the same function because it is virtual. This means that late binding is used. However, this does not apply to the data such as the default arguments. They are statically bound and thus early binding is used.

What's next?

Now it is done. In the previous two articles and this one, I introduced all 20 rules about class hierarchies. One question remains open: How can the objects of the class hierarchies be addressed. The next article will deal with precisely the answer to this question.

Additional Information:

  • The PDF package with the article on C ++ 17 is available at www.grimm-jaud.de. In addition to the 30-page PDF, it contains all code examples and a simple one cmake-File.

Rainer Grimm

Rainer Grimm has worked as a software architect, team and training manager for many years. He enjoys writing articles on the programming languages ​​C ++, Python and Haskell, but also likes to speak frequently at specialist conferences. On his blog Modernes C ++ he deals intensively with his passion for C ++.

Read CV »