Going Far into Polymorphic Helpers
When we saw How to Assign Derived Classes in C++, we came up with a technique involving runtime polymorphism mixed with CRTP.
This allowed derived classes to benefit from a polymorphic assignment operator without implementing it themselves, thus reducing boilerplate code.
But assignment is just a special case of a polymorphic function to implement on a derived class. In this post, we see how to combine multiple polymorphic functions, such as comparing derived objects with operator==
on top of assigning with operator=
.
This is unconventional and experimental, and I would welcome your feedback. It will involve template metaprogramming and if anything, it is a good practice at manipulating C++ to make it do what we want.
But first, let’s recap on where we stand now.
What you need to know
Consider two classes A
and B
that inherit from a base class X
:
class A : public X { // ... }; class B : public X { // ... };
We have some polymorphic code manipulating references to X
, that could be pointing to concrete objects of type A
and B
.
Consider the following assignment:
x1 = x2;
By default, this assigns only the base class part of x2
into x1
, which is probably not what we want. Indeed, the derived part of x1
stays unchanged.
To copy the whole object, we make operator=
virtual
in X
:
class X { public: virtual X& operator=(X const& other) = 0; virtual ~X() = 0; };
And instead of implementing operator=
in each derived class, we implement it once in this intermediate class:
template<typename Derived, typename Base> class VirtualAssignable : Base { public: VirtualAssignable& operator=(Base const& other) override { auto& thisDerived = static_cast<Derived&>(*this); if (auto* otherDerived = dynamic_cast<Derived const*>(&other)) { thisDerived = *otherDerived; } else { // error handling... } return thisDerived; } };
And we make the derived classes derive from this class instead of deriving from X
directly:
class A : public VirtualAssignable<A, X> { // ... }; class B : public VirtualAssignable<B, X> { // ... };
With that, A
and B
get the polymorphic operator=
for free.
There is more to it, and you can read everything in How to Assign Derived Classes in C++, but the above is what you need to know to understand the rest of this article.
Multiple skills
With the above design, the hierarchy on the side of A
looks like this:
Now what if we also want to implement a polymorphic operator==
?
We can adapt the above technique and write this component:
template<typename Derived, typename Base> struct VirtualComparable : Base { bool operator==(real_base const& other) override { auto& thisDerived = static_cast<Derived&>(*this); if (auto* otherDerived = dynamic_cast<Derived const*>(&other)) { return thisDerived == *otherDerived; } else { return false; } } };
But then, how do we use it? If we write this:
class A : public VirtualAssignable<A, X>, public VirtualComparable<A, X> { // ... };
Then the hierarchy would look like this:
And this doesn’t look good. It looks like the dreaded diamond hierarchy, and this is probably not a path we’d like to go down.
How about having a single line of inheritance, like this:
No diamond any more, but it doesn’t work as is. Indeed, consider the code of VirtualComparable
:
template<typename Derived, typename Base> struct VirtualComparable : Base { bool operator==(Base const& other) override { auto& thisDerived = static_cast<Derived&>(*this); if (auto* otherDerived = dynamic_cast<Derived const*>(&other)) { return thisDerived == *otherDerived; } else { return false; } } };
With the single line of inheritance, Base
is VirtualAssignable<A, X>
. Therefore, this is not an overload of the virtual operator==
in the base class:
virtual bool operator==(X& const other);
The one in the base class expects an X
and the one in VirtualComparable
expects a VirtualAssignable<A, X>
.
To make this work, we need to be able at any point in the hierarchy to find the “real base”, here X
.
For the experiment, let’s try and find that real base.
Finding the real base
Here is a possible algorithm to determine the real base: if the base class has a real_base
alias, then this alias is the real base and we declare it as an alias too. And if the base class doesn’t have a real_base
alias, we consider the base class to be the real_base
.
A limitation of this algorithm is the case where X
has a real_base
alias. This is not very common, but we could come up with an even less likely name to mitigate that risk. Let’s stick with real_base
here.
Here is what we need then:
- determine if the base class has a
real_base
alias - get the real base
- use it in the class
Determining if the base class has a real_base
alias
Determine if the base class Base
has an alias real_base
is equivalent to determining if typename Base::real_base
is a valid expression. And to do that we can use the detection idiom.
We examined in detail the detection idiom in Expressive C++ Template Metaprogramming. Some compilers offer it with std::experimental::is_detected
, but here is some code to emulate it:
template<typename...> using try_to_instantiate = void; using disregard_this = void; template<template<typename...> class Expression, typename Attempt, typename... Ts> struct is_detected_impl : std::false_type{}; template<template<typename...> class Expression, typename... Ts> struct is_detected_impl<Expression, try_to_instantiate<Expression<Ts...>>, Ts...> : std::true_type{}; template<template<typename...> class Expression, typename... Ts> constexpr bool is_detected = is_detected_impl<Expression, disregard_this, Ts...>::value;
We can use it with an expression we want to determine the validity for a given type. Here we use this:
template<typename T> using real_base_alias_expression = typename T::real_base;
And we make the validity check this way:
template<typename T> constexpr bool has_real_base = is_detected<real_base_alias_expression, T>;
Getting the real base
Now that we can determine if a base class has the real_base
alias, we can apply our algorithm: if it has a real_base
use it, otherwise the real base if the type itself.
An interesting note is that we can’t use std::conditional
for this. Indeed, we would use it this way:
template<typename T> using real_base = std::conditional_t<has_real_base<T>, typename T::real_base, T>;
But this doesn’t compile as it would instantiate T::real_base
before testing the value of has_real_base
. And in the case it is false
, typename T::real_base
is by definition invalid code, and it does not compile.
Instead we have to go back to the good old template specialisation on booleans:
template<typename T, bool has_real_base> struct get_real_base_impl { using type = typename T::real_base; }; template<typename T> struct get_real_base_impl<T, false> { using type = T; }; template<typename T> using get_real_base = typename get_real_base_impl<T, has_real_base<T>>::type;
Using the real base
Now that we have code to determine the real base, we can use it in our virtual skills classes:
template<typename Derived, typename Base> struct VirtualAssignable : Base { using real_base = get_real_base<Base>; VirtualAssignable& operator=(real_base const& other) override { auto& thisDerived = static_cast<Derived&>(*this); if (auto* otherDerived = dynamic_cast<Derived const*>(&other)) { thisDerived = *otherDerived; } return thisDerived; } }; template<typename Derived, typename Base> struct VirtualComparable : Base { using real_base = get_real_base<Base>; bool operator==(real_base const& other) override { auto& thisDerived = static_cast<Derived&>(*this); if (auto* otherDerived = dynamic_cast<Derived const*>(&other)) { return thisDerived == *otherDerived; } else { return false; } } };
We can now define derived classes using multiple polymorphic skills:
class A : public VirtualAssignable<A, VirtualComparable<A, X>> { // ... };
Factoring skills
The above definition compiles and does what we expect, but it has several drawbacks:
- this looks more complicated than normal inheritance,
- it repeats
A
several times, - the list of skills (
VirtualAssignable
,VirtualComparable
) are not together, - if we add more skills, including custom ones, the above problems get worse.
The fact that it looks more complicated than normal inheritance is the price to pay to get polymorphic code for free in the derived classes. If we go down the path of inserting intermediary classes, we get funny looking first lines of class definitions.
But we can do something about the repetition of A
and the fact that the skills are not located together in the inheritance expression. Instead of using types, we can use templates as template parameters, and write something like this:
struct A : InheritWith<A, X, VirtualAssignable, VirtualComparable> { // ... };
(if you see better names than those, I’ll be glad to hear your ideas in the comments section.)
InheritWith
doesn’t exist, let’s write it.
But before doing that, is this a good idea? We’re solving the above problems but we’re introducing yet an another non-standard component. It’s a trade-off and you can decide if it is worth it.
What’s sure though, InheritWith
is interesting to implement, and good exercise for our C++ muscles. So let’s go ahead and make the above code work.
InheritWith
As stated at the beginning of the article we don’t want multiple inheritance here, but a single line of classes that end up inheriting from X
. This line must contain VirtualAssignable
, VirtualComparable
, and potentially other custom skills.
A natural way to define the template parameters of InheritWith
is this:
template<typename Derived, typename Base, template<typename, typename> class... VirtualSkills> struct InheritWith
The first template parameter is the derived class (in our case A
), the second one is the base class at the top (in our case X
), and then there is a variadic pack of template skills (which reminds a bit of strong type skills).
The only way I see to implement InheritWith
is to use recursion (even though it is generally not the best practice for variadic templates–if you see another way, please let me know in the comments section!).
The base case is when there is only one skill:
template<typename Derived, typename Base, template<typename, typename> class VirtualSkill> struct InheritWith<Derived, Base, VirtualSkill> : VirtualSkill<Derived, Base> {};
This corresponds to the following hierarchy, with A
, X
and only VirtualAssignable
:
Let’s now write the general case of the recursion:
template<typename Derived, typename Base, template<typename, typename> class VirtualSkill, template<typename, typename> class... VirtualSkills> struct InheritWith : VirtualSkill<Derived, InheritWith<Derived, Base, VirtualSkills...>> {};
This corresponds to the following hierarchy with A
, X
, VirtualAssignable
and VirtualComparable
:
What’s your take on this?
Part of what I do on Fluent C++ is to diffuse best practices of coding, and part is to experiment new things. This falls under the second category, and your feedback is very valuable.
What do you think of all this?
Did you also encounter the need for factoring polymorphic functions such as assignment and comparison?
Is the whole package including InheritWith
too much? At which point did it become too much?
How would you solve the same need differently?
You will also like
- How to Assign Derived Classes in C++
- Strong types: inheriting the underlying type’s functionalities
- Variadic CRTP: An Opt-in for Class Features, at Compile Time
- How to Reduce the Code Bloat of a Variadic CRTP
Share this post!