Современный C++ принес нам кучу возможностей, которых раньше в языке остро не хватало. Чтобы хоть как-то получить подобный эффект на протяжении долгого времени изобретались потрясающие костыли, в основном состоящие из очень больших портянок шаблонов и макросов (зачастую еще и автогенеренных). Но и сейчас время от времени возникает потребность в возможностях, которых все еще нет в языке. И мы начинаем снова изобретать сложные конструкции из шаблонов и макросов, генерировать их и достигать нужного нам поведения. Это как раз такая история.
За последние пол-года мне дважды понадобились значения, которые можно было бы использовать в параметрах шаблона. При этом хотелось иметь человеко-читаемые имена для этих значений и исключить необходимость в объявлении этих имен заранее. Конкретные задачи, которые я решал — отдельный вопрос, возможно позже я еще напишу про них отдельные посты, где-нибудь в хабе «ненормальное программирование». Сейчас же я расскажу о подходе, которым я решал эту задачу.
Итак, когда речь идет о параметрах шаблонов, мы можем использовать либо тип, либо static const значение. Для большинства задач этого более-чем достаточно. Хотим использовать в параметрах человеко-читаемые идентификаторы — объявляем структуру, перечисление или константу и используем их. Проблемы начинаются тогда, когда мы не можем заранее определить этот идентификатор и хотим сделать это на месте.
Можно было бы задекларировать структуру или класс прямо в параметре шаблона. Это даже будет работать, если шаблон не будет делать с этим параметром чего-либо, что требует полного описания структуры. К тому же, мы не можем контролировать пространство имен, в котором декларируется такая структура. И полностью одинаковые на вид подстановки шаблонов будут превращаться в совершенно разный код, если эти строчки находятся в соседних классах или пространствах имен.
Нужно использовать литералы, а из всех литералов в C++ читаемыми можно назвать только символьный литерал и строковой литерал. Но символьный литерал ограничен четырьмя символами (при использовании char32_t), а строковой литерал является массивом символов и его значение нельзя передать в параметры шаблона.
Получается какой-то замкнутый круг. Нужно либо объявлять что-то заранее, либо использовать неудобные идентификаторы. Попробуем добиться от языка того, к чему он не приспособлен. Что если имплементировать макрос, который сделает из строкового литерала что-то пригодное для использования в аргументах шаблона?
Для начала сделаем основу для строки. В C++11 появились variadic template arguments.
Объявляем структуру, которая в аргументах содержит символы строки:
github
Это работает. Мы даже можем сразу использовать такие строки примерно вот так:
Отлично. Было бы не плохо уметь доставать значение этой строки в рантайме. Пусть будет дополнительная шаблонная структура, которая будет извлекать аргументы из такой строки и делать из них константу:
Это тоже работает. Так как строки у нас не содержат '\0' на конце, нужно достаточно аккуратно оперировать с этой константой (лучше, на мой взгляд, сразу создавать string_view используя в аргументах конструктора константу и sizeof от нее). Можно было бы просто добавить '\0' в конце массива, но для моих задач это не нужно.
Ладно, что еще можно делать с такими строками? Например конкатенировать:
github
В принципе, можно сделать боле-менее любую операцию (я не пробовал, так как мне не нужно, но примерно представляю, как можно сделать поиск подстроки или даже замену подстроки).
Теперь у нас остался главный вопрос, как в compile-time извлечь символы из строкового литерала и положить их в аргументы шаблона.
Начнем со способа положить символы в аргументы шаблона по одному:
github
Я использую отдельную специализацию для символа '\0', чтобы не добавлять его в используемую строку. К тому же, это несколько упрощает другие части макроса.
Хорошая новость — строковой литерал может быть параметром constexpr функции. Напишем функцию, которая вернет символ по индексу в строке либо '\0', если длина строки меньше, чем индекс (вот тут пригодится специализация PushBackCharacter для символа '\0').
В принципе, мы уже можем писать нечто вроде этого:
Помещаем такую портянку, да подлиньше (мы же умеем писать скрипты для генерации кода) внутрь нашего макроса, и все!
Есть нюанс. Если количество символов в строке окажется больше, чем уровней вложенности в макросе, строчка просто обрежется и мы этого даже не заметим. Непорядок.
Сделаем еще одну структуру, которая никак не преобразовывает поступившую в нее строку, но делает static_assert, что ее длина не превышает константу:
Ну и макрос будет выглядеть примерно вот так:
github
Реализацию, которая получилась у меня, можно найти на гитхабе.
Мне было бы очень интересно услышать о возможных применениях этого механизма, отличных от тех, что придумал я.
За последние пол-года мне дважды понадобились значения, которые можно было бы использовать в параметрах шаблона. При этом хотелось иметь человеко-читаемые имена для этих значений и исключить необходимость в объявлении этих имен заранее. Конкретные задачи, которые я решал — отдельный вопрос, возможно позже я еще напишу про них отдельные посты, где-нибудь в хабе «ненормальное программирование». Сейчас же я расскажу о подходе, которым я решал эту задачу.
Итак, когда речь идет о параметрах шаблонов, мы можем использовать либо тип, либо static const значение. Для большинства задач этого более-чем достаточно. Хотим использовать в параметрах человеко-читаемые идентификаторы — объявляем структуру, перечисление или константу и используем их. Проблемы начинаются тогда, когда мы не можем заранее определить этот идентификатор и хотим сделать это на месте.
Можно было бы задекларировать структуру или класс прямо в параметре шаблона. Это даже будет работать, если шаблон не будет делать с этим параметром чего-либо, что требует полного описания структуры. К тому же, мы не можем контролировать пространство имен, в котором декларируется такая структура. И полностью одинаковые на вид подстановки шаблонов будут превращаться в совершенно разный код, если эти строчки находятся в соседних классах или пространствах имен.
Нужно использовать литералы, а из всех литералов в C++ читаемыми можно назвать только символьный литерал и строковой литерал. Но символьный литерал ограничен четырьмя символами (при использовании char32_t), а строковой литерал является массивом символов и его значение нельзя передать в параметры шаблона.
Получается какой-то замкнутый круг. Нужно либо объявлять что-то заранее, либо использовать неудобные идентификаторы. Попробуем добиться от языка того, к чему он не приспособлен. Что если имплементировать макрос, который сделает из строкового литерала что-то пригодное для использования в аргументах шаблона?
Сделаем структуру для строки
Для начала сделаем основу для строки. В C++11 появились variadic template arguments.
Объявляем структуру, которая в аргументах содержит символы строки:
template <char... Chars>
struct String{};
github
Это работает. Мы даже можем сразу использовать такие строки примерно вот так:
template <class T>
struct Foo {};
Foo<String<'B', 'a', 'r'>> foo;
А теперь протащим эту строку в рантайм
Отлично. Было бы не плохо уметь доставать значение этой строки в рантайме. Пусть будет дополнительная шаблонная структура, которая будет извлекать аргументы из такой строки и делать из них константу:
template <class T>
struct Get;
template <char... Chars>
struct Get<String<Chars...>> {
static constexpr char value[] = { Chars... };
};
Это тоже работает. Так как строки у нас не содержат '\0' на конце, нужно достаточно аккуратно оперировать с этой константой (лучше, на мой взгляд, сразу создавать string_view используя в аргументах конструктора константу и sizeof от нее). Можно было бы просто добавить '\0' в конце массива, но для моих задач это не нужно.
Проверим, что можем манипулировать такими строками
Ладно, что еще можно делать с такими строками? Например конкатенировать:
template <class A, class B>
struct Concatenate;
template <char... Chars, char... ExtraChars...>
struct Concatenate<String<Chars...>, String<ExtraChars...>> {
using type = String<Chars..., ExtraChars...>;
};
github
В принципе, можно сделать боле-менее любую операцию (я не пробовал, так как мне не нужно, но примерно представляю, как можно сделать поиск подстроки или даже замену подстроки).
Теперь у нас остался главный вопрос, как в compile-time извлечь символы из строкового литерала и положить их в аргументы шаблона.
Дорисовываем совуПишем макрос
Начнем со способа положить символы в аргументы шаблона по одному:
template <class T, char c>
struct PushBackCharacter;
template <char... Chars, char c>
struct PushBackCharacter<String<Chars...>, c> {
using type = String<Chars..., c>;
};
template <char... Chars>
struct PushBackCharacter<String<Chars...>, '\0'> {
using type = String<Chars...>;
};
github
Я использую отдельную специализацию для символа '\0', чтобы не добавлять его в используемую строку. К тому же, это несколько упрощает другие части макроса.
Хорошая новость — строковой литерал может быть параметром constexpr функции. Напишем функцию, которая вернет символ по индексу в строке либо '\0', если длина строки меньше, чем индекс (вот тут пригодится специализация PushBackCharacter для символа '\0').
template <size_t N>
constexpr char CharAt(const char (&s)[N], size_t i) {
return i < N ? s[i] : '\0';
}
В принципе, мы уже можем писать нечто вроде этого:
PushBackCharacter<
PushBackCharacter<
PushBackCharacter<
PushBackCharacter<
String<>,
CharAt("foo", 0)
>::type,
CharAt("foo", 1)
>::type,
CharAt("foo", 2)
>::type,
CharAt("foo", 3)
>::type
Помещаем такую портянку, да подлиньше (мы же умеем писать скрипты для генерации кода) внутрь нашего макроса, и все!
Есть нюанс. Если количество символов в строке окажется больше, чем уровней вложенности в макросе, строчка просто обрежется и мы этого даже не заметим. Непорядок.
Сделаем еще одну структуру, которая никак не преобразовывает поступившую в нее строку, но делает static_assert, что ее длина не превышает константу:
#define _NUMBER_TO_STR(n) #n
#define NUMBER_TO_STR(n) _NUMBER_TO_STR(n)
template <class String, size_t size>
struct LiteralSizeLimiter {
using type = String;
static_assert(size <= MAX_META_STRING_LITERAL_SIZE,
"at most " NUMBER_TO_STR(MAX_META_STRING_LITERAL_SIZE)
" characters allowed for constexpr string literal");
};
#undef NUMBER_TO_STR
#undef _NUMBER_TO_STR
Ну и макрос будет выглядеть примерно вот так:
#define MAX_META_STRING_LITERAL_SIZE 256
#define STR(literal) \
::LiteralSizeLimiter< \
::PushBackCharacter< \
... \
::PushBackCharacter< \
::String<> \
, ::CharAt(literal, 0)>::type \
... \
, ::CharAt(literal, 255)>::type \
, sizeof(literal) - 1>::type
github
Получилось
template <class S>
std::string_view GetContent() {
return std::string_view(Get<S>::value, sizeof(Get<S>::value));
}
std::cout << GetContent<STR("Hello Habr!")>() << std::endl;
Реализацию, которая получилась у меня, можно найти на гитхабе.
Мне было бы очень интересно услышать о возможных применениях этого механизма, отличных от тех, что придумал я.