c++ strong types holy grail

This is the second installment of an aptly named “c++ strong duck” post. (a title is an act of deliberate sarcasm vs ‘duck typing’ ). If you haven’t before please make a slight detour to that post, for a brief introduction on strong types, as an antidote to “type punning“.

Exact types of Fasteners
Exact types of Fasteners type. It is impossible to confuse them with each other.

I just thought to make a little post about the use-case scenarios of this seemingly “toy concept”, whatever that label “toy concept” means.

For me the core benefits are simplicity and applicability to both C and C++. And , I indeed might think, I can show a few, perhaps surprisingly effective, examples.

I see nothing wrong with using macros in this instance. Here is the core single macro.  Please rename it if it clashes with something in your development environment.

// generate strong type
#define STRONG(N,T) \
struct N final { \
  typedef T value_type; \
  T v; \
}

That creates the strong type , a simple struct with the required type name and with the member v of the required value_type.

That is not “C”. I am sure C aficionados, will be more than able to make a C version.  But. This is one context where C++ comes out as superior. Especially because of the ability to have nested typedefs.

/* this is illegal C */
typedef struct Meter {
   /* C syntax error */
   typedef double value_type ;
   double v ;
} Meter ;

How allowing typedefs inside C structs will “ruin” the C, I honestly do fail to see?  Yes, I know that will introduce the scope resolution operator in C aka :: . Let’s just not “go there” in this post. Disclaimer: I do like C and often, I am reminded how effective it can be.

Let’s dive into some hopefully convincing use cases.

First classical use case: strong types as “units”. We want units to be distinctive types, not intrinsic (native) types, where only values are named appropriately.

// these are not unit types
double Mm ;
double Km ;
double Cm ;

// even worse
typedef double unit;
unit Mm;
unit Km;
unit Cm;

There are also numerous examples where people have been inventing and coding quite complex unit types, as large C++ classes.

I do not do that. Here is how we generate few declaration of strong types, representing various units.

// Weight measurement units
STRONG(Mg, double);
STRONG(Gram, double);
STRONG(Kg, double);
STRONG(Ton, double);

// linear distance units 
STRONG(Mm, double);
STRONG(Cm, double);
STRONG(Meter, double);
STRONG(Km, double);

// and so on with other units ...

That’s about it.  Above we have eight strong types. They al have name and value_type. They are all structs. Extremely simple structs.

// strong types
// are structures
// having only one data memner
// and one nested type
struct Meter {
  typedef double value_type ;
   double v ;
} ;

Do not lose the fact, about the simplicity of the code generated above.  I have not seen a simpler implementation of the concept of the strong type.  But that is not all in the goodie bag.

Strong types are literals

If, value types are literals too.  The above “unit” Meter is a literal.

// strong type literal
constexpt Meter ten_meters = Meter{ 10.0 };

static_assert ( ten_meters.value == 10.0 );

The Use

Now we can easily code rather safe and simple transformations of linear units, aka lengths. For example.

constexpr inline Mm transform(Cm len_) noexcept 
{ return { len_.v * 100 }; }

constexpr inline Mm transform(Meter len_) noexcept 
{ return { len_.v * 1000 }; }

constexpr inline Mm transform(Km len_) noexcept 
{ return { len_.v * 10000 }; }

Above we follow the C++ “pass by value” mandate. Thus we can pass temporaries, and we need no constant arguments.

Above can be all used at compile time, too.

Also, note how these overloads will never confuse the compiler, and in turn, the compiler will never confuse you. One quality, I am sure you being a C++ aficionado, do appreciate very much.

The usage.

// compile time
constexpr Mm m1 = transform(Cm{632.45});
constexpr Mm m2 = transform(Meter{3.45});
constexpr Mm m3 = transform(Km{0.5});
// mistakes can not be made
// overload not found
// Mm m1_1 = transform(632.45);
// overload not found
// Mm m4 = transform(Kg{ 42 });

Non allowed transformations simply do not compile.

Nested strong types

Strong types are indeed useful as class members. In which case they are nested strong types.

// members as nested strong types
struct Coordinate {
 STRONG(X, double) x;
 STRONG(Y, double) y;
};
/*
struct Coordinate {
 struct X { typedef double value_type; double v; } x;
 struct Y { typedef double value_type; double v; } y;
};
*/

That is indeed simple and it works smoothly as long as intrinsic types are used for value types of nested types.  Otherwise things will go very complex very quickly.

(Note: in this post I am using C++20 “designated initializers“. They are already working in all three main compilers.)

So, let’s nest the above nested struct into the Point struct.

/* 
  The non trivial strong type
  struct Point {
    typedef Coordinate  value_type;
    Coordinate value;
  };
*/
STRONG(Point, Coordinate);
// giving values to Point's. 
// Coordinate is nested strong type
Point p1{ {.x = 23.45 , .y = 42.67 } };

Not so bad. But. To reach the actual X and Y values, on the Point, one must type:

/*
struct Point { 
   typedef Coordinate value_type; 
   struct Coordinate {
     struct X { typedef double value_type; double v; } x;
     struct Y { typedef double value_type; double v; } y;
   } v; 
};

p1 is a instance of the above struct
*/
inline auto p_x = p1.v.x.v;
inline auto p_y = p1.v.y.v;

Not exactly simple or trivial.  So, please stick to single level nesting.

// single level type nesting
struct Point{
 STRONG(X, double) x;
 STRONG(Y, double) y;
};
/*
struct Point{
 struct X { typedef double value_type; double v; } x;
 struct Y { typedef double value_type; double v; } y;
};
*/
constexpr inline Point_type ps{ .x = 23.45 ,.y = 42.67  };
// rarely needed
inline auto psx = ps.x.v;
inline auto psy = ps.y.v;

The above declaration is useful and allows for simple, elegant and safe code. No deep nesting necessary. Example.

// C++ mandate: pass by value
inline constexpr Point point_mover(Point p_, Point location_ ) 
{
   // we return a new point created   
   return p_ = location_;
}

The key point is one can not make a mistake of mixing x and y. They are  strong types, not intrinsic arithmetic types. That is the whole point of using strong types. One simply can not make a mistake.

Complex nested strong types

Often times , we have complex scenarios and we simply can not evade nesting complex strong types as class members. With a bit of thinking we can solve that issue too.

Let us assume we want to add some color to our Duffy, the strong  duck from the fist post on strong types.

Color is combination of red, green and blue components.  And them components are expressed as hex values. Let’s generate the required code for this  design.

STRONG(Hex, unsigned char);
// strong types
STRONG(RED, Hex);
STRONG(GREEN, Hex);
STRONG(BLUE, Hex);
// final Color type
struct Color final {
RED	red;
GREEN	green;
BLUE	blue;
// make and return std::array<Hex,3>
auto mix() { return array{ red.v, green.v, blue.v }; }
};

With the method mix() above we have solved the complexity required by users actually wishing to use the Color instances. Here is the Duck type.

// Wild Duck Research Facility 
struct Duck final {
// mutable members are
// changeable even on
// compile time constants
mutable Mm	height;
mutable Kg	weight;
mutable Color	color;
};

Using the above here is how we create duffy but with some color added.

// C++20 designated initialization
constexpr  Duck duffy{ 
   .height = 350.32, 
   .weight = 4.323, 
   .color = { 0xA,0xFF,0xA } 
};

It even looks simple and logical. Now let’s see the usability.

// duffy is constexpr but properties are mutable
duffy.height = Mm{ 394.12 };
duffy.weight = Kg{ 4.52 };
// make duffy a bit more red
duffy.color.red = RED { 0xFF };

And to actually use the color we do

auto color =  duffy.color.mix();

Which type is std::array<Hex,3>.

I certainly will use this in production code. I hope, you have seen some value of this simple concept for your projects too.