[C++] User-Defined Literals To Handle Units
You can find this example here on compiler explorer.
Recently I needed a custom time value at work. The problem was that I needed to provide a raw value as unsigned long in tenths of a second (or 100ms) to a legacy API and also needed the value in milliseconds and in seconds. So let's create a own type for that where I can break down my solution in this article.
User Defined Literals
Since C++11 we can implement user defined literals, which allow us the following:
long double operator""_deg_to_rad(long double deg)
{
long double radians = deg * std::numbers::pi_v<long double> / 180;
return radians;
}
// ...
// value is now 90 degree in radiants -> 1.5707...
double value = 90.0_deg_to_rad;
And the same technique we'll use in combination with cwt::time
, our time type:
// let's start with the time class (of course the order in the actual code is the other way around):
class time
{
public:
// ... default ctors and assignment operators impl. here ...
// a templated constructor which accepts a templated type T
// the concept time_type checks if this type has a time_factor
// with which it's value is multiplied
template<time_type T>
constexpr time(const T& t) : m_value(t.value * T::time_factor) {}
// ... we'll come to member functions down below.
private:
// the actual value which we hold underneith
// since this is private, we can't modify this from outside
std::size_t m_value{0};
}
// and here the according concept, to verify time type
template<typename T>
concept time_type = requires(T t) {
T::time_factor;
t.value;
};
// now we need some specific time units:
// as base unit we'll use milliseconds so the factor is 1
struct time_ms{
static constexpr std::size_t time_factor = 1;
std::size_t value{0};
};
// the tenths of a second are 100ms steps, gives a factor of 10
struct time_100ms{
static constexpr std::size_t time_factor = 100;
std::size_t value{0};
};
// and finally 1000 ms per seconds gives a factor of 1000
struct time_s{
static constexpr std::size_t time_factor = 1000;
std::size_t value{0};
};
with this very first code, you're only allowed to create an instance of it by giving the exact time to it:
cwt::time t1(cwt::time_ms{100}); // OK
cwt::time t2(cwt::time_100ms{100}); // OK
cwt::time t3(cwt::time_s{100}); // OK
cwt::time t4(100); // Error: no matching function for call to 'cwt::time::time(int)'
Now, we can implement user defined literals to use another way here to create our time type:
// let's put the overloads into a own namespace
// we'll just return our specific time types in each overload
namespace time_literals
{
constexpr inline time operator""_s(unsigned long long value){
return time(cwt::time_s{value});
}
constexpr inline time operator""_100ms(unsigned long long value){
return time(cwt::time_100ms{value});
}
constexpr inline time operator""_ms(unsigned long long value){
return time(time_ms{value});
}
} // namespace time_literals
//...
// and this gives us the ability to write our code in this way:
using namespace cwt::time_literals;
cwt::time t1(100_ms); // OK
cwt::time t2(100_100ms); // OK
cwt::time t3(100_s); // OK
cwt::time t4(100); // Error: no matching function for call to 'cwt::time::time(int)'
Now we can overload comparison and arithmetic operators to improve the cwt::time
and add some member functions to modify and access our time value:
class time{
public:
// constructors as before...
// getter for the raw value as unsigned long
[[nodiscard]] const std::size_t value() const noexcept { return m_value; }
// a templated add function to specife the specific time type
// as template argument and to pass a integral value in it
template<time_type T>
void add(const std::size_t rhs) { m_value += rhs * T::time_factor; }
// a add function for same time
void add(const time& rhs) { m_value += rhs.value(); }
// a increment function which increments by the timefactor
template<time_type T>
void increment() { m_value += T::time_factor; }
// a decrement function which increments by the timefactor
template<time_type T>
void decrement() { m_value -= T::time_factor; }
// a templated getter to specify in which unit we want the value
// for my use case i cut off floating point numbers...
template<time_type T>
[[nodiscard]] std::size_t value_in() const { return m_value / T::time_factor; }
// and here the arithmetic operators
template<time_type T>
void operator+=(const T& rhs) { m_value += rhs.value * T::time_factor; }
template<time_type T>
void operator-=(const T& rhs) { m_value -= rhs.value * T::time_factor; }
void operator+=(const time& rhs) { m_value += rhs.value(); }
void operator-=(const time& rhs) { m_value -= rhs.value(); }
friend time operator+(time lhs, const time& rhs) { lhs += rhs; return lhs; }
friend time operator-(time lhs, const time& rhs) { lhs -= rhs; return lhs; }
// and the comparison operators
[[nodiscard]] bool operator==(const time& rhs) const noexcept { return m_value == rhs.value(); }
[[nodiscard]] bool operator>=(const time& rhs) const noexcept { return m_value >= rhs.value(); }
[[nodiscard]] bool operator<=(const time& rhs) const noexcept { return m_value <= rhs.value(); }
[[nodiscard]] bool operator>(const time& rhs) const noexcept { return m_value > rhs.value(); }
[[nodiscard]] bool operator<(const time& rhs) const noexcept { return m_value < rhs.value(); }
private:
std::size_t m_value{0};
};
And now you can use this time value.
Comparisons:
using namespace cwt::time_literals;
cwt::time t1(100_ms); // OK
cwt::time t2(100_100ms); // OK
cwt::time t3(100_s); // OK
// cwt::time t4(100); // Error: no matching function for call to 'cwt::time::time(int)'
// we can only compare our time units:
assert(t1 == t1);
assert(t2 > t1);
assert(t2 < t3);
assert(t1 == 100_ms);
assert(t1 < 5_s);
assert(t3 > cwt::time_s{1});
// this doesn't work:
// assert(t1 < 2); // Error: no match for 'operator>' (operand types are 'cwt::time' and 'int')
Modifications:
cwt::time t;
t += 1_s; assert(t.value_in<cwt::time_100ms>() == 1000);
t += 15_ms; assert(t.value_in<cwt::time_100ms>() == 1015);
t.increment<cwt::time_s>(); assert(t.value_in<cwt::time_100ms>() == 2015);
t.add(1_100ms); assert(t.value_in<cwt::time_100ms>() == 2115);
t.add<cwt::time_ms>(20); assert(t.value_in<cwt::time_100ms>() == 2135);
std::size_t v_ms = t.value_in<cwt::time_ms>(); assert(v_ms == 2135);
std::size_t v_100ms = t.value_in<cwt::time_100ms>(); assert(v_100ms == 21);
std::size_t v_s = t.value_in<cwt::time_s>(); assert(v_s == 2);
Now you can go through all the statements step by step and check the asserts (after each modification on the right) how the value is changing accordingly. And note: we're explicitly want to compare this in ms (we could use also 100ms or seconds).
You can find this example here on compiler explorer.
Conclusion
And I really like this type, especially when you've worked with legacy code where you find sort of code a lot: long t = 15
or long t = get_time_value();
, you can use it but at a certain point you don't know which time value it actually is when you have different units.
With this I'm now able to know which time it is in my code and it is easy to read. And you also can this apply to other metrics, like meters or any other physical values.
But that's it for now.
Best Thomas