Calling Functions and Methods on Strong Types
Strong types are a way to put names over data in code in order to clarify your intentions, and the more I work on it the more I realize how deep a topic that is.
So far we’ve seen the following subjects in our 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++?
- Calling functions and methods on strong types
- Making strong types implicitly convertible
For a general description of strong typing and a way to implement it with NamedType
, Strong Types for Strong Interfaces is a good place to start if you’re joining in the series now.
We had started to tackle some aspects of how to inherit some functionalities from he underlying type and why this can be useful. For example we’ve seen ways to reuse operators, and how to reuse hashing from the underlying type.
Now let’s go further into that direction, by addressing the following question: how can we call on a strong type functions and methods that are related to the underlying type?
Motivation: calling functions and methods
Several people have asked asked me this question: shouldn’t a strong type be implicitly convertible to its underlying type, instead of forcing a user to call .get()
each time they want to retrieve the underlying value?
For instance, consider the following code:
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'; }
Note that we need to call .get()
to be able to pass the strongly typed label to the function expecting its underlying type, std::string
.
If we had an imaginary NamedType skill called FunctionCallable
, wouldn’t it be nicer to be able to use the label directly with the toUpperCase
function:
using Label = NamedType<std::string, struct LabelTag, FunctionCallable>; std::string toUpperCase(std::string const& s); void display(Label const& label) { std::cout << toUpperCase(label) << '\n'; }
Ok, you may say meh. But now imagine that, instead of one usage of a label like in the above snippet, we had a piece of code that contained 50 of them. Would it be nice to see that many .get()
all over the place?
I don’t say it’s bad, but it is at least worth considering. And even more so if those 50 usages of labels where already there in code, and we had to go over them all and litter our existing code with .get()
calls.
Well, we could add an operator*
that does the same thing as the .get()
method, with arguably less visual noise. But what if it was 500 and not 50? It would still be annoying to make that change, wouldn’t it?
Second, consider calling methods on a strong type, that come from its underlying type. To carry on with the label example, suppose we’d like to use the append
method of the underlying string class to add new characters:
using Label = NamedType<std::string, struct LabelTag>; Label label("So long,"); label.get().append(" and thanks for all the fish.");
Wouldn’t it be nicer to be able to call the append
method directly on label
while keeping it more strongly typed than a std::string
, if we had an imaginary skill called MethodCallable
?
using Label = NamedType<std::string, struct LabelTag, MethodCallable>; Label label("So long,"); label.append(" and thanks for all the fish.");
(Disclaimer: in this post we won’t write it with this exact syntax. We’ll use operator->
instead.)
Wouldn’t that kill the purpose of strong typing?
Not entirely.
Even though the purpose of strong types is being a different type from the underlying type, allowing an implicit conversion from the strong type to the underlying type doesn’t means the two types become completely equivalent.
For instance, consider a function taking a Label
as a parameter. Even if Label
is implicitly convertible to std::string
, the conversion doesn’t go the other way. Which means that such a function would not accept an std::string
or another strong type over std::string
than Label
.
Also, if the strong type is used in a context, for instance std::vector<Label>
, there is no conversion from or to std::vector<std::string>
. So the strong type stays different from the underlying type. A little less different though. So it would be the decision of the maintainer of the Label
type to decide whether or not to opt in for that conversion feature.
Let’s implement FunctionCallable
, MethodCallable
and, while we’re at it, Callable
that allows to do both types of calls.
If you directly want the final code, here is the GitHub repo for NamedType.
Calling functions on strong types
While we will see the general case of reusing the implicit conversions of the underlying type in a dedicated post, here we focus on the particular case of doing an implicit conversion of a NamedType
into its underlying type, for the purpose of passing it to a function.
In general, an implicit conversion typically instantiates a new object of the destination type:
class A { ... operator B() const // this method instantiates a new object of type B { ... } };
Here we need to get the object inside the NamedType
in order to pass it to a function. The object itself, not a copy of it. If the function takes its parameter by value an makes a copy of it, then good for that function, but at least we will present it the underlying object itself and not a copy of it.
So we need our conversion operator to return a reference to T
:
operator T&() { return get(); }
And similarly, if the NamedType
object is const
then we need a const reference to the underlying object inside:
operator T const&() const { return get(); }
Now to make this an opt-in so that a user of NamedType
can choose whether or not to activate this feature, let’s package those two implicit conversions into a FunctionCallable
skill:
template<typename NamedType_> struct FunctionCallable; template <typename T, typename Tag, template<typename> class... Skills> struct FunctionCallable<NamedType<T, Tag, Skills...>> : crtp<NamedType<T, Tag, Skills...>, FunctionCallable> { operator T const&() const { return this->underlying().get(); } operator T&() { return this->underlying().get(); } };
(crtp
is a helper base class for implementing the CRTP pattern, that provides the underlying()
method, made for hiding the static_cast
of the CRTP).
And we can now write this example code using it:
using Label = NamedType<std::string, struct LabelTag, FunctionCallable>; std::string toUpperCase(std::string const& s); void display(Label const& label) { std::cout << toUpperCase(label) << '\n'; }
The case of operators
Note that one particular case of functions that this technique would make callable on a strong type is… operators!
Indeed, if a NamedType
has FunctionCallable
then it no longer needs Addable
, Multiplicable
and that kind of operators, because using them directly on the strong type will trigger the implicit conversion to the underlying type.
So you can’t use FunctionCallable
if you want to pick an choose some operators amongst the variety that exists.
Note that this wouldn’t be the case for all operators, though. For instance, due to the specificity of the hashing specialization, FunctionCallable
doesn’t replace Hashable
.
Calling methods
Since we can’t overload operator.
in C++ (yet?), we can resort to using operator->
. It wouldn’t be the first time that operator->
is used with the semantics of accessing behaviour or data in a component that doesn’t model a pointer. For instance, optional uses this approach too.
How operator->
works
Here is a little refresher on how operator->
works. If you feel already fresh enough, feel free to skip over to the next subsection.
The only operator->
that C++ has natively is the one on pointers. It is used to access data and methods of the pointed object, via the pointer. So it’s the only thing C++ knows about operator->
.
Now to use a ->
on a user-defined class, we need to overload operator->
for this class. This custom operator->
has to return a pointer, on which the compiler will call the native operator->
.
Well, to be more accurate, we can in fact return something on which the compiler calls operator->
, which returns something on which the compiler calls operator->
and so on, until it gets an actual pointer on which to call the native operator->
.
Implementing operator->
for NamedType
Let’s make operator->
return a pointer to the underling object stored in NameType
:
T* operator->() { return std::addressof(get()); }
Like its name suggests, std::addressof
retrieves the address of the object that it receives, here the underlying value of the strong type. We use that rather than the more familiar &
, just in case operator&
has been overloaded on the underlying type and does something else than returning the address of the object. It shouldn’t be the case but… you never know right?
Let’s not forget to return a const
pointer in the case where the strong type is const
:
T const* operator->() const { return std::addressof(get()); }
Finally, let’s get all this up into a MethodCallable
skill, so that a user can choose whether or not to use this feature on their strong type:
template<typename NamedType_> struct MethodCallable; template <typename T, typename Tag, template<typename> class... Skills> struct MethodCallable<NamedType<T, Tag, Skills...>> : crtp<NamedType<T, Tag, Skills...>, MethodCallable> { T const* operator->() const { return std::addressof(this->underlying().get()); } T* operator->() { return std::addressof(this->underlying().get()); } };
Calling both functions and methods
While we’re at it, let’s add the Callable
skill, that behaves as if you had both FunctionCallable
and MethodCallable
.
Since all this skill mechanism uses inheritance via the CRTP, we can simply compose them by inheriting from both:
template<typename NamedType_> struct Callable : FunctionCallable<NamedType_>, MethodCallable<NamedType_>{};
We can now use Callable
the following way, to be able to call both functions and methods (with operator->
for methods) on a strong type:
using Label = NamedType<std::string, struct LabelTag, Callable>;
This should make strong types easier to integrate in code.
The GitHub repo is one click away if you want a closer look. And like always, all your feedback is welcome!
Related articles:
- What the Curiously Recurring Template Pattern can bring to your code
- Strongly typed constructors
- Strong types for strong interfaces
- Inheriting functionalities from the underlying type
- Making strong types hashable
- Metaclasses, the Ultimate Answer to Strong Typing in C++?
Share this post!