Thoughts from a C++ library developer.

Tutorial: Emulating strong/opaque typedefs in C++

Last week, I’ve released my type_safe library.
I described it’s features in the corresponding blog post but because the blog post got rather long,
I couldn’t cover one feature: strong typedefs.

Strong or opaque typedefs are a very powerful feature if you want to prevent errors with the type system

and as I’ve been advocating for, you want that.
Unlike “normal” typedefs, they are a true type definition:
they create a new type and allow stuff like overloading on them and/or prevent implicit conversions.

Sadly, C++ doesn’t provide a native way to create them,
so you have to resort to a library based emulation.

BTW, type_safe received a couple of requested features:
There are improvements to the monadic optional functions (bind(), map(), unwrap() as well as a new transform()),
multi-visitation of optionals and ArithmeticPolicy to control over/underflow behavior of the ts::integer<T>.

Motivation

Suppose your code has to deal with some units.
Now you could employ the same technique as the excellent std::chrono library,
but maybe you just need meters and kilograms and it would be overkill.
To make it more clear which variables store which unit, you define some type aliases:

usingmeter=int;usingkilogram=int;

Instead of declaring your heights as int height, you write meter height.
Everything is wonderful until you want to write a function to calculate the body mass index:

intbmi(meterheight,kilogramweight);

Hours pass by, the deadline approaches and late at night you quickly need to call that function somewhere:

autoresult=bmi(w,h);

You forgot the correct order of arguments, call the function incorrectly and waste a lot of time debugging.

Now, clearly a meter is not a kilogram, so it should be an error to convert those to.
But the compiler does not know that, the type alias is just that: a different name for the same type.
Strong typedefs can help here:
They create a new type with the same properties as the original one.
But there is no implicit conversions from one strong typedef type to the other one.

There is a proposal to add strong typedefs to the core language,
but I couldn’t get much information about it’s status, so I’ve asked on twitter:

What's the current status of opaque/strong typedefs?If they're not coming: Why?!#cpp#cplusplus

Nobody seemed to know, so I wrote a mail to the author, Walter E. Brown, and asked him.
He told me that Bjarne doesn’t like that feature (anymore), so it is very unlikely that it will come anytime soon.

We’ve created our new type meter, it is explicitly convertible to and from int.
The explicit conversion from int is useful to prevent errors like:

bmi(70,180);

Once again we messed up the parameter order but if the new types were implicitly convertible, it would work just fine.
The explicit conversion to int on the other hand could be implicit.
This would allow:

voidfunc(int);…func(meter(5));

But I find it cleaner if you need a cast there to show your intent.
Making the conversion to int explicit also prevents a lot of other things, however:

This module takes care of creating and storing the value, but you still need to write the interface.
That’s where other modules come in.
But first we need a way to get the underlying type - the interface is so minimal,
it does not provide a way to get it!

But no worries, it can be made a non-member very easily.
A first approach can be partial template specializations:

With partial template specializations you can decompose a type and extract it’s template arguments.
But this approach does not work here because we create a new strong typedef by inheriting from the basic module.
underlying_type<meter> would be ill-formed because meter inherits from strong_typedef and is not the class itself.
So we need a way that allows a derived-to-base conversion - a function:

This is just a tiny class that only creates some friend functions.
The problem is that we want to conditionally provide operators for our strong typedef type.
An elegant way to do this is to use those friend functions.
In case you didn’t know, if you write a friend function definition inside the class,
the function name is not injected into the outer namespace,
it is just found via ADL.

This is perfect here.
We simply create friend functions in our module that overload the operator for our strong typedef type.
When we inherit from the module, the friend functions are available for the derived class, but not for anything else.

Did you know that operator+= can be a non-member as well?
While we can simply make it a member function of addition,
this can lead to problem because it requires a conversion of this.
If we combine it with the mixed_addition defined later,
this will lead to an ambiguous conversion.

The approach in the module is simple:
we convert both arguments to the underlying type which should provide the operator,
do the operation and convert them back.
This return type conversion is very important, otherwise we would be loosing our abstraction!

But why not overload every operator directly?

But why are we using this modular design?
Why not provide everything in the strong_typedef directly, screw the entire inheritance and write:

structmeter_tag{};usingmeter=strong_typedef<meter_tag,int>;

Well, because type safety.
That’s why.

The built-in type are quite general.
They provide a lot of operations.
But often when creating a strong typedef you add some level of semantics on top of them.
And sometimes, some operations just don’t make sense!

For example, suppose you’re dealing with integer handles, like those used in APIs such as OpenGL.
To prevent implicitly passing regular integers as a handle, you create a strong typedef,
and imagine it would generate all the operator overloads:

For a handle type you do not want arithmetic!
You only want equality and maybe relational comparison, but not much more.

For that reason, the basic strong_typedef module I’ve described does not create any operations,
so it can be used as basis in all situations.
If you want some overloads, inherit from the module or overload the operators yourself.

What about user-defined types?

Okay, now we’ve written overloads for all the common operator overloads and can create strong typedefs to integers and even iterators:

But the interfaces of some types do not consist solely of operators (citation needed).
To be precise: user-defined types also have named member functions.

And this is where strong typedef emulation fails.
While the operators have (reasonable) semantics and a well-defined interface,
arbitrary member functions do not.

So you can’t write generic modules (usually), you’d have to bite the bullet:

structmy_new_udt:strong_typedef<my_new_udt,udt>{voidfoo(my_new_udt&u){static_cast<udt&>(*this).foo(static_cast<udt&>(u));}my_new_udtbar(inti)const{returnmy_new_udt(static_cast<constudt&>(*this).bar(i));}my_new_udt&foobar(){auto&udt=static_cast<udt&>(*this).foobar();// uhm, who am I supposed to convert it to my_new_udt& exactly...?
}};

This is verbose.
There is no really solution to that problem either.

There is the operator.() proposal which would allow calling functions on the underlying type without knowing them,
but it does not convert arguments or return types to the strong typedef type instead of the underlying.

This is exactly why we need strong typedefs as a language feature or at least some form of reflection to do this kind of work automagically.
To be fair, the situation isn’t so bad,
because more often than not you need a strong typedef to a built-in type
and/or can add a phantom type like the Tag used in the strong_typedef here to differentiate between otherwise identical types.

But for the situations where you can’t do that, you’re screwed.

Conclusion

Strong typedefs are a great way to add more semantics to your types and catch even more errors at compile time.
But they are rarely used in C++ because C++ lacks a native way to create one.
While you can emulate them quite well for built-in types,
using them for user defined type is very verbose,
so the language really needs native support for them.

The strong typedef facility shown here is provided by type_safe.
I’ve already written many modules for you, they are available in the sub-namespace strong_typedef_op.
If you haven’t already, you can also check out my previous post that outlines the other features of this library.

This post was made possible by my Patreon supporters.
If you'd like to support me as well, please head over to my Patreon and do so!
One dollar per month can make all the difference.