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

в 20:19, , рубрики: c++, C++14, constexpr, strings

Современный 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... };
};

Это тоже работает. Так как строки у нас не содержат '' на конце, нужно достаточно аккуратно оперировать с этой константой (лучше, на мой взгляд, сразу создавать string_view используя в аргументах конструктора константу и sizeof от нее). Можно было бы просто добавить '' в конце массива, но для моих задач это не нужно.

Проверим, что можем манипулировать такими строками

Ладно, что еще можно делать с такими строками? Например конкатенировать:

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...>, ''> {
  using type = String<Chars...>;
};

github

Я использую отдельную специализацию для символа '', чтобы не добавлять его в используемую строку. К тому же, это несколько упрощает другие части макроса.

Хорошая новость — строковой литерал может быть параметром constexpr функции. Напишем функцию, которая вернет символ по индексу в строке либо '', если длина строки меньше, чем индекс (вот тут пригодится специализация PushBackCharacter для символа '').

template <size_t N>
constexpr char CharAt(const char (&s)[N], size_t i) {
  return i < N ? s[i] : '';
}

В принципе, мы уже можем писать нечто вроде этого:

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.com/alex-ac/metastring
Мне было бы очень интересно услышать о возможных применениях этого механизма, отличных от тех, что придумал я.

Автор: alexac

Источник

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


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