Passing strong types by reference
On Fluent C++ we had already considered passing strong types by references, and realized that this wasn’t such a simple thing to do. To understand why, I suggest you read the problem statement in this previous post before starting this one, so that we are in line.
So far the series on strong types contains the following articles:
- 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
- Converting strong units to one another
- Metaclasses, the Ultimate Answer to Strong Typing in C++?
- Making strong types implicitly convertible
In our previous attempt, we came up with this class that would be used exclusively to create strong types representing strongly typed references of primitive types:
template<typename T, typename Parameter> class NamedTypeRef { public: explicit NamedTypeRef(T& t) : t_(std::ref(t)){} T& get() {return t_.get();} T const& get() const {return t_.get();} private: std::reference_wrapper<T> t_; };
that could be instantiated the following way:
using FirstNameRef = NamedTypeRef<std::string, struct FirstNameRefParameter>;
This works fine. But has the unpleasant disadvantage of creating a new component, different from the central one we made for representing strong types in C++: NamedType.
After I presented this work to various people, I got feedback and suggestions that steered me into another direction. The result is that we can actually represent references, strongly types, by using the NamedType class itself. Let me show you how.
Strengthening a reference
A very simple way to represent a reference strongly type is to take the NamedType wrapper, made for adding strong typing over any type, and use it on a type that is itself a reference:
using FirstNameRef = NamedType<std::string&, struct FirstNameRefParameter>;
Simple, right?
Except this doesn’t compile.
Incompatibility with NamedType
The compilation error comes from the constructors in NamedType. Here is the NamedType class:
template <typename T, typename Parameter> class NamedType { public: explicit NamedType(T const& value) : value_(value) {} explicit NamedType(T&& value) : value_(value) {} T& get() { return value_; } T const& get() const {return value_; } private: T value_; };
When T is a reference, say it is U&, then reference collapsing does the following when instantiating the template:
- In the first constructor,
T const&
becomesU& const&
which collapses intoU&
,
- In the second constructor,
T&&
becomesU& &&
which collapses intoU&
.
If you are unfamiliar with reference collapsing, the last part of this excellent talk from Scott Meyers tells you everything you need to know to understand the above two lines.
Anyway, the bottom line is that the two resulting constructors take a U const&
and a U&
respectively, which is ambiguous, and the code won’t compile.
Making it compatible
A simple idea to make it compatible with NamedType is to remove the constructor by r-value reference if T is a reference. It would not make much sense to move a reference anyway, so this constructor is not needed in this case.
This can be achieved by using template meta-programming, and SFINAE in particular. I’m going to show you one way to do it, and then explain how that works, if you have an interest in understanding that bit. But it is important to realize that this can be considered as an implementation detail, because a user of NamedType can just instantiate its type with the above syntax and not worry about this removed constructor.
So here it is:
template<typename T_ = T> explicit NamedType(T&& value, typename std::enable_if<!std::is_reference<T_>{}, std::nullptr_t>::type = nullptr) : value_(std::move(value)) {}
The central piece in this construction is std::enable_if
that aims at “enabling” some code (in this case the constructor) only when a certain condition is true, if this condition is verifiable at compile type. And checking whether T is a reference can be checked at compile time. If this condition doesn’t hold then enable_if
fails in its template substition. And as SFINAE would have it, Substitution Failure Is Not An Error. So the code compiles and the constructor just goes away.
One particular thing is that there has to be a substition, meaning there must be a template parameter. And from the perspective of the constructor, T is not a template parameter, because to instantiate the constructor T is already known. This is why we artificially create a new template parameter, T_, which is actually the same as T.
If you don’t fully understand the previous two paragraphs, or can’t be bothered to dig, it’s all right. The thing to remember is that you can just use the following class and wrap it around references:
template <typename T, typename Parameter> class NamedType { public: explicit NamedType(T const& value) : value_(value) {} template<typename T_ = T> explicit NamedType(T&& value, typename std::enable_if<!std::is_reference<T_>{}, std::nullptr_t>::type = nullptr) : value_(std::move(value)) {} T& get() { return value_; } T const& get() const {return value_; } private: T value_; };
All the code is on a GitHub repository if you want to play around with it. And more posts are to come, to describe new functionalities that turn out to be very useful to add to strong type.
This series is definitely not over.
Related articles:
- Strongly typed constructors
- Strong types for strong interfaces
- 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!