Jonathan Boccara's blog

Strong Templates

Published January 9, 2018 - 9 Comments

Strong typing consists in creating a new type that stands for another type and adds meaning through its name. What would it look like to apply this idea to template interfaces?

Disclaimer: What you’ll see in this post is experimental, and it’d be great to have your feedback on it at the end.

Strong types for strong interfaces

We’ve talked a lot about how strong types can help clarify interfaces. Here is a quick example, that you can safely skip if you’re already familiar with strong types.

Consider a case where we want to represent in code the concept of rows and columns.

We could use ints to represent both, but doing this doesn’t carry any information about what those int represents, and that can even get confusing in an interface:

void setPosition(int row, int column);

Indeed, this interface expects a row first and then a column, but you can’t see that at call site:

setPosition(12, 14);

When writing that code, there is a risk of mixing up the row and the column. And when someone reads it, they can’t know whether 12 represents the row, the column, or even something completely unrelated.

Well, in theory, they can. They can go look up the definition of setPosition and check which parameters means what. But we don’t want the people that read our code to go look up the definition of every function we use, do we?

So we can define two dedicated types: Row and Column. Let’s do this by using the NamedType library:

using Row = NamedType<int, struct RowTag>;
using Column = NamedType<int, struct ColumnTag>;

This reads: “Row is like an int, but it’s a different type with a name stuck on it that says it’s a row, and not just any int“. And same for Column.

Using them clarifies the intent of the interface:

void setPosition(Row row, Column column);

which leads to both a more expressive code at call site:

setPosition(Row(12), Column(14));

and more safety against the risk of mixing up the parameters. Indeed, the following would not compile since Row and Column are two different types:

setPosition(Column(14), Row(12)); // compilation error!

This example was a function interface, but this idea can be also applied to template interfaces.

Template interface

By template interface, I mean a template instantiation out of which we can get a result.

Here is a simple one in the standard library since C++11 (but that could be replicated even in C++98):

template< typename Base, typename Derived >
struct is_base_of;

is_base_of “returns” a boolean that indicates whether or not the first template parameter is a base class of the second template parameter.

Such a template interface has several ways of “returning” something that depends on its template parameters. In this particular case it returns a value, and the convention for it is that this value is stored in a static public constant member of the class, called value.

So, if Derived derives from Base then is_base_of<Base, Derived>::value is true. Otherwise, it is false.

And in C++14 appear template variables, which let us store the result into a variable, encapsulating the ::value:

template<typename Base, typename Derived>
constexpr bool is_base_of_v = std::is_base_of<Base, Derived>::value;

(despite being technically doable in C++14, is_base_of_v becomes standard in C++17).

This looks OK. But what if, like it is in reality, our types are not called Base and Derived? What if they’re called A and B (which are not realistic names either, hopefully, but this is to illustrate the case where the name don’t show which is the base and which is the derived)?

is_base_of_v<A, B>

What does the above mean? Should this read “A is the base of B“, or rather “B is the base of A“? I suppose the first one is more likely, but the interface doesn’t express it explicitly.

To quote Andrei Alexandrescu in Modern C++ Design:

Why use SUPERSUBCLASS and not the cuter BASE_OF or INHERITS? For a very practical reason. Initially Loki used INHERITS. But with INHERITS(T, U) it was a constant struggle to say which way the test worked—did it tell whether T inherited U or vice versa?

Let’s try to apply the ideas of strong typing that we saw above to this template interface.

Strong templates

So, just like we had Row(12) and Column(14), the purpose is to have something resembling Base(A) and Derived(B).

Since these are template types, let’s create a template Base and a template Derived, which exist just for the sake of being there and don’t contain anything:

template<typename T>
struct Base{};

template<typename T>
struct Derived{};

We can then use those two templates to wrap the parameters of the is_base_of interface. Just for fun, let’s call that strong_is_base_of:

template<typename, typename>
constexpr bool strong_is_base_of_v;

template<typename base, typename derived>
constexpr bool strong_is_base_of_v<Base<base>, Derived<derived>> = is_base_of_v<base, derived>;

Note that, contrary to the usual strong typing we do on types, we don’t need an equivalent of the .get() method here. This is because templates use pattern matching of types (this is why there is a primary template that is declared but not defined, and a secondary template with a specific pattern containing Base and Derived that is fully defined).

The above uses C++14 template variables (that can be partially specialized).

Here is what it looks like before C++14 without variable templates:

template<typename, typename>
struct strong_is_base_of{};

template<typename base, typename derived>
struct strong_is_base_of<Base<base>, Derived<derived>> : std::is_base_of<base, derived> {};

It is designed along the same lines of the C++14 solution, but uses inheritance of is_base_of to bring in the value member instead of a variable template.

Usage

Let’s now see what this looks like at call site, which was the point of all this implementation!

Let’s use a type A that is the base class of a type B:

class A
{
    // ...
};

class B : public A
{
    // ...
};

Here is how to check that A is indeed a base class of B, as the following compiles:

static_assert( strong_is_base_of_v<Base<A>, Derived<B>>, "A is a base of B");

The point of this is to make it explicit in code that we’re determining whether A is the Base and B is the Derived, and not the opposite.

We now check that B is not a base class of A:

static_assert( !strong_is_base_of_v<Base<B>, Derived<A>>, "B is not the base of A");

And if we accidentally mix up the arguments, by passing in the derived class first:

strong_is_base_of_v<Derived<A>, Base<B>>

It does not compile. What is happening is that this expression calls the primary template of strong_is_base_of_v, that has no definition.

NamedTemplate

In the above code, the two definitions of the Base and Derived templates don’t mention that they exist for the purpose of strong typing:

template<typename T>
struct Base{};

template<typename T>
struct Derived{};

Maybe it’s ok. But if we compare that to the usual definition of a strong type:

using Row = NamedType<int, struct RowTag>;

We see that the latter definition shows that it is a strong type. Can we have a similar definition for a strong template?

To achieve that, we can define a NamedTemplate template;

template<typename T, typename Tag>
class NamedTemplate {};

Which we can use to define our strong templates Base and Derived:

template<typename T>
using Base = NamedTemplate<T, struct BaseTag>;

template<typename T>
using Derived = NamedTemplate<T, struct DerivedTag>;

Which has the advantage of expressing that Base and Derived are “strong templates”, but also has the drawback of adding more code to figure out.

As this technique is experimental, I’m writing it as a basis for discussion rather than a finished product. So if you have an opinion on this, it’s the moment to chip in!

More specifically:

1) Do you think that the concept of strong typing makes sense in a template interface, like it does in a normal interface?

2) What do you think of the resulting code calling the strong is_base_of?

3) Do you think there is a need to express that Base and Derived are strong templates in their definition?

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

Comments are closed