The Number that Broke and Spoke – C++ Investigator

How many times 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 who ever 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 2 am when I got a really bad feeling. I knew that something just happened to that patch I created the day before. All I done 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:

/*void delay_m(float minutes);
void delay_s(float seconds);
void delay_millis(float milliseconds);
void delay_micros(float microseconds);
void delay_ns(float nanoseconds);*/

// ... A class function ...
    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 chars and chars array.

It's called User-defined literals.

Sincerely yours,
cppreference.

User-defined literals

This feature makes it possible to mark with a suffix numbers, chars and strings, 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:

// void delay(std::chrono::nanoseconds) {}
// ...
    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 a 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 (sice 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 that 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::;. Without it the syntax would look like this:

auto str = std::string_literals::operator""s("aaa", 3);
// Instead Of: auto str = "aaa"s;

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 on 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 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(0, arg);
  • if - returns std::complex(0, arg);
  • il - returns std::complex(0, arg);
int main() {
    using namespace std::complex_literals;
    std::complex<double> c = 1.0 + 1i; // std::complex<double>(1.0, 1.0)
    std::complex<float> z = 3.0f + 4.0if; // std::complex<float>(3.0, 4.0)
}

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; // 180
    std::cout << deg.get_radians() << std::endl; // 3.14..
    std::cout << rad.get_degrees() << std::endl; // 360
    std::cout << rad.get_radians() << std::endl; // 6.28..
    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 &amp;&amp; m % 2 || m >= 8 &amp;&amp; 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;
}

Summarize

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.

This article originally published on my personal blog: C++ Senioreas

Full examples repository: cppsenioreas-user-defined-literals

20