This is the second instalment 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. Perhaps an antidote to “type punning“.
The Why
Consider this real-life code
1 2 3 4 5 6 7 |
// Air Density(kg/m3) from relative humidity(%), // temperature(°C) and absolute pressure(Pa) double AirDensity(double hr, double temp, double abs_press) { return (1/(287.06*(temp+273.15))) * (abs_press - 230.617 * hr * exp((17.5043*temp)/(241.2+temp))); } |
One can mix up values when calling this function in more than one way, on more than one occasion. And, equally bad, if that mistake is made it is extremely hard to catch it. This is the whole point of using strong types. One simply can not make a mistake, that easy as without them.
1 2 3 4 5 6 7 8 9 10 11 |
STRONG( Humidity, double) ; STRONG( Temperature, double) ; STRONG( Pressure, double) ; // Air Density(kg/m3) from relative humidity(%), // temperature(°C) and absolute pressure(Pa) double AirDensity(Humidity hr, Temperature temp, Pressure abs_press) { return (1/(287.06*(temp.v+273.15))) * (abs_press.v - 230.617 * hr.v * exp((17.5043 * temp.v) / (241.2 + temp.v))); } |
I just thought to make a little post about the use-case scenarios of this strong type paradigm shift.
For me, the core benefits are simplicity and applicability to both C and C++. And, I indeed might be so bold to think, I can show a few, perhaps surprisingly effective, examples.
Let’s dive
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.
1 2 3 4 5 |
#ifdef __cplusplus #define STRONG(N,T) struct N final { using value_type = T; T v; } #else // C #define STRONG(N,T) typedef struct { T v; } N #endif |
That creates the strong type, a simple struct with the required type name and with the member v
of the required value_type
. C and C++ dichotomy accounted for from the start.
Whenever necessary, I will notify of the language of the snippet: C or C++.
First classical use case: strong types as physical “units”. One of the oldest general confusions in programming at large. We want units to be distinctive “strong” types, not intrinsic (native) types, where only values are named appropriately.
1 2 3 4 5 6 7 8 9 10 |
// these are not unit strong types double Mm ; double Km ; double Cm ; // even worse anti pattern 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 a few declarations of strong types, representing various units.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 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. It is as simple as that. Above we have eight strong types. They all have a name and value_type
. They are all structs. Extremely simple structs. Although it is debatable if structs can be simple in C++.
1 2 3 4 5 |
// C++ "strong type" struct Meter final { using value_type = double ; 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. There is more.
Strong types are literals
The above “unit” Meter is literal. That is C++ context.
1 2 3 4 |
// strong type literal constexpt Meter ten_meters = Meter{ 10.0 }; static_assert ( ten_meters.v == 10.0 ); |
The Use
We can already code rather safe and simple transformations of linear units, aka lengths. For example.
1 2 3 4 5 6 7 8 9 |
// C++ overloading made much simpler 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, came to appreciate very much.
The usage.
1 2 3 4 5 6 7 8 9 10 11 |
// C++ 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 // fo example // overload not found // Mm m1_1 = transform(632.45); // overload not found // there is no kilgorams overload // Mm m4 = transform(Kg{ 42 }); |
Non allowed transformations simply do not compile. Can the above be made with overloaded function templates? It can. But much more difficult to do it right. And the end result will be just the same. And it will take much longer to develop.
Let us talk a bit more about
Nested strong types
Strong types are indeed useful as class members. In which case they are nested strong types.
1 2 3 4 5 |
// members as nested strong types struct Coordinate final { STRONG(X, double) x; STRONG(Y, double) y; }; |
That is indeed simple and it works smoothly as long as fundamental 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 into the Point struct.
1 2 3 4 5 6 7 8 9 10 11 |
/* The non trivial strong type struct Point { Coordinate v; }; */ STRONG(Point, Coordinate); // giving values to Point's. // Coordinate is nested struct // made of two strong types Point p1{ {.x = 23.45 , .y = 42.67 } }; |
That code requires focus, but it is not that hard. To reach the actual X and Y values, on Point p1, one must type:
1 2 3 4 5 6 7 8 9 10 11 12 |
/* struct Point { struct Coordinate { struct X { double v; } x; struct Y { 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. It is equally effective.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// single level nesting aka flat struct Point{ STRONG(X, double) x; STRONG(Y, double) y; }; /* struct with two strong types nested struct Point{ struct X { double v; } x; struct Y { 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. The key point is: one can not make the easy mistake of mixing x and y. They are strong types, not intrinsic arithmetic types.
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 colour to our Duffy, the strong duck from the fist post on strong types.
Colour is a combination of red, green and blue components. And their components are expressed as hex values. Let’s generate the required code for this design.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 improved.
1 2 3 4 5 6 |
// "Flying Duck Research" code struct Duck final { mutable Mm height; mutable Kg weight; mutable Color color; }; |
That is one non-trivial structure layout. Using the above here is how we create duffy
but this time, with some colour, added.
1 2 3 4 5 6 |
// C++20 designated initialization constexpr Duck duffy{ .height = 350.32, .weight = 4.323, .color = { 0xA,0xFF,0xA } }; |
That even looks simple and logical. Now let’s see the usability.
1 2 3 4 5 |
// 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 }; |
Not trivial but simple. And to actually use the colour we do
1 |
auto color = duffy.color.mix(); |
Which type is std::array<Hex,3>
.
I certainly will use this in the production code. I hope you have seen some value in this simple concept for your projects too.
Sub-types of Fasteners type. They are sub-types but it is impossible to confuse them with each other.