Strong types for strong interfaces
Strong types are a popular topic in the C++ community. In this post I want to focus specifically on how they can be used to make interfaces clearer and more robust.
This post in the second one in the series on strong types:
- Strongly typed constructors
- Strong types for strong interfaces
- Passing strong types by reference
- Strong lambdas: strong typing over generic types
- Good news: strong types are (mostly) free in C++
- Inheriting functionalities from the underlying type
- Making strong types hashable
- Strong Types For Strong Interfaces: my talk at Meeting C++
- Converting strong units to one another
- Metaclasses, the Ultimate Answer to Strong Typing in C++?
- Calling functions and methods on strong types
- Using Strong Types to Return Multiple Values
- Making strong types implicitly convertible
- Strong templates
- Strong optionals
Motivation
First of all, what is a strong type? A strong type is a type used in place of another type to carry specific meaning through its name. As opposed to strong types would be general-use types, like native types such as ints and double for example. Often, native types don’t tell much about the meaning of their instances.
To illustrate this, let’s take the example of a class modelling a Rectangle. Say that a Rectangle can be initialized with a width and a height. To write this as an interface, the first idea that comes to mind is to use doubles:
class Rectangle { public: Rectangle(double width, double height); .... };
doubles are a fairly generic type, so as per our above definition they don’t constitute a strong type here. But from anything we can see in this piece of code, we have to say that there seems to be nothing wrong with it.
The problem with too generic types appears at call site, when calling the above interface:
Rectangle r(10, 12);
For a reader of this call to the constructor, there is absolutely no indication which one of 10 or 12 is the width or the height. This forces the reader to go check the interface of the Rectangle class, that is presumably located away in another file. For this reason, the usage of too generic types is detrimental for readability, and for no good reason: the code knows very well that 10 is the width and 12 is the height; it just won’t say it to you.
Additonally, there is another issue with this Rectangle interface using doubles: nothing prevents the caller from passing the parameters in the wrong order. For example, the following will compile:
Rectangle r(12, 10); // oops, meant to set 10 as width, but mixed up the arguments
Making strong types
To solve this obfuscation of the code, one solution is to show the meaning of the parameters, at call site.
This is what strong types do. In the first article of this series, we encountered the need to write out a name over some parts of an interface, in the particular case of constructors. And to do this, we built a thin wrapper around the native type, for the sole purpose of giving it a specific name. To show that a particular double was meant to represent a Radius, we wrote the following wrapper:
class Radius { public: explicit Radius(double value) : value_(value) {} double get() const { return value_; } private: double value_; };
Now it clearly appears that there is nothing specific to doubles or radii in this idea. It is therefore natural to write a generic component that would do the wrapping of a given type T. Let’s call this component NamedType:
template <typename T> class NamedType { public: explicit NamedType(T const& value) : value_(value) {} explicit NamedType(T&& value) : value_(std::move(value)) {} T& get() { return value_; } T const& get() const {return value_; } private: T value_; };
(this is not the final implementation – see bottom of this post)
The occurrences of doubles have been basically replaced by the generic type T. Except for passing and returning the value, because even though doubles are passed by value, in the general case for a type T passing parameters to a method is done by reference-to-const.
There are several approaches to instantiate a particular named type, but I find the following one quite unambiguous:
using Width = NamedType<double>;
Some implementations use inheritance, but I find the above is more expressive because it shows that we conceptually just want a type with a label put on it.
Using phantoms to be stronger
If you think about it, the above implementation is in fact not generic at all. Indeed, if you wanted to have a specific type for representing Height, how would you go about it? If you did the following:
using Height = NamedType<double>;
we would be back to square one: Width and Height would only be 2 aliases for NamedType<double>, thus making them interchangeable. Which defeats the point of all this.
To solve this issue, we can add a parameter, that would be specific for each named type. So one parameter for Width, another one for Height, etc.
Said differently, we want to parametrize the type NamedType. And in C++, parameterizing types is done by passing template parameters:
template <typename T, typename Parameter> class NamedType { ....
Actually the Parameter type is not used in the implementation the class NamedType. This is why it is called a Phantom type.
Here we want a template parameter for each instantiation of NamedType that would be unique across the whole program. This can be achieved by defining a dedicated type each time. Since this dedicated type is created for the sole purpose of being passed as a template parameter, it does not need any behaviour or data. Let’s call it WidthParameter for the instantiation of Width:
struct WidthParameter {}; using Width = NamedType<double, WidthParameter>;
In fact, WidthParameter can be declared within the using statement, making it possible to instantiate strong types in just one line of code:
using Width = NamedType<double, struct WidthParameter>;
And for Height:
using Height = NamedType<double, struct HeightParameter>;
Now Width and Height have explicit names, and are really 2 different types.
The Rectangle interface can the be rewritten:
class Rectangle { public: Rectangle(Width, Height); .... };
Note that the parameter names are no longer needed, because the types already provide all the information.
And at call site, you have to state what you’re doing:
Rectangle r(Width(10), Height(12));
Otherwise the code won’t compile.
Strong types and user-defined literals
This plays well with user defined literals and units. To illustrate this, let’s add a unit for expressing lengths in meters. A meter is just a numeric value with a specific meaning, which is exactly what NamedType represents:
using Meter = NamedType<double, struct MeterParameter>;
NamedTypes can be combined, and width and height can take a unit this way:
using Width = NamedType<Meter, struct WidthParameter>; using Height = NamedType<Meter, struct HeightParameter>;
If we add a user-defined litteral for meter:
Meter operator"" _meter(unsigned long long length) { return Meter(length); }
(to cover floating point literals, another overload should be also added for long double)
then we get a code at call site that is quite pretty:
Rectangle r(Width(10_meter), Height(12_meter));
Conclusion & to go further
Strong types reinforce interfaces by making them more expressive, especially at call site, and less error-prone by forcing the right order of arguments. They can be implemented by the following thin wrapper:
template <typename T, typename Parameter> class NamedType { public: explicit NamedType(T const& value) : value_(value) {} explicit NamedType(T&& value) : value_(std::move(value)) {} T& get() { return value_; } T const& get() const {return value_; } private: T value_; };
that can be used the following way:
using Width = NamedType<double, struct WidthParameter>;
To go deeper in this useful and popular topic, you can explore the following aspects:
- enforcing business rules with strong types on Simplify C++!
- providing more functionality to strong types in a modular way on foonathan::blog()
On my side I will cover the passage of strong types by reference. Indeed, all of the above implementations perform copies of the underlying types each time they are passed to an interface, but in some cases this is not what you want. I haven’t seen this aspect of strong types treated anywhere yet, so it will be the focus of the following post in our series on strong types.
Related articles:
- Strongly typed constructors
- Passing strong types by reference
- Strong lambdas: strong typing over generic types
- Good news: strong types are (mostly) free in C++
- Inheriting functionalities from the underlying type
- Making strong types hashable
- Converting strong units to one another
- Metaclasses, the Ultimate Answer to Strong Typing in C++?
- Making strong types implicitly convertible
Share this post!