C++ Core Guidelines: More Rules to Class Hierarchies

In the last post, I started our journey with the rules to class hierarchies in modern C++. The first rules had a quite general focus. This time, I will continue our journey. Now, the rules have a closer focus.

 Here are the rules for class hierarchies.

hierarchy

Let's continue with the fourth one.

C.129: When designing a class hierarchy, distinguish between implementation inheritance and interface inheritance

At first, what is the difference between implementation inheritance and interface inheritance? The guidelines gives a definite answer. Let me cite it.

  • interface inheritance is the use of inheritance to separate users from implementations, in particular to allow derived classes to be added and changed without affecting the users of base classes.
  • implementation inheritance is the use of inheritance to simplify implementation of new facilities by making useful operations available for implementers of related new operations (sometimes called “programming by difference”).

Pure interface inheritance will be if your interface class has only pure virtual functions. In contrast, if your base class has data members or implemented functions, you have an implementation inheritance. The guidelines gives an example of mixing both concepts. 

class Shape {   // BAD, mixed interface and implementation
public:
    Shape();
    Shape(Point ce = {0, 0}, Color co = none): cent{ce}, col {co} { /* ... */}

    Point center() const { return cent; }
    Color color() const { return col; }

    virtual void rotate(int) = 0;
    virtual void move(Point p) { cent = p; redraw(); }

    virtual void redraw();

    // ...
public:
    Point cent;
    Color col;
};

class Circle : public Shape {
public:
    Circle(Point c, int r) :Shape{c}, rad{r} { /* ... */ }

    // ...
private:
    int rad;
};

class Triangle : public Shape {
public:
    Triangle(Point p1, Point p2, Point p3); // calculate center
    // ...
};

 

Why is the class Shape bad?

  • The more the class grows, the more difficult and error-prone it may become to maintain the various constructors.
  • The functions of the Shape class may never be used.
  • If you add data to the Shape class, a recompilation may become probable.

If Shape would be a pure interface consisting only of pure virtual functions, it wouldn't need a constructor. Of course with a pure interface, you have to implement all functionality in the derived classes.

How can we get the best of two worlds: stable interfaces with interface hierarchies and code reuse with implementation inheritance. One possible answer is dual inheritance. Here is a quite sophisticated receipt for doing it.

1. Define the base Shape of the class hierarchy as pure interface

class Shape {   // pure interface
public:
    virtual Point center() const = 0;
    virtual Color color() const = 0;

    virtual void rotate(int) = 0;
    virtual void move(Point p) = 0;

    virtual void redraw() = 0;

    // ...
};

 

2. Derive a pure interface Circle from the Shape

class Circle : public Shape {   // pure interface
public:
    virtual int radius() = 0;
    // ...
};

 

3. Provide the implementation class Impl::Shape 

class Impl::Shape : public Shape { // implementation
public:
    // constructors, destructor
    // ...
    Point center() const override { /* ... */ }
    Color color() const override { /* ... */ }

    void rotate(int) override { /* ... */ }
    void move(Point p) override { /* ... */ }

    void redraw() override { /* ... */ }

    // ...
};

 

4. Implement the class Impl::Circle by inheriting from the interface and the implementation

class Impl::Circle : public Circle, public Impl::Shape {   // implementation
public:
    // constructors, destructor

    int radius() override { /* ... */ }
    // ...
};

 

5. If you want to extend the class hierarchy, you have to derive from the interface and from the implementation 

The class Smiley is a pure interface, derived from Circle. The class Impl::Smiley is the new implementation, public derived from Smiley and from Impl::Circle.

class Smiley : public Circle { // pure interface
public:
    // ...
};

class Impl::Smiley : public Smiley, public Impl::Circle {   // implementation
public:
    // constructors, destructor
    // ...
}

 

Here is once more the big picture to the two hierarchies.

  • interface: Smiley -> Circle -> Shape
  • implementation: Impl::Smiley -> Imply::Circle -> Impl::Shape

 

By reading the last lines maybe you had a déjà vu. You are right. This technique of multiple inheritance is similar to the adapter pattern, implemented with multiple inheritance. The adapter pattern is from the well-known design pattern book.

The idea of the adapter pattern is to translate an interface into another interface. You achieve this by inheriting public from the new interface and private from the old one. That means you use the old interface as implementation.

AdapterrClass

C.130: Redefine or prohibit copying for a base class; prefer a virtual clone function instead

I can make it quite short. Rule C.67 give a good explanation for this rule.

C.131: Avoid trivial getters and setters

If a trivial getter or setter provides no semantic value, make the data item public. Here are two examples for trivial getters and setters:

class Point {   // Bad: verbose
    int x;
    int y;
public:
    Point(int xx, int yy) : x{xx}, y{yy} { }
    int get_x() const { return x; }
    void set_x(int xx) { x = xx; }
    int get_y() const { return y; }
    void set_y(int yy) { y = yy; }
    // no behavioral member functions
};

 

x and y can have an arbitrary value. This means an instance of Point maintains no invariant on x and y. x and y are just values. Using a struct as a collection of values is more appropriate.

struct Point {
    int x {0};
    int y {0};
};

 

C.132: Don’t make a function virtual without reason

This is quite obvious. A virtual function is a feature that you will not get for free.

A virtual function

  • increases the run-time and the object code-size
  • is open for mistakes because it can be overwritten in derived classes

C.133: Avoid protected data

Protected data make your program complex and error-prone. If you put protected data into a base class, you can not reason about derived classes in isolation and, therefore, you break encapsulation. You always have to reason about the whole class hierarchy.

This means you have to answer at least this three questions.

  1. Do I have to implement a constructor to initialise the protected data?
  2. What is the actual value of the protected data if I use them?
  3. Who will be affected if I modify the protected data?

Answering this questions becomes more and more difficult the bigger you class hierarchy becomes.

If you think about it: protected data is a kind of global data in the scope of the class hierarchy. And you know, non-const global data is bad.

Here is the interface Shape enriched with protected data.

class Shape {
public:
    // ... interface functions ...
protected:
    // data for use in derived classes:
    Color fill_color;
    Color edge_color;
    Style st;
};

 

What's next

We are not done with the rules for class hierarchies and, therefore,  I will continue with my tour in the next post.

I have to make a personal confession. I learned a lot by paraphrasing the C++ core guidelines rules and provided more background info if that was necessary from my perspective. I hope the same will hold for you. I would be happy to get comments. So, what's your opinion?

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson and Paul Baxter.

 

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 43

All 506509

Currently are 152 guests and no members online