Jonathan Boccara's blog

Calling Functions and Methods on Strong Types

Published November 7, 2017 - 7 Comments

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:

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:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed