In theory, practice and theory are the same, in practice they are not. So, after having read how brilliant and smart smart-types are, it is now time to have a closer look at the compiler and figure out what C++ can offer.
After my last post, I found that Smart Types are also known as Refined (or refinement) Types. And here is a notable implementation for Scala.
Simple things first, if you need a type with a bunch of possible values, don’t use int
& #define
s, don’t use bool either (please), use enum, or, even better enum class.
Now that we’ve done with the trivialities, let’s proceed to something more challenging – numeric types. Ideally, we want some template code that wraps the numeric type and saves us the boredom of writing all the usual +, -, *, /, ==, !=, <… operators, while letting us define the rules of the existence of the represented type.
I mean, for each smart type, you would have to write an endless list of code like:
[[nodiscard]] T T::operator X( T a ) const noexcept
{
return T{ m_value X a.m_value };
}
And mostly the same for relational operators.
And there’s no other way since using and typedef just define type aliases so the type checker doesn’t distinguish between them. e.g.
typedef int Speed;
typedef int Acceleration;
void f( Acceleration acceleration );
Speed a = 3;
f( a ); // that's ok, Speed, Acceleration and int are indeed the same type
Being a C programmer at heart, my first idea was to use macros and preprocessor to craft the arithmetic smart type skeleton. Inspired to X-macros, something like:
#define BASE_TYPE int
#define TYPE_NAME Speed
#include <x-smartType.hh>
#undef TYPE_NAME
#undef BASE_TYPE
The x-smartType.hh
file would have been something like:
#if !defined( BASE_TYPE )
# error "Undefined preprocessor symbol named BASE_TYPE"
#endif
#if !define( TYPE_NAME )
# error "Undefined preprocessor symbol named TYPE_NAME"
#endif
class TYPE_NAME
{
public:
[[nodiscard]] TYPE_NAME operator+( TYPE_NAME other ) const noexcept
{
return m_value+other.m_value;
}
// all the other operators (*,/,-,<,<=,==,>,>=,!=)
private:
BASE_TYPE m_value;
};
Unfortunately, this approach yields at least two problems – first using the preprocessor is not a well-regarded practice in C++, second it solves half of the problem since you still need to provide the factory methods to construct the objects (remember you cannot just provide straightforward constructor because you need to ensure that only valid values are used).
More precisely using the inheritance relationship is not that straightforward. Consider the sum operation – you can define it in the base so that you can pass derived class as arguments, but the base class can’t return a derived object –
class SmartBase
{
public:
SmartBase operator+( SmartBase b ) const noxcept;
};
class Speed : public SmartBase
{ /*...*/ };
Speed a = ...;
Speed b = ...;
Speed s = a+b; // doesn't work
The last line above doesn’t work because a+b returns a SmartBase type, while s is of type Speed and either there is no way to convert a SmartBase into Speed, or SmartBase can be converted implicitly in every one of the derived types nullifying the effort to build a smart type.
Also, this doesn’t work because you can use any of the class derived from SmartBase and pass them as arguments, so you can sum Speed and Acceleration, and that’s not good.
Enter CRTP. If we parametrize the base class on the derived type we can write operators with the right types:
template<typename T>
class SmartBase
{
public:
[[nodiscard]] T operator+( T rhs ) const noexcept;
};
This is just half of the solution because the left-hand side of the sum is not bound to be a T and can be anything derived from SmartBase.
To get the desired result, we need a free function operator. Since this operator needs to access the implementation, it has to be declared as a friend –
template<typename T>
class SmartBase
{
public:
friend [[nodiscard]] T operator+( T lhs, T rhs ) const noexcept;
};
Now the problem is – how do you implement the operator?
The only way I found is to require that the derived class has a constructor from the numeric type (conversion constructor) and that is a friend to the base class so that methods of the base class can access the conversion constructor.
Moreover, this constructor cannot be called from friends of the base class but needs to be called from member functions of the base class.
For this reason, I defined a set of member functions that implements the operations:
template<typename T, typename N>
class SmartBase
{
public:
friend constexpr operator+( T lhs, T rhs ) noexcept;
protected:
explicit SmartBase( N value ) noexcept : m_value{value}{}
private:
static constexpr [[nodiscard]] T sum( T a, T b ) noexcept
{
return T{a.m_value+b.m_value};
}
N m_value;
};
Recap – you need operator+ with two arguments to avoid the first argument polymorphically accepting any descendant from the base class. But operators are friends of the base class, therefore they can’t build the result. Since the derived class is a friend of the base class, then only methods of the base class can build a derived object. So this requires a set of static functions that implement the operator.
I’m aware that this is a bit convoluted and still requires CRTP and base class to befriend the derived class, but it is a small price to pay for generic smart types that can easily make your code safer and stronger.
And … tah-dah, that’s all. All the pieces fall in place and you have a smart type template for C++.
PS: I’m working to make available this source code and others as well, just don’t hold your breath 🙂