Making Strong Types Implicitly Convertible
Strong types and implicit conversions, doesn’t this sound like incompatible features ?
It can be argued that they are compatible, in fact. We saw why it could be useful to inherit from the underlying type’s features, and if the underlying type is implicitly convertible to something then you might want to inherit that feature too for your strong type.
In fact, NamedType
user Jan Koniarik expressed on Twitter a need for exactly this feature for the NamedType library. I think the need is interesting, and some aspects of the implementation are worth considering too; which is why I’m sharing this with you today.
This article is part of 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
- Converting strong units to one another
- Metaclasses, the Ultimate Answer to Strong Typing in C++?
- Making strong types implicitly convertible
Adding an ImplicitlyConvertibleTo
skill
The functionalities inherited from the underlying type, also named “Skills” in the NamedType library, are grouped into separate classes using the CRTP pattern. For example, to reuse the operator+
of the underlying type the Addable
skill looks like this:
template <typename T> struct Addable : crtp<T, Addable> { T operator+(T const& other) const { return T(this->underlying().get() + other.get()); } };
The crtp
class that this skill inherits from is a helper that gives easy access the underlying of the CRTP, that is the class that inherits from it. If you’re curious about this you can check it all out in the post on the CRTP helper.
If the type T
that NamedType is strengthening is convertible, say to int
, then we can implement a skills that performs an implicit conversion of the strong type to an int
:
template <typename T> struct ImplicitlyConvertibleToInt : crtp<T, ImplicitlyConvertibleToInt> { operator int() const { return this->underlying().get(); } };
Fine. But int
is a very specific case, our type T
could be implicitly convertible to anything. It seems natural to template this class on the destination type of the conversion.
But there is a problem, this class is already a template! How can we template a class that’s already a template?
I suggest you pause for just a moment and try to think about how you would do it.
(🎶 musical interlude 🎶)
Done?
One way to go about this is to wrap this template class into another template class. This comes from a fairly common metaprogramming technique, whose naming convention is to call the inner template class “templ”. Let’s do this:
template <typename Destination> struct ImplicitlyConvertibleTo { template <typename T> struct templ : crtp<T, templ> { operator Destination() const { return this->underlying().get(); } }; };
Since the underlying type can have implicit conversions, I think it’s right to offer the possibility to the strong type to inherit that feature. It’s just a possibility, your strong type doesn’t have to have an ImplicitlyConvertibleTo
skill even if its underlying type supports implicit conversions.
The two directions of implicit conversions
We can now use this skill in our instantiation of NamedType. Let’s test it with a type A that is convertible to B because it implements an implicit conversion operator:
struct B { }; struct A { operator B () const { return B(); } };
Then a strong type over A
could keep this propriety of being convertible to B
:
using StrongA = NamedType<A, struct StrongATag, ImplicitlyConvertibleTo<B>::templ>; B b = strongA; // implicit conversion here
There is another way for A
to be convertible to B
: if B has a constructor taking an A
and that is not explicit
:
struct A { }; struct B { B(A const& a){} };
The same usage of our ImplicitlyConvertibleTo
skill works:
using StrongA = NamedType<A, struct StrongATag, ImplicitlyConvertibleTo<B>::templ>; B b = strongA; // another implicit conversion here
You may have noticed the ::templ
in the client code. This is really annoying, and I must admit I didn’t find a way to make it disappear. I would have loved to rename the real skill something like ImplicitlyConvertibleTo_impl
and declare an alias for the simpler name:
// Imaginary C++ template <typename Destination> using ImplicitlyConvertibleTo = ImplicitlyConvertibleTo_Impl<Destination>::template templ;
But there is no such thing as an alias for template templates in C++. I’m not entirely sure why, but I understand that this feature was considered by the C++ committee, but didn’t make it into the standard (yet?).
So for the moment let’s stick with the trailing ::templ
in client code. If you see how to hide this, please, shout!
Not made for calling functions
At first sight, it seems that this sort of implicit conversion could be used to invoke a function that expects an underlying type by passing it a NamedType
instead. Indeed, we could declare the NamedType
to be implicitly convertible to its underlying type. This way we wouldn’t have to write a call to .get()
every time we pass a NamedType
to a function that existed before it:
using Label = NamedType<std::string, struct LabelTag, ImplicitlyConvertibleTo<std::string>::templ>; std::string toUpperCase(std::string const& s); void display(Label const& label) { std::cout << toUpperCase(label) << '\n'; }
Indeed, without this skill we need to pass the underlying type taken from the NamedType
explicitly:
using Label = NamedType<std::string, struct LabelTag>; std::string toUpperCase(std::string const& s); void display(Label const& label) { std::cout << toUpperCase(label.get()) << '\n'; }
Of course, this stays an opt-in, that is to say you can choose whether or not not activate this conversion feature.
However, while I this implementation can be appropriate for implicit conversions in general, it isn’t the best solution for the case of calling functions on strong types. Indeed, looking back at our implicit conversion skill, its operator was defined like this:
operator Destination() const { return this->underlying().get(); }
In the above example, Destination
is std::string
.
Given that this method returns an object inside the class by value, it creates a copy of it. So if we use this to call function, it means that we’ll pass copies of the underlying value as arguments to the function. This has the drawbacks of potentially making a useless copy, and to prevent the function to bind to an argument (which can be useful – std::back_inserter
does it for instance).
No, ImplicitlyConvertible
works for implicit conversions, but for allowing to call functions we need something different. Something which is detailed in Calling Functions and Methods on Strong Types.
Related articles:
- Strongly typed constructors
- What the Curiously Recurring Template Pattern can bring to your code
- 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++?
Share this post!