Как стать автором
Обновить

Помещаем строки в параметры шаблонов

Время на прочтение5 мин
Количество просмотров9.6K
Современный C++ принес нам кучу возможностей, которых раньше в языке остро не хватало. Чтобы хоть как-то получить подобный эффект на протяжении долгого времени изобретались потрясающие костыли, в основном состоящие из очень больших портянок шаблонов и макросов (зачастую еще и автогенеренных). Но и сейчас время от времени возникает потребность в возможностях, которых все еще нет в языке. И мы начинаем снова изобретать сложные конструкции из шаблонов и макросов, генерировать их и достигать нужного нам поведения. Это как раз такая история.

За последние пол-года мне дважды понадобились значения, которые можно было бы использовать в параметрах шаблона. При этом хотелось иметь человеко-читаемые имена для этих значений и исключить необходимость в объявлении этих имен заранее. Конкретные задачи, которые я решал — отдельный вопрос, возможно позже я еще напишу про них отдельные посты, где-нибудь в хабе «ненормальное программирование». Сейчас же я расскажу о подходе, которым я решал эту задачу.

Итак, когда речь идет о параметрах шаблонов, мы можем использовать либо тип, либо 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;

Реализацию, которая получилась у меня, можно найти на гитхабе.

Мне было бы очень интересно услышать о возможных применениях этого механизма, отличных от тех, что придумал я.
Теги:
Хабы:
Всего голосов 22: ↑22 и ↓0+22
Комментарии29

Публикации

Истории

Работа

Программист C++
105 вакансий
QT разработчик
4 вакансии

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань