We can use user-defined literals in order to control conversions between units, and this way, we can significantly reduce our code complexity and reduce the chances for units conversion mistakes.
How many times do you find yourself writing hard-coded numbers inside your code, while trying to make sure as much as you can to make these numbers’ units visible? You probably found yourself mentioning the units as part of the variable name, or at the comment, praying that whoever gets to your code will understand it. After reading this article, you will have the most maintainable way of doing so.
Numbers Attack
It was 2am when I got a really bad feeling. I knew that something just happened to that patch I created the day before. All I did was to add the following function:
void delay(float milliseconds) { ... }
In the morning, I ran into the office and checked the recent changes. This is what I saw:
float initial_delay_ns = 1000;
delay_ns(initial_delay_ns);
float increased_delay_s = 1;
delay_s(increased_delay_s);
auto current_delay_ns = initial_delay_ns + increased_delay_s * 1e9;
auto wait_time_m = 2.65;
delay_m(wait_time_m);
std::cout << "Total delay: " << current_delay_ns +
wait_time_m * 60 * 1e9 << std::endl;
Well, it can’t stay this way. The code looks like an active crime scene.
Bring Numbers to Justice
Those numbers won’t speak a single word without their lawyers - the variables. We need to find a way to make them talk even when their layers are out for business. I decided to hire a consultant – cppreference. After some days, she sent me the following message:
Hi there!
I heard some words about a way to mark your numbers, so you’ll be able to treat them in any way you want to. This mark can leave them as they are and can even transform them into a different type.
Note: This ability also relevant for char
s and char
s array.
It’s called User-defined literals.
Sincerely yours,
cppreference.
User-Defined Literals
This feature makes it possible to mark with a suffix numbers, char
s and string
s, so they’ll be more informative and understandable. This mark will produce a different object, that we’ll define.
Before we go deeper into syntax and rules, let’s see how the above example would look with user-defined literals:
using namespace std::literals::chrono_literals;
auto initial_delay = 1000ns;
delay(initial_delay);
auto increased_delay = 1s;
delay(increased_delay);
auto current_delay = initial_delay + increased_delay;
auto wait_time = 3min;
delay(std::chrono::duration_cast<std::chrono::nanoseconds>(wait_time));
std::cout << "Total delay: " <<
std::chrono::duration_cast<std::chrono::seconds>
(current_delay + wait_time).count() << " seconds." << std::endl;
Syntax
In order to create new user-defined literals, regardless what the standard offer to us, we have to use the following syntax:
[constexpr] [inline] out_type operator"" _suffix(in_type);
out_type
– Can be any type you need. suffix
– The characters you want (There are several rules which apply here, for the simplicity always start with a lowercase letter. For further reading: cppreference – user literal). in_type
– Only the following parameter lists are allowed:
const char*
unsigned long long int
long double
char
wchar_t
char8_t
(since C++20) char16_t
char32_t
const char*
, std::size_t
const wchar_t*
, std::size_t
const char8_t*
, std::size_t
const char16_t*
, std::size_t
const char32_t*
, std::size_t
- In case the function is a template function, it must have an empty parameter list and it must be a non-type template with the following restrictions:
The template is a non-type template parameter pack with element type char
:
template <char...> out_type operator"" _suffix();
Since C++20, it can be also a non-type template parameter of class type (in which case it is known as a string literal operator template):
struct A { constexpr A(const char *); };
template<A a> A operator"" _a();
Standard Library Main Literals
How to Use?
In order to use the standard user-defined literals, we have to use using namespace std::<literals namespace>;
. Without it, the syntax would look like this:
auto str = std::string_literals::operator""s("aaa", 3);
Usually, we don’t get to see this kind of syntax, even when we don’t use using namespace ...
which usually considered as a bad practice, so why do we need it here?
The answer for this question is ADL (Argument-dependent lookup), which I won’t be able to cover on this article. Make a long story short: The compiler can identify a function namespace if one of the parameters that sent to this function comes from the same namespace (e.g., std::cout
).
String Literals
using namespace std::string_literals;
The user-defined literals we get from this namespace are std::string_literals::operator""s
which accepts the following parameters:
const char* __str, size_t __len
const wchar_t* __str, size_t __len
const char8_t* __str, size_t __len
const char16_t* __str, size_t __len
const char32_t* __str, size_t __len
Each one of them will return a std::basic_string
object of the specified type. For example:
auto str = "Hello String Literals"s;
Will make str
of type std::basic_string<char>
which is the same as std::string
. This one is a very common usage of user-defined literals.
Chrono Literals
using namespace std::chrono_literals;
Chrono literals return a std::chrono::duration
object. It supplies the following user-defined literals: h
[hours], min
[minutes], s
[seconds], ms
[milliseconds], us
[microseconds], ns
[nanoseconds], y
[A specific year in the range: (-32767, 32767)], d
[Representing a day of a month in the calendar (legal only for values lower than 256)].
A usage example you can find in the first user-defined literals example in this article.
Complex Literals
using namespace std::literals::complex_literals
Complex literals will return a given number as an imaginary part with real part initialized to zero. Available user-defined literals:
i
– returns std::complex<double>(0, arg);
if
– returns std::complex<float>(0, arg);
il
– returns std::complex<long double>(0, arg);
int main() {
using namespace std::complex_literals;
std::complex<double> c = 1.0 + 1i; std::complex<float> z = 3.0f + 4.0if; }
User-Defined Literals Example
Angles Example
class angle {
public:
struct degrees {};
struct radians {};
constexpr angle(float deg, degrees) { _deg = deg; }
constexpr angle(float rad, radians) { _deg = rad * 180 / M_PI; }
[[nodiscard]] constexpr float get_degrees() const { return _deg; }
[[nodiscard]] constexpr float get_radians() const { return _deg * M_PI / 180; }
private:
float _deg;
};
namespace literals {
constexpr angle operator"" _deg(long double deg) {
return angle(deg, angle::degrees{});
}
constexpr angle operator"" _deg(unsigned long long int deg) {
return angle(deg, angle::degrees{});
}
constexpr angle operator"" _rad(long double rad) {
return angle(rad, angle::radians{});
}
constexpr angle operator"" _rad(unsigned long long int rad) {
return angle(rad, angle::radians{});
}
}
using namespace literals;
int main() {
constexpr auto deg = 3.14159265_rad;
constexpr auto rad = 360_deg;
static_assert(deg.get_degrees() == 180);
static_assert(rad.get_radians() == (float)(M_PI * 2));
std::cout << deg.get_degrees() << std::endl; std::cout << deg.get_radians() << std::endl; std::cout << rad.get_degrees() << std::endl; std::cout << rad.get_radians() << std::endl; return EXIT_SUCCESS;
}
Dates Example
Based on dates example from Become a Compile-Time Coder.
struct date_offset {
int d;
int m;
int y;
constexpr date_offset(int days, int months, int years) :
d(days), m(months), y(years) {}
[[nodiscard]] constexpr date_offset operator+(date_offset ref) const {
return date_offset(d + ref.d, m + ref.m, y + ref.y);
}
[[nodiscard]] constexpr date_offset operator-(date_offset ref) const {
return *this + date_offset(-ref.d, -ref.m, -ref.y);
}
};
class date {
public:
constexpr date(int day, int month, int year)
: d(day), m(month), y(year) {
self_balance();
}
[[nodiscard]] constexpr date offset(date_offset offset_data) const {
const auto after_years_offset = date(d, m, y + offset_data.y);
const auto after_month_offset =
date(d, m + offset_data.m, after_years_offset.get_year());
return date(after_month_offset.get_day() + offset_data.d,
after_month_offset.get_month(), after_month_offset.get_year());
}
[[nodiscard]] constexpr date operator+(date_offset offset_data) const {
return offset(offset_data);
}
[[nodiscard]] constexpr unsigned short get_day() const { return d; }
[[nodiscard]] constexpr unsigned short get_month() const { return m; }
[[nodiscard]] constexpr unsigned short get_year() const { return y; }
private:
int d, m, y;
constexpr void self_balance() {
unsigned short days_in_month;
unsigned short days_in_prev_month = 31;
bool is_change_detected = false;
if (m == 2) {
if (!(y % 4)) {
days_in_month = 29;
} else {
days_in_month = 28;
}
} else {
if (m <= 7 && m % 2 || m >= 8 && m % 2 == 0) {
days_in_month = 31;
if (m != 8) {
days_in_prev_month = 30;
}
} else {
days_in_month = 30;
}
}
if (d > days_in_month) {
d -= days_in_month;
m++;
is_change_detected = true;
} else if (d < 1) {
d += days_in_prev_month;
m--;
is_change_detected = true;
}
if (m > 12) {
m = 1;
y++;
is_change_detected = true;
} else if (m < 1) {
m = 12 - m;
y--;
is_change_detected = true;
}
if (is_change_detected) self_balance();
}
};
namespace literals {
inline namespace dates_literals {
constexpr date_offset operator"" _d(unsigned long long int days) {
return date_offset(days, 0, 0);
}
constexpr date_offset operator"" _m(unsigned long long int months) {
return date_offset(0, months, 0);
}
constexpr date_offset operator"" _y(unsigned long long int years) {
return date_offset(0, 0, years);
}
}
}
using namespace literals;
int main() {
constexpr date my_date(23, 8, 2020);
constexpr auto new_date = my_date + 8_d + 3_m + 5_y;
std::cout << new_date.get_day() << " / " <<
new_date.get_month() << " / " << new_date.get_year() << std::endl;
static_assert(new_date.get_day() == 1 &&
new_date.get_month() == 12 && new_date.get_year() == 2025);
return EXIT_SUCCESS;
}
Summary
A number without a literal is just a number without any meaning, and it makes the code really hard to maintain in the long term. We can use user-defined literals in order to control conversions between units, and this way, we can significantly reduce our code complexity and reduce the chances for units conversion mistakes.
Full examples repository: cppsenioreas-user-defined-literals