Last time we discussed strong types and in particular, strongly typed containers. We introduced the idea through a constructor that takes two integers and two boolean values and we saw how easy it is to mess them up.
A little recap of the problem
There is not much difference between the two below instantiations of the Car
constructor
Car::Car(unit32_t horsepower, unit32_t numberOfDoors, bool isAutomatic, bool isElectric);
//...
auto myCar{Car(96, 4, false, true)};
auto myCar{Car(4, 96, true, false)};
Yet one doesn't make much sense, while the other is something meaningful. Then we ended up with the following constructor and instantiations:
Car::Car(Horsepower hp, DoorsNumber numberOfDoors, Transmission transmission, Fuel fuel);
auto myCar = Car{Horsepower{98u}, DoorsNumber{4u}, Transmission::Automatic, Fuel::Gasoline};
auto myCar = Car{DoorsNumber{98u}, Horsepower{4u}, Transmission::Automatic, Fuel::Gasoline}; // Really?
Here we could, we can already see the value of strong typing, it's much more difficult to make a mistake. Not only the - sometimes hardcoded - numbers and the variable names represent values, but the types as well. One more checkpoint.
Though that's not the last step if you want to increase safety and readability, especially in unit tests, where most of the hardcoded values reside.
User-defined literals to the rescue
User-defined literals allow integer, floating-point, character, and string literals to produce objects of user-defined type by defining a user-defined suffix.
Ok, what does it mean in practice?
It means that still keeping the strong types of Horsepower
and DoorsNumber
, you can declare a Car
object as such:
auto myCar = Car{98_hp, 4_doors, Transmission::Automatic, Fuel::Gasoline};
Just like in the previous version, you have to write the type or something similar, yet if you look at it, it seems more natural to write 98_hp
or 4_doors
than Horsepower(98u)
or DoorsNumber(4u)
. We are closer to the ideal state of code when it reads like a well-written prose as Grady Booch wrote in Object
Oriented Analysis and Design with Applications.
All that you need for that is a user-defined literal for both types. For the sake of brevity, let's omit Transmission
and Fuel
.
#include <iostream>
class Horsepower {
public:
Horsepower(unsigned int performance) : m_performance(performance) {}
private:
unsigned int m_performance;
};
Horsepower operator"" _hp(unsigned long long int horsepower) { //1
return Horsepower(horsepower); //2
}
class DoorsNumber {
public:
DoorsNumber(unsigned int numberOfDoors) : m_numbeOfDoors(numberOfDoors) {}
private:
unsigned int m_numbeOfDoors;
};
DoorsNumber operator"" _doors(unsigned long long int numberOfDoors) { //3
return DoorsNumber{static_cast<unsigned int>(numberOfDoors)}; //4
}
class Car {
public:
Car(Horsepower performance, DoorsNumber doorsNumber) : m_performance(performance), m_doorsNumber(doorsNumber) {}
private:
Horsepower m_performance;
DoorsNumber m_doorsNumber;
};
int main() {
auto car = Car{98_hp, 4_doors};
}
There are a couple of things to notice here. On lines 1) and 3) we use unsigned long long int
. Either we envision extremely powerful cars with a door for everyone in the world, or there is something else going on.
It's something else.
For a reason that I haven't found myself, only about a dozen types are allowed on literal operators and this seemed to be the best available option.
This doesn't mean that we should change the types wrapped by Horsepower
or DoorsNumber
. There is no reason to change them, so in the literal operators, we must narrow from an unsigned long long int
to an unsigned int
.
We could of course fall back an implicit narrowing as we did on line 2), but implicit conversions are barely a good idea, and narrowing conversions are even worse - even according to the Core Guidelines. If you really must perform one, be explicit about it, like we were on line 4). Please note, that probably gsl::narrow_cast
is a better idea, given that you have access to gsl
.
static_cast
has no performance overhead like dynamic_cast
has, so that cannot be a concern. And besides, the above usage is mostly to increase the readability of unit tests, and their performance is not a big concern.
But I don't want to imply that user-defined literals can only be useful when you write unit tests. Even with the above usage, you might increase the readability of your production code when you define some constants, but more importantly, there can be other usages.
Imagine that it makes come conversions, such as you could use it for converting between Celsius and Fahrenheit.
#include <iostream>
long double operator"" _celsius_to_fahrenheit(long double celsius) {
return celsius * 9 / 5 +32;
}
int main() {
std::cout << "100 Celsius is " << 100.0_celsius_to_fahrenheit << std::endl;
}
Conclusion
Today, we have learned about user-defined literals, a powerful way to boost the readability of your code. Whether you want to perform some conversions on certain primitive types or you want to improve the instantiation of your strongly-typed primitives, user-defined literals will help you.
Have you already used them? Please share your use-cases!
Thank you for reading, and let's connect!
Thank you for reading my article. Feel free to subscribe to my newsletter and connect on Twitter!
Top comments (4)
Great article, user defined literals looks really promising to enforce type safety and avoid conversion errors.
There is one domain where it can help it's when you do physics and you have a lot of units to convert to and from.
For example the code works with radians, but for a human it's easier to write constants or parameters in degrees so you can create a class Angle and the corresponding literals for degrees and radians and it will handle conversions for you.
There is a actually a library making use of this: github.com/mpusz/units
But it needs to be compiled with c++20 standard.
Thanks for your comment. This seems a promising library, let's see if it gets into the next standard in 3 years!
Also, time to get some C++20 locally to try this. What compiler do you use?
Well I use g++ but we aren't using this lib nor c++20 yet it's too recent.
I try to install clang today, but the build takes forever and I always get a signal 9. But at least later and later as I add the hacks proposed...