Table of Contents
Introduction
Multiple inheritance being the base of all evils, I thought it would be a brilliant idea to try and do something similar statically. Turns out, you can make something that looks like multiple inheritance (but isn’t really).
Using a theoretical use case of some digital signal processor, it looks something like this:
struct processor : inherits<source, sink, publisher>
{
};
The general idea here is that we can use templates to create a linear inheritance hierarchy, with each “base” is agnostic to what it derives from. We can then extend this with a CRTP like technique to make each base aware of the complete class (so they can call methods from others), add constructors, and use concepts to provide constraints similar to what might be found with pure-virtual methods.
A Basic Implementation
Base Requirements
To start with, the magic starts with a class “inherits
” that accepts
a template argument for each base to add to the inheritance chain.
It follows that each of these bases must themselves take a template parameter for the next base to derive from:
template <typename NextBase>
struct source : NextBase {
// Exposition only
auto get_data() -> std::vector<std::uint8_t>;
};
template <typename NextBase>
struct sink : NextBase {
// Exposition only
auto push_data(std::vector<std::uint8_t> const &) -> void;
};
template <typename NextBase>
struct publisher : NextBase {
// Exposition only
auto push_event(std::vector<std::uint8_t> const &) -> void;
};
We can define a base class with this, but there is a problem:
struct processor : source<sink<publisher<???>>>
If every intermediate base is agnostic to the next, we have to terminate the inheritance chain. This can be solved by providing an “empty” class:
struct Empty {};
struct processor : source<sink<publisher<Empty>>>
In effect, we need to support for currying template parameters.
In programming, currying is taking a function that accepts multiple parameters and converting it into a sequence of functions that accept a single argument. From the wikipedia page example:
x = f(a, b, c) becomes: h = g(a) i = h(b) x = i(c)
Or alternatively expressed by:
x = g(a)(b)(c)
Unpacking/Currying
We can now revisit the inherits
class. We are expecting it to accept a list
of template parameters, which are classes that themselves accepts a template
parameter. This means we have to use, rather unfortunately, [template-template]
parameters. The signature would look like this:
template <template <typename> typename ... Bases>
struct inherits_impl;
I’m using the
impl
suffix for a reason, I’ll get to that later.
We need to recursively unpack the base list and apply them. I’m struggling a little bit here with how to explain this. I’m reminded of the apocryphal quotes by Richard Feynman and Albert Einsteing aling the lines of: “If you cannot explain something in simple terms, you don’t understand it”, but such is C++…
template <
template <typename> typename Current,
template <typename> typename ... Bases>
struct inherits_impl<Current, Bases...> {
using type = Current<???>;
};
template <
template <typename> typename LastBase>
struct inherits_impl<Current, Bases...> {
using type = LastBase<Empty>;
};
The first struct
template specialisation “captures” the first template
parameter and defines a type alias “type
” to it. This would then have the
remaining bases applied to it as type parameter, using the same inherits_impl
class.
The second is specifically applied when the last base is reached, which then applies the dummy class to terminate the inheritance chain, as explained before.
I’ve written the template parameters to the current base type as question marks because it would otherwise invoke typical C++ hieroglyphics, which can be simplified.
For any nested type declaration, since the compiler has no way of distinguising them from a static variable or member, we have to tell the compiler that it is, in fact, a type. For exposition, this type would have to be accessed as follows:
using base = typename inherits_impl<source, sink, publisher>::type;
However, we can use a templated type alias to simplify this:
template <template <typename> typename ... Bases>
using inherits = typename inherits_impl<Bases...>::type;
Going back to the specialisation, we can now write the following:
template <
template <typename> typename Current,
template <typename> typename ... Bases>
struct inherits_impl<Current, Bases...> {
using type = Current<inherits<Bases...>>;
};
Everytime we write inherits<Bases...>
, it will yield a type and is called
recursively. The following is an attempt at some terrible template psuedocode of
what is going on:
inherits<source, sink, publisher>
= source<inherits<sink, publisher>> = source<sink<publisher<Empty>>>
sink<inherits<publisher>> = sink<publisher<Empty>>
publisher<Empty> = publisher<Empty>
Empty = Empty
The following expression should now be valid:
struct processor : inherits<source, sink, publisher>
{
};
Extending Further
Adding Constructors
Constructors can be added to each mixin, but it does increase complexity somewhat.
- If a mixin has a no-argument constructor, it must call the derived from constructor (as would hopefully be obvious);
- Even if only one mixin’s constructor accepts an argument, all of the constructors in the chain need to be aware of it, currying the arguments along the way.
For arguments sake, say that our source mixin doesn’t require any arguments at all (perhaps this is a contract for the most final type). We need to capture all of the possible arguments, and forward them to the next base in the chain.
This is easily achieved with forwarding references:
template <typeame NextBase>
struct source : NextBase
{
template <typename ... Args>
source(Args && ... args)
: NextBase{std::forward<Args>(args)...}
{}
};
The constructor accepts 0 or more arguments, which can have any value category.
std::forward
preserves the value category as it’s passed along, so that, for example, a referenced argument doesn’t become a value type by copy etc.
However, our theoretical publisher mixin pushes events to some broker, for which an address is needed. Hopefully, after following the previous example, this should be kind of obvious: we are “stealing” the first argument and passing the rest along.
template <typename NextBase>
struct publisher : NextBase
{
template <typename ... Args>
publisher(std::string broker_address, Args && ... remaining_args)
: NextBase{std::forward<Args>(remaining_args)...}
, m_broker{broker_address}
{
}
std::string m_broker;
}
The good news is, that for mixins that don’t need a custom constructor, we can merely inherit the next base’s.
template <typename NextBase>
struct source : NextBase
{
using NextBase::NextBase;
};
Finally, when the final class is defined, it is useful to declare a type alias
to the complete inheritance chain, otherwise we would have to write
inherits<source, sink, publisher>
in the base class declaration and, the base
class constructor invocation. This is not necessary, but it’s less error-prone.
/* Define a type alias so we don't have to repeat the inherits type in the
base constructor call */
using plugin_base = inherits<source, publisher, sink>;
struct my_plugin : plugin_base
{
my_plugin(std::string broker_address)
: plugin_base{broker_address}
{}
};
Becoming Self-Aware Using CRTP
It is useful for a class to understand what it’s final form, and act upon functions and members defined in other places. This will become important later, but for now, assume a mixin may wish to use a “plugin id” static variable defined by the final (or most derived) class.
This is already achievable in a static context via CRTP:
template <typename Impl>
struct logger
{
auto info(std::string const & message) -> void {
std::cout << Impl::plugin_id << ": " << message << "\n";
}
};
struct my_plugin : logger<my_plugin>
{
constexpr static auto id = "plugin"sv;
};
We can add this to our “framework”, giving every mixin the ability access members of not only the most-derived type, but every mixin in the chain. This is done by supplying an extra template parameter representing the final type, which is passed into all the bases – just like CRTP. The standard caveat with CRTP being that elements within the most derived can only be used in scopes where the class is complete, e.g. functions.
This makes everything quite a bit more complicated, but the takeaways are:
- Each mixin must accept an additional template parameter defining the
final implementation (
typename Impl
), in addition to it’s next base; - The
inherits_impl
andinherits
template must also accept the same template parameter; - The
inherits_impl
base/mixin parameter pack used to supply mixins must now accept mixin types which have two template arguments (i.e.,template <typename, typename>
) - The
Impl
parameter is now passed as a template argument when each individual mixin is declared (using type = CurrentBase<Impl, ... >;
) - And the final most derived class (our plugin) must pass itself as a type
parameter to the
inherits
expression, just like CRTP.
struct empty {};
/* Our first argument _will be_ the "complete" type. The remaining is the
the list of bases, and since these has to have the implementation injected,
as well as the calculated next base, it now takes two template arguments */
template <typename Impl, template <typename, typename> typename ... CurrentBase>
struct inherits_impl;
/* Helper, rewritten */
template <typename Impl, template <typename, typename> typename ... Bases>
using inherits = typename inherits_impl<Impl, Bases...>::type;
/* Modified as above, both the current and next bases now have two template
arguments. */
template <
typename Impl,
template <typename, typename> typename CurrentBase,
template <typename, typename> typename ... Bases>
struct inherits_impl<Impl, CurrentBase, Bases...>
{
using type = CurrentBase<Impl, inherits<Impl, Bases...>>;
};
template <
typename Impl,
template <typename, typename> typename CurrentBase>
struct inherits_impl<Impl, CurrentBase>
{
using type = CurrentBase<Impl, empty>;
};
/* Note the extra Impl template parameter on each of these classes */
template <typename Impl, typename NextBase>
struct source : NextBase {
auto get_data() -> std::vector<std::uint8_t>;
};
template <typename Impl, typename NextBase>
struct sink : NextBase {
auto push_data(std::vector<std::uint8_t> const &) -> void;
};
template <typename Impl, typename NextBase>
struct publisher : NextBase {
auto push_event(std::vector<std::uint8_t> const &) -> void;
};
And the consequent expression would look like this:
struct processor = inherits<processor, source, publisher, sink>
{};
You could of course write some more template machinery to make this a little bit more visually appealing; having a sequence of arguments in a row with all but one being treated similarly being potentially confusing e.g.,
struct processor : module<processor>::inherits<source, publisher, sink>
{}
(Although this might cause more problems when using with constructors because of requiring forward declarations of things and dependent names)
Using Concepts to Provide Virtual-like Behaviour
In the future (now if you’re really keeping up with standards), you can use concepts to enforce constraints, for example, creating an error if a member doesn’t exist, is not a specific or is missing a function with a specific signature. Now you might suggest that you could do this with type traits, SFINAE et al., but to be honest:
- It’s way too much boilerplate for my liking
- The function detection (with signature) code I saw I didn’t actually understand…
Concepts and constraints specify requirements (restrictions) on template arguments. This subject is quite broad, and I couldn’t hope to get into it all, but the below example (I think) is fairly simple and satisfies our… ahem… requirements for enforcing classes to do stuff.
The relevant thing here, is that we can create a concept that will cause a compilation error if our complete class does not have a specific function with specific arguments and return type, but only if we put things in the right place.
In this example, we’re saying that when a class derives from source
(and
likewise inherits<source,...>
), that they must implement a the get_data()
function, with the specified signature.
/* The requires keyword in this context introduces a set of expressions which
are used to test the validity of specific operations. The names/types inside
parantheses declare hypothetical instances of things we would need to form
an expression, and is optional depending on the expression under test. */
template <typename Impl>
concept Source = requires (Impl instance) /* Parameter list */
{
/* Compound requirement, tests a function and its return type. The
implementation of `std::same_as` is defined like a type trait with two
template arguments, but the first is automatically "consumed" by the
return type of the expression. */
{ instance.get_data() } -> std::same_as<std::vector<std::uint8_t>>;
};
template <typename Impl, typename NextBase>
struct source : NextBase
{
source() {
/* Since a constraint evaluates to a boolean, we can use it in a static
assertion, which we have to do in a function because of CRTP rules.
*/
static_assert(Source<Impl>, "Must implement Processor concept");
}
};
Disadvantages & Conclusion
So far, I have demonstrated that you can at least simulate something that looks like multiple inheritance but in a static context, can curry constructor arguments, demonstrates chained CRTP, and finally, enforcement of pure-virtual like behaviour with concepts.
Really, a big problem with this pattern is that it lends itself to (eventually) large classes, which to some is anathema. I can’t even think of a scenario in the virtual world where I required an inheritance chain that was larger than two or a similarly large number of static “mixins”. I also really struggled coming up with a compelling example, but here we are. In any case, I’m not condoning it; this is more about the stereotypical “look at me I can do templates” blah blah blah… (and which I’m almost certain I’ve made mistakes here!)
Then there is visibility between the intermediate classes. It’s not so much that they, in basic examples, can only “talk up” to their respective bases; this would be the case in a “normal” inheritance hierarchy. It’s more to do with the fact that injecting types with templates at the most derived class means intermediate mixins have no idea who they’re with until the class is complete. Sure, CRTP helps, but no code completion or linting here, you’re on your own. Of course, this applies to the bases, and not the most derived.
Then there is the matter of writing the mixins themselves. To add to the issue
of having to write them without any help, it can become complicated quite
quickly, for instance, remembering whether you have to write a forwarding
constructor when the mixin accepts a constructor argument, or whether you can
just inherit the next base. And sure, writing constraints and concepts is cool,
but, it’s not supported on many compilers, and writing them requires yet another
level of C++ wizadry on top of the large amount that already exists. I found
while prototyping I was making silly mistakes, like expecting to use type traits
in a requires-expression without using a nested requires-clause etc. Being new
to concepts, i’m sure it’s just growing pains. But could they have named the
requires
keyword constrains
or constrained_by
or something? I suppose
they could have finally found a use for the restrict
keyword… (hahaha just
kidding I know how ridiculous that would be).
In the end, it was a bit of fun, but I’m not sure I can recommend it!
Footnotes
Diamonds are Forever
I had originally written this inline, but realised it didn’t fit with the rest of the article. I thought it was funny though, and didn’t want to waste it, so here it is.
The diamond problem is where a derived class inherits from multiple base classes that themselves inherit from a shared base.
It’s a stupid example, but, our “Catdog” will more than likely inherit a
meow()
function, as well as bark()
. So far, so unrelated. But suppose our
Animal
base class wanted an intermediate base to implement a play()
function. Each of our intermediates are highly likely to implement one. dogs
usually like to play fetch and hopelessly chase rabbits, whereas a cat is likely
to find the rabbit, destroy it, bring it back into the house and wantonly knock
over anything that got in it’s way in the process, only to finally sit down
somewhere inconvenient and stare at you until it gets bored. You’re not playing
with the cat.
They’re playing with you.
The point of this is, with the diamond problem, when you call play()
you can’t
know whether or not you’re going to the park to play ball, or, cleaning the
house for the next 2 hours.