Современный C++ принес нам множество возможностей, которых раньше в этом языке катастрофически не хватало.
Чтобы хоть как-то добиться подобного эффекта, уже давно придуманы удивительные костыли, состоящие в основном из очень больших портянок шаблонов и макросов (зачастую еще и автогенерируемых).
Но даже сейчас время от времени возникает потребность в возможностях, которых еще нет в языке.
И мы начинаем изобретать сложные конструкции из шаблонов и макросов, генерировать их и добиваться нужного нам поведения.
Это как раз такая история.
За последние полгода мне дважды потребовались значения, которые можно было бы использовать в параметрах шаблона.
При этом мне хотелось иметь удобочитаемые имена для этих значений и исключить необходимость объявлять эти имена заранее.
Конкретные задачи, которые я решал, это отдельный вопрос; возможно, позже я напишу о них отдельные посты, где-нибудь в хабе «ненормального программирования».
Теперь я расскажу вам о подходе, который я использовал для решения этой проблемы.
Итак, когда дело доходит до параметров шаблона, мы можем использовать либо тип, либо статическое константное значение.
Для большинства задач этого более чем достаточно.
Если мы хотим использовать в параметрах удобочитаемые идентификаторы, мы объявляем структуру, перечисление или константу и используем их.
Проблемы начинаются, когда мы не можем заранее определить этот идентификатор и хотим сделать это на месте.
Можно было бы объявить структуру или класс непосредственно в параметре шаблона.
Это будет работать даже в том случае, если шаблон не делает с этим параметром ничего, требующего полного описания структуры.
Кроме того, мы не можем контролировать пространство имен, в котором объявлена такая структура.
А совершенно одинаковые на вид замены шаблонов превратятся в совершенно другой код, если эти строки будут находиться в соседних классах или пространствах имен.
Вам необходимо использовать литералы, а из всех литералов в C++ только символьный литерал и строковый литерал можно назвать читаемыми.
Но символьный литерал ограничен четырьмя символами (при использовании char32_t), а строковый литерал представляет собой массив символов, и его значение нельзя передать в параметры шаблона.
Получается какой-то порочный круг.
Либо нужно что-то объявлять заранее, либо использовать неудобные идентификаторы.
Попробуем добиться от языка того, для чего он не приспособлен.
Что, если вы реализуете макрос, который превращает строковый литерал во что-то, что можно использовать в аргументах шаблона?
Давайте создадим структуру для строки
Сначала давайте создадим основу для линии.В C++11 представлены аргументы шаблона с переменным числом аргументов.
Мы объявляем структуру, которая содержит в своих аргументах строковые символы:
github Оно работает. Мы даже можем сразу использовать такие строки:template <char. Chars> struct String{};
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 В принципе, можно проделать более-менее любую операцию (не пробовал, потому что мне это не нужно, но примерно представляю, как можно найти подстроку или даже заменить подстроку).
Теперь у нас остался главный вопрос: как извлечь символы из строкового литерала во время компиляции и поместить их в аргументы шаблона.
Заканчиваем рисовать сову Написание макроса
Начнем со способа поочередного помещения символов в аргументы шаблона: 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;
Реализация, которую я получил, может быть найти на github .
Мне было бы очень интересно услышать о возможных приложениях этого механизма помимо того, что я придумал.
Теги: #C++ #constexpr #c++14 #строки
-
Должен Ли Msp Стать Mssp?
19 Oct, 24 -
Жизнь Гис В Облаке: Не Отказывайтесь Сразу
19 Oct, 24