Variadic CRTP Packs: From Opt-in Skills to Opt-in Skillsets

Last week we’ve seen the technique of the variadic CRTP, that allowed to plug in generic extra features to a class.

For instance, we’ve seen the following class X :

template<template<typename> typename... Skills> class X : public Skills<X<Skills...>>... { public: void basicMethod(); }; 1 2 3 4 5 6 template < template < typename > typename . . . Skills > class X : public Skills < X < Skills . . . >> . . . { public : void basicMethod ( ) ; } ;

X can accept extra features that plug into its template parameters:

using X12 = X<ExtraFeature1, ExtraFeature2>; 1 using X12 = X < ExtraFeature1 , ExtraFeature2 > ;

To be compatible with X , each of those features follows the CRTP pattern:

template<typename Derived> class ExtraFeature1 { public: void extraMethod1() { auto derived = static_cast<Derived&>(*this); // uses derived.basicMethod() } }; template<typename Derived> class ExtraFeature2 { public: void extraMethod2() { auto derived = static_cast<Derived&>(*this); // uses derived.basicMethod() } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 template < typename Derived > class ExtraFeature1 { public : void extraMethod1 ( ) { auto derived = static_cast < Derived & > ( * this ) ; // uses derived.basicMethod() } } ; template < typename Derived > class ExtraFeature2 { public : void extraMethod2 ( ) { auto derived = static_cast < Derived & > ( * this ) ; // uses derived.basicMethod() } } ;

Since each of these features is a member of the variadic pack of X ‘s template parameters, a natural name for this technique is variadic CRTP.

With it, X can be augmented with the methods coming from the extra features classes:

using X12 = X<ExtraFeature1, ExtraFeature2>; X12 x; x.extraMethod(); x.extraMethod2(); 1 2 3 4 using X12 = X < ExtraFeature1 , ExtraFeature2 > ; X12 x ; x . extraMethod ( ) ; x . extraMethod2 ( ) ;

If you’re not familiar with the variadic CRTP, you can read more about it in its dedicated article. To extend the variadic CRTP, I’d like to focus today on a little addition to this pattern: grouping extra features into packs.

Variadic CRTP packs

If you offer several extra features that can be plugged in your class X , like NamedType does with its skills for instance, it could make sense to bundle them into groups (which NamedType doesn’t at the time of this writing, but it could make sense to refactor it in that way).

Indeed, bundling several related features into groups, or packs, has several advantages:

it is less to learn for a user of X : they can only learn the groups, as opposed to learning every skill,

: they can only learn the groups, as opposed to learning every skill, it makes skills more discoverable: a user can explore groups, which is more logical than exploring the skills by alphabetical order or whatever,

it makes the definition of X more readable: enumerating skillsets is shorter than enumerating skills.

So let’s see how we could bundle several extra features into a pack, and pass that pack to X the same way that we’d pass individual skills.

Inheriting from packs

The trick is not a difficult one: it consists in using intermediary class in the inheritance hierarchy.

Indeed, the point of a feature pack is to behave as if X itself inherited from the features it contains. A natural way to do this is to make the pack inherits from the features, and X inherit from the pack.

To illustrate, let’s consider 4 extra features, and say that the first two relate together, and the last two relate together too. So we’d like to have two packs: a first one with features 1 and 2, and a second one with features 3 and 4.

Without packs, the class hierarchy looks like this:

And by adding packs in:

Now let’s see how to implement such feature packs.

The implementation of a feature pack

We want packs to be CRTP classes (so that X inherits from them) and to inherit from the skill classes. So we have:

template<typename Derived> struct FeaturePack1 : ExtraFeature1<Derived>, ExtraFeature2<Derived> {}; 1 2 template < typename Derived > struct FeaturePack1 : ExtraFeature1 < Derived > , ExtraFeature2 < Derived > { } ;

And:

template<typename Derived> struct FeaturePack2 : ExtraFeature3<Derived>, ExtraFeature4<Derived> {}; 1 2 template < typename Derived > struct FeaturePack2 : ExtraFeature3 < Derived > , ExtraFeature4 < Derived > { } ;

X inherits from them through its template parameters:

using X1234 = X<FeaturePack1, FeaturePack2>; 1 using X1234 = X < FeaturePack1 , FeaturePack2 > ;

Doing this augments X with the methods coming from all four extra features:

X1234 x; x.extraMethod1(); x.extraMethod2(); x.extraMethod3(); x.extraMethod4(); 1 2 3 4 5 6 X1234 x ; x . extraMethod1 ( ) ; x . extraMethod2 ( ) ; x . extraMethod3 ( ) ; x . extraMethod4 ( ) ;

The composite design pattern

An interesting thing to note is that we haven’t changed anything in X to allow for packs to plug in. This means that we can still add individual features to X along with the packs:

using X12345 = X<FeaturePack1, FeaturePack2, ExtraFeature5>; X12345 x; x.extraMethod1(); x.extraMethod2(); x.extraMethod3(); x.extraMethod4(); x.extraMethod5(); 1 2 3 4 5 6 7 8 9 using X12345 = X < FeaturePack1 , FeaturePack2 , ExtraFeature5 > ; X12345 x ; x . extraMethod1 ( ) ; x . extraMethod2 ( ) ; x . extraMethod3 ( ) ; x . extraMethod4 ( ) ; x . extraMethod5 ( ) ;

This looks like the Composite design pattern. Indeed, the Composite design pattern described in the classical GOF book on Design Pattern is about runtime polymorphism with virtual methods, but its spirit is the following: wholes and parts should look alike from the perspective of client code. And this is what the variadic CRTP is allowing here.

But the interest of skillsets does not stop here. One specific use of them allows to reduce the bloat of template symbols, and that’s what we will see in a later post.

You may also like

Share this post! Don't want to miss out ?