Strong Templates
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 int
s 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?
Share this post!