Пользовательские литералы в C++11

в 14:34, , рубрики: c++, c++11, user-defined literals, Программирование, метки: , ,

Более полугода прошло с момента принятия стандарта C++11. В сети можно найти много материалов посвященных новому стандарту, однако большинство из них касаются самых простых возможностей, самых сладких. Я говорю о лямбда-функциях, системе автоматического выведения типов, новых спецификаторах, умных указателях и т.д. Да, это действительно интересные вещи и, можно смело сказать, они одни из самых полезных и часто используемых. Но на них свет клином не сошелся, и новенький C++11 предлагает нам не только их.

Ниже я хочу рассказать о пользовательских литералах — весьма полезном средстве, хоть и не в повседневных целях.

Что такое литерал?

Литерал — это некоторое выражение, создающее объект. Литералы появились не только в C++11, они были и в C++03. Например, есть литералы для создания символа, строки, вещественных чисел, и т.д.

    'x';      // character
    "some";   // c-style string
    7.2f;     // float
    74u;      // unsigned int
    74l;      // long
    0xF8;     // hexadecimal number

Все это литералы. С понятием литералов, думаю, мы разобрались. Самое время вернуться к C++11.

Пользовательские литералы в C++11

Как уже было отмечено выше, новый стандарт предлагает средства для создания пользовательских литералов. Существует две категории пользовательских литералов: сырые литералы (raw) и литералы для встроенных типов (cooked).

Стоит, однако, заметить, что C++ позволяет создавать только литералы-суфиксы. Иными словами, создать литералы префиксы (как, например, 0x), или префиксо-суфиксные (как "") — не получится.

Литералы для численных типов

Начнем с литералов для встроенных типов. Чтобы создать литерал для численных типов необходимо воспользоваться одной из двух сигнатур:

    // сигнатура литерала для целочисленных типов
    OutputType operator "" _suffix(unsigned long long);

    // сигнатура литерала для вещественных типов
    OutputType operator "" _suffix(long double);

Использование литерала будет осуществляется следующим образом:

    42_suffix;      // OutputType operator "" _suffix(unsigned long long);
    42.24_suffix;   // OutputType operator "" _suffix(long double);

Обратите внимание на сигнатуры:

  • литерал для целых чисел в качестве аргумента принимает unsigned long long
  • литерал для вещественных чисел в качестве аргумента принимает long double

Данные типы взяты не спроста и их нельзя заменить на другие. Они являются обязательными и утверждены стандартом языка.

Ниже приведен пример литерала, преобразовывающего минуты в секунды.

    unsigned long long operator "" _min(unsigned long long minutes)
    {
        return minutes * 60;
    }

    // ...

    std::cout << 5_min << std::endl; // на экран выведится 300
Литералы для строковых типов

Для создания литерала этого типа, необходимо воспользоваться одной из следующих сигнатур:

    OutputType operator "" _suffix(const char* str, size_t size);
    OutputType operator "" _suffix(const wchar_t* str, size_t size);
    OutputType operator "" _suffix(const char16_t* str, size_t size);
    OutputType operator "" _suffix(const char32_t* str, size_t size);

Сигнатура выбирается в зависимости от типа строки:

    "1234"_suffix;   // operator "" _suffix(const char* str, size_t size);
    u8"1234"_suffix; // operator "" _suffix(const char* str, size_t size);
    L"1234"_suffix;  // operator "" _suffix(const wchar_t* str, size_t size);
    u"1234"_suffix;  // operator "" _suffix(const char16_t* str, size_t size);
    U"1234"_suffix;  // operator "" _suffix(const char32_t* str, size_t size);

Пример литерала преобразующего C-style строку в std::string приведен ниже.

    std::string operator "" s(const char* str, size_t size)
    {
        return std::string(str, size);
    }

    // ...

    std::cout << "some string"s.length() << std::endl;
Сырые литералы

Ну и наконец настало время сырого литерала. Сигнатура сырого литерала выглядит следующим образом:

    OutputType operator "" _suffix(const char* literalString);

Этот тип литералов приходить на помощь тогда, когда входное число надо разобрать по-символьно. Т.e. в этом случае число передается в оператор как строка. Если не совсем понятно, взгляните на приведенный ниже код:

    OutputType operator "" _x(unsigned long long);
    OutputType operator "" _y(const char*);

    1234_x;     // call: operator "" _x(1234);
    1234_y;     // call: operator "" _y("1234");

Используя данный тип литералов, можно написать литерал преобразующий двоичное число в десятичное. Например вот так:

    unsigned long long operator "" _b(const char* str)
    {
        unsigned long long result = 0;
        size_t size = strlen(str);

        for (size_t i = 0; i < size; ++i)
        {
            assert(str[i] == '1' || str[i] == '0');
            result |= (str[i] - '0') << (size - i - 1);
        }

        return result;
    }

    // ...

    std::cout << 101100_b << std::endl; // выведет 44

Существует еще одна сигнатура для сырых литералов. Основана она на применении Variadic Template:

    template <char...>
        OutputType operator "" _b();

Преимущества литералов на базе Variadic Template заключается в том, что они могут вычисляться на этапе компиляции. Тот же литерал преобразования двоичного числа в десятичное может быть переписан так:

    template <char... bits>
        struct to_binary;

    template <char high_bit, char... bits>
        struct to_binary<high_bit, bits...>
        {
            static_assert(high_bit == '0' || high_bit == '1', "Not a binary value!");
            static const unsigned long long value =
                (high_bit - '0') << (sizeof...(bits)) | to_binary<bits...>::value;
        };

    template <char high_bit>
        struct to_binary<high_bit>
        {
            static_assert(high_bit == '0' || high_bit == '1', "Not a binary value!");
            static const unsigned long long value = (high_bit - '0');
        };

    template <char... bits>
        constexpr unsigned long long operator "" _b()
        {
            return to_binary<bits...>::value;
        }

    // ...

    int arr[1010_b]; // значение вычисляется во время компиляции
    std::cout << 101100_b << std::endl; // выведет 44

У внимательно читателя мог возникнуть вопрос: «А что если создать и сырой литерал, и литерал для числа с одним и тем же именем? Какой литерал компилятор применит?». Стандарт по этому поводу дает точный ответ и говорит о попытке компилятора применить литералы в следующем порядке:

  • operator "" _x (unsigned long long) или operator "" _x (long double)`
  • operator "" _x (const char* raw)
  • operator "" _x <'c1', 'c2', ... 'cn'>

Полезно знать, что если определенный пользователем литерал совпадает с системным (например f), то выполнится системный.

    long operator "" f(long double value)
    {
        return long(value);
    }
    // ...
    std::cout << 42.7f << std::endl; // выведет 42.7

Выводы

Бьёрн Страуструп на конференции Going Native 2012 приводил полезный пример использования литералов. Мне кажется, он наглядно демонстрирует факт повышения читаемости кода, а также снижает вероятность ошибиться.

    Speed sp1 = 100m / 9.8s;    // very fast for a human
    Speed sp2 = 100m / 9.8s2;   // error (m/s2 is acceleration)
    Speed sp3 =  100 / 9.8s;    // error (speed is m/s and 100 has no unit)

Механизм пользовательских литералов — это полезный в некоторых случаях инструмент. Использовать его где попало не стоит. Подумайте дважды, прежде чем их использовать, ведь литералы коварны: они могут…

  • как повысить читаемость кода, так и понизить;
  • как сыграть вам на руку, так и против вас.

p.s:
Пользовательские литералы поддерживаются компиляторами gcc 4.7 и clang 3.1.

Автор: ikalnitsky

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js