The quest of private inheritance in C++

Sandor Dargo - Apr 1 '20 - - Dev Community

This article has been originally posted on my blog. If you are interested in receiving my latest articles, please sign up to my newsletter.

I love mentoring.

It requires a huge quantity of humility, and if you possess it, it will bring you tremendous benefits on a human as well as on a technical level.

A few weeks ago, I met with one of my mentees who told me that she finally started to work on interesting tasks. In the team, they have been doing pair programming, but they don't always have the time to go into deeper explanations. I asked Cathy if she faced some issues she would like to discuss and she came up with private inheritance that they tried to use with more-or-less success.

We talked a little bit about it, but I had to tell the truth that I had never used it since school probably, so I didn't remember exactly how it works.

Have you ever had teachers who returned questions as homework when he didn't know the answer?

I wanted to play. We opened up my laptop, connected to an online IDE/Compiler and started to have some fun.

Experimenting with non-public inheritance

We started to with a simple example of the usual public inheritance which worked as expected.

#include <iostream>

class Base {
public:
    Base() = default;
    virtual ~Base() = default;
    virtual int x() { 
        std::cout << "Base::x()\n";
        return 41; 
    }

protected:
    virtual int y() { 
        std::cout << "Base::y()\n";
        return 42; 
    }
};

class Derived : public Base {
public:
    int x() override { 
        std::cout << "Derived::x()\n";
        return Base::y(); 
    }
};

int main() {
    Base* p = new Derived();
    std::cout << p->x() << std::endl;
}
Enter fullscreen mode Exit fullscreen mode

In this very example we take advantage of being able to access Derived::x(), through a pointer to Base. We call Base::y() from Derived::x() just to make a call from a function that is public in both Base and Derived to a protected function in Base.

Then we decided to take the experimental way combining with the methodology of Compiler Driven Development. We changed the public keyword in the inheritance to protected and recompiled waiting for the compilation errors.

This line didn't compile anymore.

Base* p = new Derived();
// main.cpp:25:27: error: 'Base' is an inaccessible base of 'Derived'
//   25 |     Base* p = new Derived();
Enter fullscreen mode Exit fullscreen mode

Seemed reasonable, no big surprise at first sight. So I just changed that line and it compiled.

Derived* p = new Derived();
Enter fullscreen mode Exit fullscreen mode

As the next step, we changed the inheritance to private and clicked on the compile button. It expected the compilation to fail, I expected that Base::y() would be handled as private to Derived and as such in Derived::x() would fail to compile. But. It. Compiled.

This meant that something about non-public inheritance we didn't remember well or got completely misunderstood.

Let's stop for a second. Is this embarrassing?

It is.

I could start enumerating some excuses. But who cares? Nobody. And those excuses wouldn't matter anyway. What is important is that I realized I didn't know something well and I used the situation to learn something.

It was high time to open up some pages about non-public inheritance and re-read them carefully.

The access specifier of the inheritance doesn't affect the inheritance of the implementation. The implementation is always inherited based on the function's access level. The inheritance's access specifier only affects the accessibility of the class interface.

This means that all the public and protected variables and functions will be useable from the derived class even when you use private inheritance.

On the other hand, those public and protected elements of the base class will not be accessible from the outside through the derived class.

When does this matter?

It counts when the next generation is born.

A grandchild of a base class, if its parent inherited privately from the base (the grandparent...), it won't have any access to the base's members and functions. Not even if they were originally protected or even public.

Just to make the point here is another example. You can play with it on coliru.

#include <iostream>

class Base {
public:
    Base() = default;
    virtual ~Base() = default;
    virtual int x() { 
        std::cout << "Base::x()\n";
        return 41; 
    }

protected:
    virtual int y() { 
        std::cout << "Base::y()\n";
        return 42; 
    }

};

class Derived : private Base {
public:
    int x() override { 
        std::cout << "Derived::x()\n";
        return Base::y(); 
    }
};

class SoDerived : public Derived {
public:
    int x() override { 
        std::cout << "SoDerived::x()\n";
        return Base::y(); 
    }
};

int main() {
    SoDerived* p = new SoDerived();
    std::cout << p->x() << std::endl;
}
Enter fullscreen mode Exit fullscreen mode

What is private inheritance for?

We probably all learnt that inheritance is there for expressing is-a relationships, right?

If there is Car class inheriting from Vehicle, we can all say that a Car is a Vehicle. Then Roadster class inherits from Car, it's still a Vehicle having access to all Vehicle member( function)s.

But what if that inheritance between Vehicle and Car was private? Then that little shiny red Roadster will not have access to the interface of Vehicle, even if it publicly inherits from Car in the middle.

We simply cannot call it an is-a relationship anymore.

It's a has-a relationship. Derived class, in this specific example Car, will have access to the Base(=> Vehicle) and exposes it based on the access level, protected or private. Well, this latter means that it's not exposed. It serves as a private member.

In the case of protected, you might argue that well, Roadster still have access to Vehicle, that is true.

But you cannot create a Roadster as a Vehicle, in case of non-public inheritance this line will not compile.

Vehicle* p = new Roadster();
Enter fullscreen mode Exit fullscreen mode

Just to repeat it, non-public inheritance in C++ expresses a has-a relationship.

Just like composition. So if we want to keep the analogy of cars, we can say that a Car can privately inherit from the hypothetical Engine class - while it still publicly inherits from Vehicle. And with this small latter addition of multiple inheritance, you probably got the point, why composition is easier to maintain than private inheritance.

But even if you have no intention of introducing an inheritance tree, I think private inheritance is not intuitive and it's so different from most of the other languages that it's simply disturbing to use it. It's not evil at all, it'll be just more expensive to maintain.

That's exactly what you can find on the ISO C++ page.

Use composition when you can, private inheritance when you have to.

But when do you have to use private inheritance?

According to the above reference ISO C++ page, you have a valid use-case when the following conditions apply:

  • The derived class has to make calls to (non-virtual) functions of the base
  • The base has to invoke (usually pure-virtual) functions from the derived

Conclusion

Today, I made the point that if the humble and more difficult road is taken, mentoring will pay off with great benefits to both parties. Recently, that's how I (re)discovered non-public inheritance in C++.

Non-public inheritance is - to me - a syntactically more complicated way to express a has-a relationship compared to composition. Even though from time to time you might encounter use-cases, when it provides some benefits, most often it just results in code that is more difficult to understand and maintain.

Hence, do as the C++ Standard FAQ says: Use composition when you can, private inheritance when you have to.

Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player