Такие разные литералы

Много копий сломано по-поводу массового использования литералов в коде приложения. Апологеты продвигают идеи удобного использования литералов, дабы внести больше строгости в работе с оными, противники же против подобных изменений, так как считают, что подобные удобства только поощряют использование литералов в коде. Действительно, “магические числа” и прочие литералы, которые населяют код множества приложений могут ввести в исступление даже самого спокойного программиста, и, казалось бы, оппоненты продвижения литералов правы – нужно сказать нет литералам! Но, то ли лобби апологетов оказалось сильнее, то ли оппоненты не так уж и правы, но факт остается фактом: начиная с C++11 литералы упрочили своё положение в языке, и, судя по всему, продолжат своё наступление в будущих редакциях C++. В настоящей статье хотелось бы провести обзор нововведений в работе с литералами, а также, с темами которые так или иначе соприкасаются с ними.

К нам приходит Unicode!?

Помимо признания наличия нескольких потоков в современных приложениях, комитет по стандартизации, так же, обратил свой взор на Unicode, который игнорировать практически так же сложно как и многопоточность. Действительно, все те, кто пишут приложения, которые должны взаимодействовать с пользователем, всецело зависят от наличия unicode, в тех средствах с помощью которых приложение создаётся. Что получилось у комитета с unicode мы сейчас и рассмотрим.

Как многие наверное  знают на арене unicode правят бал три основных типа кодирования: utf-8, utf-16 и utf-32, каждый из которых обладает своими плюсами и минусами. Т.к. utf-8 представляет собой набор байт, то для его хранения нам не нужны никакие средства – он может быть сохранён в обычный char*. С utf-16 и utf-32 ситуация несколько иная: минимальная единица utf-16 состоит из 2 байт, а utf-32 из четырёх и C++ не содержит типов, которые могли бы гарантировано хранить данные для utf-16 и utf-32. Поэтому комитет по стандартизации ввёл 2 новых типа: char16_t и char32_t. Это два абсолютно новых типа, они не являются псевдонимами других типов(как size_t, к примеру).

Кто-то может быть подумает про wchar_t, но, к сожалению, этот тип не применим для unicode, т.к. его размер зависит от платформы. К примеру, в Visual Studio: sizeof(wchar_t) == 2, тогда как в gcc: sizeof(wchar_t) == 4

Помимо новых типов, в нашем распоряжении появились и новые типы литералов:

  • utf-8 – литерал u8””
  • utf-16 – литерал u””
  • utf-32 – литерал U””

Или кодом:

char* utf8String = u8"С Новым Годом!";
char16_t* utf16String = u"С Новым Годом!";
char32_t* utf32String = U"С Новым Годом!";

При этом, данные литералы не просто указание: “Считать каждый символ литерала типом X”, как это сделано в литерале L”” для wchar_t; это именно что unicode. В каждой из этих строк содержится строка “С Новым Годом!” в соответствующем кодировании. Давайте проверим этот с помощью clang:

#include <iostream>
#include <string>
using namespace std;

void printHex(const unsigned char* bytes, size_t size)
{
    for(size_t i = 0; i < size; i++)
        std::cout << std::hex << (int)bytes[i] << " ";
}

int main() 
{
    string utf8String = u8"🌲";
    u16string utf16String = u"🌲";
    u32string utf32String = U"🌲";
    cout << "Utf-8 bytes: ";
    using pcuc = const unsigned char*;
    printHex(reinterpret_cast<pcuc>(utf8String.data()),
        utf8String.size());
    cout << "\n";
    cout << "Utf-16 bytes: ";
    printHex(reinterpret_cast<pcuc>(utf16String.data()),
        utf16String.size()*sizeof(char16_t));
    cout << "\n";
    cout << "Utf-32 bytes: ";
    printHex(reinterpret_cast<pcuc>(utf32String.data()),
        utf32String.size()*sizeof(char32_t));
    cout << "\n";
    return 0;
}

Вывод:

Utf-8 bytes: f0 9f 8c b2 
Utf-16 bytes: 3c d8 32 df 
Utf-32 bytes: 32 f3 1 0 

Как и ожидалось напечатанные байты соответствуют действительности. Кстати, в коде выше я использовал новые псевдонимы для std::basic_string<T>: std::u16string и std::u32string. Их использование позволило узнать реальный размер строки. В отличие от utf-32, utf-16 и utf-8, в нашем примере, содержат более одного базового элемента поэтому нам обязательно нужно было знать реальный размер. В теории, компилятор мог бы “съесть” исходник в любой кодировке и преобразовать наши литералы исходя из кодировки файла, на практике же, я полагаю, все компиляторы ограничатся исходниками в приемлемой для них кодировке. Например, для примера выше  я использовал clang из XCode и он согласился проглотить исходник только в utf-8, от utf-16 он отказался. Можно это считать препятствием, но, по-сути это просто разумное ограничение. Препятствием будет, если clang будет использовать utf-8, а MSVC utf-16(gcc скорее всего тоже будет использовать utf-8), тогда о unicode литералах можно будет забыть, при кроссплатформенной разработке.

Литералы являются не единственным добавлением относящемся к unicode. Получение строки содержащей кодированный unicode это замечательно, но их ведь надо как-то использовать; что у нас есть для работы с unicode? А есть у нас, прямо скажем, крайне мало:

  • Новые фасеты для преобразования между добавленными кодировками unicode: std::codecvt_utf8<>, std::codecvt_utf16<> и std::codecvt_utf8_utf16<> 
  • std::wstring_convert – класс для перекодирования строки в одной кодировке в другую; использует вышеупомянутые фасеты.
  • std::wbuffer_convert – класс для создания потокового буфера, с помощью которого можно осуществлять операции ввода\вывода с автоматическим перекодированием; использует вышеупомянутые фасеты.

Пример:

using convertor = std::codecvt_utf8_utf16<wchar_t>;
std::string utf16ToUtf8(const std::wstring& utf16)
{
    std::wstring_convert<convertor, wchar_t> convert;
    return convert.to_bytes(utf16.c_str());
}

std::wstring utf8ToUtf16(const std::string& utf8)
{
    std::wstring_convert<convertor, wchar_t> utf16conv;
    return utf16conv.from_bytes(utf8.c_str());
}

В примере используется wchar_t, т.к. пример из Visual Studio где его размер 2 байта.

Новые фасеты позволяют выполнить конвертации следующих типов:

  • UTF-8 ↔ UCS-2 с использованием codecvt_utf8<char16_t> или codecvt_utf8<wchar_t> если sizeof(wchar_t) == 2;
  • UTF-8 ↔ UTF-32 с использованием codecvt_utf8<char32_t> или  codecvt_utf8<wchar_t> если sizeof(wchar_t) == 4;
  • UTF-16 ↔ UCS-2 с использованием codecvt_utf16<char16_t> или  codecvt_utf16<wchar_t> если sizeof(wchar_t) == 2;
  • UTF-16 ↔ UTF-32 с использованием codecvt_utf16<char32_t> или  codecvt_utf16<wchar_t> если sizeof(wchar_t) == 4;
  • UTF-8 ↔ UTF-16 с использованием codecvt_utf8_utf16<char16_t> или  codecvt_utf8_utf16<wchar_t> если sizeof(wchar_t) == 2;

Список был взять отсюда.

В целом это всё, что C++ представляет нам в плане поддержки unicode. Все эти специализации std::basic_string<> дают нам набор элементов определенного типа, эти строки не знают ничего о мульти-байтовых кодировках, поэтому их нормальное использование возможно только с utf-32 или ucs-2(которая не очень популярна в наши дни, мягко говоря), т.к. итерация идёт поэлементно, а не посимвольно. И это хорошо видно из кода выше, когда мы напечатали представление символа ёлки ‘🌲’ в utf-8, размер строки равен 4, а не 1 т.к. для std::string это просто набор байт. Но даже с utf-32 можно попасть впросак из-за того, что в некоторых языках один символ в одном регистре представлен 2 символами в другом.

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

Необработанные литералы

Еще одним новым типом литералов являются так называемые необработанные(raw) литералы.

В статьях на русском языке они могут так же встречаются как “сырые литералы”. Мне кажется, что “необработанные” звучит более однозначно, чем “сырые”, поэтому я буду использовать именно этот вариант перевода.

Суть необработанных литералов заключается в их имени – их содержимое не обрабатывается, т.е. воспринимается буквально. Что это значит? Это значит, что никакие escape-последовательности и прочие элементы, которые могут быть преобразованы из A в B не подвергаются этому преобразованию, а остаются как есть. Синтаксис создания литерала данного типа предельно прост: R“xxx()xxx”, где xxx это опциональный набор символов, размером не более 16, который позволяет уникально определить начало и конец нашего литерала. Этот идентификатор нужен лишь в одном случае – когда нам необходимо использовать скобки() внутри литерала, а следовательно просто R”()” не годится, т.к. непонятно где элемент синтаксиса, а где часть строки. Префикс R можно комбинировать с другими префиксами, так, к примеру, будет выглядеть необработанный литерал содержащий utf8: u8R”()”

Необработанные литералы, позволяют нам исполнить давнюю мечту многих C++ программистов: скопировать ASCII-картинку и отобразить её в консоли без дополнительных манипуляций:

#include <iostream>
using namespace std;

int main()
{
    const char* tree = R"===(
      *             ,
                       _/^\_
                      <     >
     *                 /.-.\         *
              *        `/&\`                   *
                      ,@.*;@,
                     /_o.I %_\    *
        *           (`'--:o(_@;
                   /`;--.,__ `')             *
                  ;@`o % O,*`'`&\ 
            *    (`'--)_@ ;o %'()\      *
                 /`;--._`''--._O'@;
                /&*,()~o`;-.,_ `""`)
     *          /`,@ ;+& () o*`;-';\
               (`""--.,_0 +% @' &()\
               /-.,_    ``''--....-'`)  *
          *    /@%;o`:;'--,.__   __.'\
              ;*,&(); @ % &^;~`"`o;@();         *
              /(); o^~; & ().o@*&`;&%O\
        jgs   `"="==""==,,,.,="=="==="`
           __.----.(\-''#####---...___...-----._
         '`         \)_`"""""`
                 .--' ')
               o(  )_-\
                 `"""` `
    )===";
    std::cout << tree;
};

Из примера выше можно заметить, что помимо того, что мы можем невозбранно использовать обратные слеши(\), так мы еще и не должны добавлять ‘\n’ к каждой строке, чтобы вывести наше художество правильно. Как не должны добавлять и ‘\’ символизируя, что следующая строка есть часть литерала и должна трактоваться соответствующе. Наоборот, всё трактуется буквально – каждый перевод строки им же в литерале и является и так с каждым символом литерала. Именно благодаря этому, мы можем печатать ёлки в консоли без каких-либо проблем! Хотя мы и рассмотрели основную причину, почему вообще необработанные литералы были добавлены в C++(а в этом никто, я думаю, не сомневается), нужно упомянуть и другие сферы применения:

  • Регулярные выражения – кто пробовал писать регулярные выражения в C++, тот знает в какой ад может превратиться написание оных. Особенно когда в требуемом выражении встречаются обратные слеши(\); тем более, что регулярные выражения теперь часть стандартной библиотеки C++. Теперь с этим нет никаких проблем – используем необработанные литералы и просто копируем наше выражение оттуда, где мы тестировали его, без каких либо модификаций.
  • Встроенный код: код шейдеров, Java Script код, SQL и т.п. теперь легко интегрируется в исходник на C++, без всех этих дилемм: лучше сделать, чтобы было читаемо или побыстрей?

 

Да, необработанные литералы не добавили ничего принципиально нового в язык, но как же проще становится жизнь тех, кто вынужден использовать сложные литералы в коде.

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

Прежде чем говорить о пользовательских литералах необходимо вспомнить, а какие литералы вообще есть в C++:

  • Целочисленные литералы: 1, 1L, 1UL, 0xAE, 0777 и т.п.
  • Вещественные литералы: 1.25, 2.22f
  • Строковые литералы: “строка”, R”(необработанная строка)”, u”utf16 строка” и т.п.
  • Символьные литералы: ‘A’, ‘\n’ и т.п.

 

Хотя имеющиеся литералы позволяют охватить довольно широкий класс задач им присущ и соответствующий недостаток – они слишком общие. К примеру, чтобы представить некоторую физическую величину, скажем массу, мы можем использовать вещественный литерал. С другой стороны, если мы захотим использовать другую величину, скажем расстояние, мы тоже будем использовать вещественный литерал. В результате нам это даст возможность сложить расстояние с массой, что противоречит физическим законам нашего мира. Существует несколько решений данной проблемы, которые не требуют никаких нововведений в C++, но все они далеки от идеала.

Из-за подобных проблем и назрела необходимость создания более специализированных и безопасных способов создания объектов из литералов. Решить проблему пользовательских литералов решили добавлением нового оператора следующего вида:

Ret operator"" X(...);

Где Ret это возвращаемое значение, X – имя нового литерала и - его аргументы. Вышеприведенная нотация определяет литерал с именем X, который позволяет создавать различные объекты с той же лёгкостью с которой мы привыкли создавать литералы встроенных типов:

1001010100101b
"I'm a string"s
R"(I'm a raw string\n)"s
u"I'm an utf16 string"s
60s// seconds
"28/12/2013"_d
60seconds
20_minutes

Из вышеприведённых пользовательских литералов можно выделить следующие особенности:

  • Литерал может начинаться с подчёркивания(‘_’)
  • Литерал может начинаться с буквы.
  • Литерал может быть заключён в кавычки(“”)
  • Литерал заключенный в кавычки может использовать соответствующие префиксы
  • Литерал может быть свободен от кавычек.
  • Литерал определяется суффиксом.

 

Можно заметить, что новый оператор определяет суффикс литерала, который может быть использован для создания собственных литералов. Использовать префиксы, по прежнему, запрещено. В вышеописанных особенностях, имена литералов, начинающихся с подчёркивания и без оного, выделены в разные категории и это не случайно. Дело в том, что литералы начинающиеся с литеры запрещено использовать в пользовательском коде – они зарезервированы для разработчиков стандартной библиотеки. Пользователю разрешено использовать только имена литералов начинающиеся с подчёркивания. Это не техническое ограничение, но ему лучше следовать, дабы однажды не получить несовместимость со стандартной библиотекой. К тому же компиляторы могут(и скорее всего будут) выдавать предупреждение.

Кроме ограничений на имя есть еще ограничение на аргументы и это уже техническое ограничение. Аргументами operator”” могут быть следующие типы:

  • operator”” X(unsigned long long)
  • operator”” X(long double)
  • operator”” X(const CHARACTER*)
  • operator”” X(CHARACTER)
  • operator”” X(const CHARACTER*, size_t)
  • template<char…> operator”” X()

 

Где CHARACTER может быть одним из следующих типов: char, char16_t, char32_t и wchar_t. Когда компилятор встречает некоторый литерал, он разбирает его и смотрит на его суффикс. Именно суффикс определяет как будет интерпретироваться то, что было до этого разобрано. В случае с пользовательским суффиксом будет вызван один из operaator””, который ищется среди всех видимых операторов. Т.к. у нас нет никаких пользовательских аргументов у operator””, то и аргументо-зависимый поиск нам не доступен, а это значит, что поиск проходит только среди тех операторов, к которым можно обратиться без квалифицирующего префикса(сиречь, напрямую). При этом, если имя пользовательского литерала совпадает с одним из стандартных суффиксов(например f), у последнего есть приоритет.

Внимательный читатель, я думаю, обратил внимание, что для целочисленной константы используется беззнаковый тип. А что же делать с отрицательными величинами? Всё дело в том, что знак минус(-) не является частью литерала, а применяется к возвращенному из operator”” объекту. Именно поэтому нет никакой нужды иметь отдельную сигнатуру для знаковых типов.

Т.к. мы имеем дело с обычным оператором, то мы имеем дело и с перегрузкой. Перегрузку operator”” можно разделить на вертикальную и горизонтальную, под вертикальной понимается перегрузка в одной категории, тогда как под горизонтальной будет пониматься перегрузка между категориями. Так, к одной категории относится целочисленный литерал с пользовательским суффиксом без кавычек. В рамках данной категории будет иметь место следующая вертикальная перегрузка:

  1. operator”” X(unsigned long long)
  2. operator”” X(const char*)
  3. template<char…> operator”” X()

Вертикальная и горизонтальная перегрузки это всего-лишь термины, введенный в рамках данной статьи. Так вертикальную перегрузку можно считать простым замещением где наивысший приоритет имеет оператор с сигнатурой находящейся выше в списке. Тут важен порядок.

Если у нас имеется вещественный литерал записанный без кавычек, тогда порядок “вертикальной перегрузки” будет следующим:

  1. operator”” X(long double)
  2. operator”” X(const char*)
  3. template<char…> operator”” X()

Если, всё же, литерал содержит двойные кавычки(“”), т.е. является строковым, то будет вызван operator”” X(const CHARACTER*, size_t), где CHARACTER будет зависеть от префикса литерала.

Если литерал содержит одинарный кавычки(‘’), то будет вызван operator”” X(CHARACTER), где CHARACTER будет зависеть от префикса литерала.

Горизонтальной перегрузкой будет тот факт, что мы можем использовать один и тот же суффикс для целочисленного, вещественного, строкового и символьного литералов и при этом иметь вызов различных операторов. Т.е. они друг друга не замещают, а имеет место истинная перегрузка.

Можно заметить, что мы имеем два схожих оператора с const char* в качестве первого аргумента, но в одном из них есть еще и size_t в качестве второго аргумента. Эти различия позволяют отделить строковые литералы от прочих; так, operator”” X(const CHARACTER*, size_t) вызывается только в случае присоединения пользовательского суффикса к строковому литералу. Тогда как operator”” X(const char*) вызывается только в случае присоединения пользовательского суффикса к не-строковому литералу.

Рассмотрим пример:

#include <iostream>
using namespace std;

void operator"" _lit(unsigned long long lit)
{
    cout << "ULL literal: " << lit << "\n";
}

void operator"" _lit(long double lit)
{
    cout << "double literal: " << lit << "\n";
}

void operator"" _lit(const char* lit, size_t)
{
    cout << "char*: " << lit << "\n";
}

void operator"" _lit(const wchar_t* lit, size_t)
{
    wcout << "wchar_t*: " << lit << "\n";
}

void operator"" _lit(const char16_t* lit, size_t)
{
    cout << "char16_t*\n";
}

void operator"" _lit(const char32_t* lit, size_t)
{
    cout << "char32_t*\n";
}

void operator"" _lit(char lit)
{
    cout << "char: " << lit << "\n";
}

void operator"" _lit(wchar_t lit)
{
    wcout << "wchar_t: " << lit << "\n";
}

void operator"" _lit(char16_t lit)
{
    cout << "char16_t\n";
}

void operator"" _lit(char32_t lit)
{
    cout << "char32_t\n";
}

void operator"" _clit(const char* clit)
{
    cout << "string literal: " << clit << "\n";
}


template<char... characters>
void operator"" _tlit()
{
    auto chars = {characters...};
    cout << "template literal: ";
    for(auto c : chars)
        cout << c;
    cout << "\n";
}


int main() 
{
    123_lit;
    123.1_lit;
    "plain string"_lit;
    R"(\n\t\n)"_lit;
    L"wide string"_lit;
    u8"utf8" "s""tring"_lit;
    u"u16 string"_lit;
    U"U32 string"_lit;
    'A'_lit;
    u'A'_lit;
    U'A'_lit;
    L'A'_lit;
    42_clit;
    01010010101110_tlit;
    return 0;
}

Выведет:

ULL literal: 123
double literal: 123.1
char*: plain string
char*: \n\t\n
wchar_t*: wide string
char*: utf8string
char16_t*
char32_t*
char: A
char16_t
char32_t
wchar_t: A
string literal: 42
template literal: 01010010101110

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

#include <iostream>
using namespace std;

std::string operator"" _wrong(const char* lit, size_t)
{
    return {lit};
}

std::string operator"" _right(const char* lit, size_t n)
{
    return {lit, n};
}

int main()
{
    cout << "Size:" << "ABC\0\0CBA"_wrong.size() << "\n";
    cout << "Size:" << "ABC\0\0CBA"_right.size() << "\n";
    return 0;
}

Так, второй параметр позволяет узнать точный размер литерала и не зависеть от наличия нулей в оном.

В предыдущем листинге приведён пример литерала для std::string, правда с не подобающим суффиксом, для string закономерным суффиксом будет – s. Но есть еще одна компонента в стандартной библиотеке, которая заслуживает отдельного литерала, и чей суффикс тоже должен быть s - std::chrono::seconds. Благодаря тому факту, что секунды легко могут быть записаны посредством целочисленного литерала, а строка есть строка и должна быть записана строковым литералом мы можем иметь два одинаковых суффикса для абсолютно разных литералов:

#include <iostream>
#include <string>
#include <chrono>
#include <thread>

using namespace std;

string operator"" _s(const char* lit, size_t size)
{
    return {lit, size};
}

chrono::seconds operator"" _s(unsigned long long lit)
{
    return chrono::seconds{lit};
}

int main() 
{
    for(int i = 11; i != 0; --i)
    {
        cout << "Bang!"_s << endl;
        this_thread::sleep_for(1_s);
    }
    cout << "Happy New Year!"_s << endl;
    this_thread::sleep_for(20_s);

    return 0;
}

Всё это хорошо, но не хватает еще одного важного свойства встроенных литералов – все они вычисляются на этапе компиляции. Что же предлагают на этот счёт пользовательские литералы?

Вездесущий constexpr

Так как operator”” является обычной функцией, то чтобы позволить ему исполняться на этапе компиляции мы должны добавить к нему ключевое слово constexpr. Как вы можете помнить из предыдущей статьи,  в теле constexpr функции можно вызывать только constexpr функции.  Благо std::chrono::duration имеет constexpr конструктор и, следовательно, мы можем переписать наш оператор следующим образом:

constexpr chrono::seconds operator"" _s(unsigned long long lit)
{
    return chrono::seconds{lit};
}

С другой стороны, мы не можем сделать подобный оператор для string, т.к. у std::basic_string отсутствует constexpr конструктор. Имея вышеописанный operator””, при определенных условиях, наш литерал будет вычислен во время компиляции. Но что делать, если есть необходимость вычисления литерала именно на стадии компиляции, вне зависимости от контекста? Причины тут могут быть разные, но самая распространённая это желание исключить ошибки при написании литерала ещё на этапе компиляции. Есть только один способ гарантировать вычисление литерала на этапе компиляции – использовать шаблонную форму operator””,  а это значит, что возможность гарантировать статическое вычисление для строковых литералов не представляется возможным. Такая возможность есть только для вещественных и целочисленных литералов.

Для примера, давайте рассмотрим следующее задание: нам необходимо написать operator””, который будет принимать десятичное число и возвращать требуемый объект chrono::duration<>(chrono::seconds). При этом, мы не хотим, чтобы пользователь использовал при записи литерала какую-либо форму записи числа отличную от десятичной(т.е. восьмеричная и шестнадцатеричная система записи запрещены). Для реализации данного задания нам нужно написать разбор нашего литерала через рекурсивное развёртывание шаблонной пачки(подробнее можно почитать тут):

template<long N, unsigned long P>
struct Pow
{
    static const long long value = Pow<N, P - 1>::value * N;
};

template<long N>
struct Pow<N, 1>
{
    static const long long value = N;
};


template<char... digits>
struct DecimalDigitParser;

template<char digit>
struct DecimalDigitParser<digit>
{
    static const unsigned long long value = digit - '0';
};

template<char digit, char... trail>
struct DecimalDigitParser<digit, trail...>
{
    using CULL = const unsigned long long;
    static CULL current = (digit - '0')*Pow<10, sizeof...(trail)>::value;
    static CULL value = current + DecimalDigitParser<trail...>::value;
};

Также нам необходимо написать проверки для восьмеричной и шестнадцатеричной записи:

template<char digit, char... trail>
struct CheckOctal
{
    static const bool value = digit == '0' && sizeof...(trail) > 0;
};

template<char... digits>
struct CheckHex;

template<char first, char second, char... trail>
struct CheckHex<first, second, trail...>
{
    static const bool value = first == '0' 
        && (second == 'x' || second == 'X');
};

template<char first>
struct CheckHex<first>
{
    static const bool value = false;
};

Теперь добавим несколько вспомогательных функций, для более чистой записи:

template<char... digits>
constexpr bool IsHex()
{
    return CheckHex<digits...>::value;
}

template<char... digits>
constexpr bool IsOctal()
{
    return CheckOctal<digits...>::value;
}

template<char... lits>
constexpr unsigned long long ParseDecimalLiteral()
{
    return DecimalDigitParser<lits...>::value;
}

Наконец сводим всё в единый, вспомогательный класс для гарантированного вычисления на этапе компиляции:

template<char... lits>
struct Seconds
{
    static_assert(!IsHex<lits...>(), 
        "Hex notation is not allowed, use decimal instead");
    static_assert(!IsOctal<lits...>(),
        "Octal notation is not allowed, use decimal instead");
    using CULL = const unsigned long long;
    static CULL value = ParseDecimalLiteral<lits...>();
};

После чего мы можем написать итоговый оператор:

template<char... lits>
constexpr chrono::seconds operator"" _s()
{
    return chrono::seconds{Seconds<lits...>::value};
}

int main()
{
    1234567890_s;
    0777_s;//Ошибка
    0xab77_s;//Ошибка
    return 0;
}

Вот мы и получили литерал, с обязательной проверкой на этапе компиляции и вменяемыми сообщениями об ошибках, если таковые случились. Может показаться, что много кода для такой простой задачи, но, на самом деле, это всё пишется довольно быстро и просто. Делать так или нет - это уже решать вам, но то, что у нас есть возможность реализации литералов полностью идентичных встроенным не может не радовать.

Ожидаемые изменения

Так как выход обновлённого стандарта, также известного как C++14, уже не за горами, хотелось бы упомянуть и о тех изменениях, которые мы скорее всего увидим в нём. Естественно, речь пойдёт только об изменениях связанных с литералами.

 

Бинарные литералы

В C++14 появляются бинарные литералы. Вообще их отсутствие вызывало много вопросов и многие, кто дорвался до пользовательских литералов, в первую очередь, стали писать свои варианты бинарных литералов. Новый стандарт устраняет это недоразумение и бинарные литералы пополняют строй 8-х, 10-х и 16-х собратьев. Синтаксис записи бинарного литерала похож на шестнадцатеричный, за тем исключение, что в бинарном литерале используется литера b или B:

0b101010101111
0B1111010101001

Единственным недостатком нового литерала является его ограниченная длина. Он не может быть длиннее, чем количество бит вмещающееся в long long. Поэтому, всё-же, пользовательские бинарные литералы могут кому-то и пригодится.

Литералы для стандартной библиотеки

Следующие литералы(суффиксы) должны быть добавлены в C++14

  • s для basic_string
  • ns для chrono::nanoseconds
  • us для chrono::microseconds
  • s для chrono::seconds
  • min для chrono::minutes
  • h для chrono::hours

Думаю, что это пробный заход и дальше мы увидим еще больше новых литералов. Главное не переборщить, потому что желающих добавить “самый нужный литерал” хоть отбавляй.  Правда, надо признать, что те, что добавят первыми действительно необходимы.

Разд��литель цифр

Ещё одним, весьма спорным, добавлением является появление разделителя для цифр. Вот как это будет выглядеть:

0xA'B'C;
1'000'000;
0b1111'0010'1010;

Естественно, эти одинарные кавычки игнорируются компилятором и служат только человеческому глазу. Теперь в коде, который пишут в разных частях света мы увидим и цифровые литералы разделенные по разным правилам? Изумительно.