Jonathan Boccara's blog

Use Private Inheritance to Restrict Interfaces

Published August 28, 2020 - 0 Comments

This is a guest post from Federico Kircheis. Federico is a (mainly C++) developer in Berlin, always looking how to improve himself, and finding interesting problems to solve. Federico is the author of the viral post Function Poisoning in C++.

Inheritance is a form of code reuse and does not necessarily indicate a relationship between classes.

C++ has different forms of inheritances, in my experience the most widely used form is public and virtual.

The main reason is probably that other languages (like Java) use it widely and only support this type of inheritance.

The second most-seen in the wild, again, in my experience, form of inheritance is public and non-virtual, the most common use-case is the CRTP pattern.

But C++ also gives the possibility to use protected and private inheritance (virtual and non-virtual).

Those forms of inheritance are less common and are most of the time disregarded favoring other techniques.

For example, isoccp.org has a FAQ entry about private inheritance and concludes that

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

since

private inheritance […] it’s just more expensive to maintain

Thus, as of today, when talking about inheritance, the first things that come to mind are classes extending functionalities or implementing a given interface.

This way of thinking is also currently enforced by the CppCoreGuidelines:

C.120: Use class hierarchies to represent concepts with inherent hierarchical structure (only)

The use-case: restricting an interface

Suppose we have a “big” interface, maybe even something like a God class, and because we want to restrict future feature creep, we want to expose only a smaller subset and/or change the behavior of a small subset of the class.

Ideally, we would have enough time to disentangle all internal dependencies and split the God class into multiple classes.

If possible, this should be the approach, but most of the time it’s much easier to let entropy increase and call it a day.

Those who are more interested in reasoning about the code and try to clean up the mess would try to avoid increasing the entropy of the system.

There are several approaches, but most of them have many drawbacks.

Leverage on static analysis and code review

Instead of changing anything about the class, just use it as is (or add the missing functionality) and leverage on external tools for ensuring that no unwanted functionality of the class is used.

While it might work in theory, in practice it is hard to enforce, unless the scope is very small, like only one (small) function.

First of all, there should be a central place, apart from the coding guidelines which enlists which methods of which class can be used in which situations.

Secondly, C++ has a static type system that is exactly designed for avoiding those types of errors at compile-time!

Thirdly, while it is theoretically possible to write an external tool to check those rules automatically, the hassle is probably not worth it, as parsing C++ is not an easy job, and even if it would, this external tool should be integrated with the build system to ensure that no violations occur.

Make a class hierarchy (public inheritance) and override the unwanted functions

This is the most common approach I saw in practice.

The Java library even uses such pattern in its standard library, for example, some implementation of java.util.List throws UnsupportedOperationException on some operations.

Ideally, the interface Collection should not have a method like add, if it makes sense for subclasses (like immutable collections) not to support such operations.

It would have been better to have two interfaces, a Collection and a UnmodifiableCollection.

This would have permitted a user to know, at compile-time, that some operations are not permitted, instead of detecting the error while the code is executed.

So, while creating a public subclass and overload all methods we do not want to support to signalize the error (through no-op, throwing, aborting, loggin, …), this blacklist approach is not ideal.

If there are many functions, we need to overload a lot of them. This is a repetitive and error-prone task, as it might be easy to oversee some functions.

As already mentioned, detecting the error at runtime is also suboptimal.

Another drawback of using public inheritance for our purpose is that we need to make the methods we want to restrict virtual in the base class (and preferably, the destructor as well). This can have a performance cost, and if we don’t have control on the base class (for example, if it is a third party), this becomes impossible.

Wrapping the class manually

Another approach is creating a wrapper class, where internally the “big bad class” is declared as a private member variable.

As we need now to reimplement every function, we have full control over what functionality to expose. It means there is no need to detect at runtime possible errors.

On the other hand, creating such a class might be a lot of repetitive work.

For every function we want to support, we need to create a wrapper function and forward all arguments.

If there are function overloads, even if from a certain point of view there are not a lot of functions, it can still be a lot to type or copy-paste.

And especially with similar-looking functions, it is easy to oversee errors, like calling internally the wrong function, the wrong overload, etc.etc.

Depending on the scope, this approach might duplicate a lot of code, which needs to be kept in sync with the wrapped class and is thus not always welcomed.

Wrapping the class automatically

It is possible, thanks to templates even without macros, to wrap a class and define custom prefixes and suffixes that would apply to every member function.

If we do not want to hide any function, and only want to add the same logic on every function call (like logging, flushing data to disk, send data over the wire, taking a lock, …) then this is a viable approach.

Unfortunately, it is for a very specific use-case.

If we want to:

  • hide at least a single function
  • execute a different action on at least a specific function
  • add a new function or overload

then this approach won’t help.

Unless we have something like reflection/introspection the wrapping class can only execute the same action, on every wrapped function.

Using an external code generator

It is obviously possible to use an external code generator, but if possible such an approach is generally avoided because:

  • it might not be easy to integrate with the build system
  • it adds a layer of indirection in a different language, so it might be harder to review

While both of those advantages might be not that relevant, the practice of using code generators, except for big libraries (like QT slots and signal mechanism) is not that common. One of the reason might be that there is no standardized tool for generating readable c++ code.

Thus at the end one has not only to debug the generated code, but even the generator itself.

Using private inheritance

Private inheritance gives us a whitelist-approach for declaring what functionality is permitted.

Compared to manually wrapping the class, it permits to declare which functions (on a name basis) are permitted without writing a single function, as long as the functionality is unchanged.

Suppose that the class we want to hide is

class big_bad_class {
    // internal details ...
public:
    int foo();
    int foo() const;

    std::string bar();
    std::string bar() && ;

    void baz(int);
    void baz(char);
    void baz(unsigned int);

    // and many others...
};

and we would like to permit only the baz (or bar or foo) functions (all of them):

class smaller_interface: private big_bad_class {
    // ...
    public: using big_bad_class::baz;
};

It’s just one line of code, while manually wrapping would mean writing every overload by hand.

What if we want to change the implementation for one given overload?

In that case, we can still use using, and then implement the function we want to change

class smaller_interface: private big_bad_class {
    // ...
    public: using big_bad_class::baz;
    void baz(int) { * do something special...*/ }
};

or even delete it:

class smaller_interface: private big_bad_class {
    // ...
public:
    using big_bad_class::baz;
    void baz(int) = delete;
};

or manually add the one we want

class smaller_interface: private big_bad_class {
    // ...
public:
    void baz(int i) {
        return big_bad_class::baz(i);
    }
    void baz(unsigned int i) {
        return big_bad_class::baz(i);
    }
    // not providing void baz(char); by design
};

So far, this approach permits to write an expressive whitelist of permitted functionalities through the using keyword.
It also permits to blacklist overloads through =delete or specialize them.

Of course if in our reduced interface we want to add *a lot* of functions from “big_bad_class”, maybe all except a couple, we still need to write *a lot* of using declarations.

But especially because of overloads, it reduces immensely the possibility of errors, and more importantly, duplicated code, even if we need to keep “smaller_interface” synchronized to “big_bad_class”.

But at least errors are a compile-time failure, and it seems to me an acceptable compromise.

Also, because we are using private inheritance, the base class is an implementation detail.

With public and virtual(!) inheritance, it is possible and common to convert the derived class to the base class:

struct base {
    virtual int foo() {
        return 42;
    }
    virtual~base() = default;
};
struct derived: base {
    virtual int foo() override {
        return 0;
    }
};

int bar(base & b) {
    return b.foo();
}

int baz() {
    derived d;
    return bar(d);
}

In this case, derived& d gets implicitly converted to base&.

With public and virtual inheritance it is a sensible approach, because (at least ideally, in practice, like in the Java Collection class hierarchy, this is not always the case) the derived class either implements the given API or extends it.
So using the base class itself or the derived class should generally not alter the correctness of the program.

Note: in case of non-virtual inheritance, the conversion (a static_cast) might not be desired, but as the type system does not take virtual into account, it is not possible to distinguish between those use-cases.

With private inheritance, we are, for example, removing some function from the interface, so we are definitively not extending the base class.

The type system does the correct thing, and converting smaller_interface& to big_bad_class& (or vice-versa) is not possible unless someone writes by hand a conversion function.

Gotchas with virtual functions in the base class

If the base class is defined as

class big_bad_class {
    // internal details ...
public:
    ~big_bad_class() = default;

    virtual void baz(int);
    virtual void baz(char);
    virtual void baz(unsigned int);
    // and a lot of other overloads...

    // and many other functions...
};

then

class smaller_interface: private big_bad_class {
    // ...
    public: using big_bad_class::baz;
    void baz(int) = delete; // the only one we do not want to expose
};

won’t compile, with the following error message

  • GCC: “deleted function ‘virtual void smaller_interface::baz(int)’ overriding non-deleted function”
  • clang “deleted function ‘baz’ cannot override a non-deleted function”
  • msvc: “error C2282: ‘smaller_interface::baz’ cannot override ‘big_bad_class::baz'”, “note: ‘big_bad_class::baz’ is not deleted”

because if the base class defines a virtual function, also the function in the derived class is virtual.
This is also true for private inheritance, even if there seem to be no valid use-cases.

Fortunately, we do not have to give up the using declaration and reimplement all overloads, we can still change the visibility of the function:

class smaller_interface: private big_bad_class {
    // ...
    void baz(int) {
        assert(false && "never called");
    }
public:
    using big_bad_class::baz;
};

What we won’t be able to change is the fact that the exposed baz functions and the destructor of smaller_interface are virtual.

As there is no way in the language to turn virtuality off, the interface shows if it uses private inheritance or wrapping, which is an implementation detail. This is a limitation of this method.

smaller_interface is not necessarily thought to be used for subclassing, but the fact that the exposed internal functions are virtual and the destructor too, might make someone believe it is (even if the virtual keyword might not appear anywhere in the class).

A similar “issue” exists if “big_bad_class” also defines a pure function:

class big_bad_class {
    // ...
public:
    virtual void a_pure_function() = 0;
};

If we do not want to expose it, declaring it private and providing an implementation seems just futile gymnastic.

One needs to take care that the function is really unused with such implementation, as it might be used internally by big_bad_class and called by smaller_interface through an exposed function.

Gotchas with a final big_bad_class

While final seems like a good idea, as with virtual public inheritance is a clear marker when a class should not be subclassed anymore, it also prohibits all other forms of inheritance, like in this case, where inheritance is used as an implementation detail.

If the class is non-virtual, remove the final modifier. If the class is virtual but there is no class hierarchy, then both final and virtual can be removed.

If the class is virtual, and there is a class hierarchy one needs to evaluate the advantages over disadvantages.

I would generally advise (as the subjects are *big* and god-like classes) to remove the final modifier also in this case, because if there is some need to extend the functionality, and making a subclass is not possible, the functionality is added in the class itself (augmenting the entropy of the class).

Unfortunately, there will be use-cases where this is not possible, for example, if the class comes from a third-party library.

Conclusion

Contrary to common advice (for example the isocpp FAQ, or on the Google style guide that even states that “All inheritance should be public”), I believe that private inheritance has valid use-cases for reducing code complexity and duplication.

Ideally, it should not be necessary, as the main use case *big* classes, but it has it’s uses also with smaller interfaces, especially in the presence of overloads, where typos and overlooks are easier to make.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin