(Продолжение. См. первую часть, где мы научились кодить на марковских алгорифмах «на бумажке»).
Сокращения:
-
НАМ — нормальные алгорифмы Маркова
-
КТ — компайл-тайм
-
РТ — рантайм
Какие неприятности нас ждут?
Я уже сказал, что реализация НАМ в КТ — это задача со звёздочкой.
Что нам придётся героически преодолеть, и о чём понятно прямо на старте?

Первое и самое главное: строки
Хотя std::string можно использовать в КТ, но с оговорками. Выделение памяти не может пересекать границу КТ-РТ.
constexpr std::string make(bool small) { if (small) return "short"; // small object optimization else return "long enough string that needs allocation"; // new char[]}constexpr auto s = make(true);constexpr auto l = make(false); // ошибка компиляцииstatic_assert(make(true) != "");static_assert(make(false) != ""); // ок. внутри контекста КТ есть свой менеджер кучи
Можно попробовать запихнуть всю НАМ-машину, вместе с программой и данными, внутрь constexpr-функции, как-то
constexpr auto run(std::string data) { // внутренние объекты, хоть с аллокацией, хоть без... const auto program = RULES(RULE("a", "b"), FINAL_RULE("c", "D"), RULE("e", "f")); const auto machine = MACHINE(program); // результат - наверняка с аллокацией std::string result = machine(data); return result;}
Ну и что мы с этим поделаем? Только в static_assert проверим? А если попытаемся записать в константу, то из-за аллокации она будет не constexpr, и мы просто получим обычный РТ-вызов, пусть и до main().
Так что нам нужны такие строки, которые были бы честными КТ.
Но эти строки — из-за того, что у них разная длина, — не могут быть однотипными. Поэтому и правила поиска-замены, параметризованные этими строками, — тоже не однотипны.
Отсюда вытекает следующая неприятность.
Второе: циклы над разнотипными переменными
Опять же, в constexpr-функциях есть и for, и while. Но, как и в РТ, они работают с переменными фиксированного типа и с однородными контейнерами.
constexpr int table[] = {1, 2, 3, 4, 5};constexpr int sum() { int s = 0; for (auto t : table) s += t; return s;}constexpr int countdown(int n) { int s = 0; while (n--) s += n; return s;}// результаты вычислений - честный КТ, и мы можем применять их, например, в типах// (здесь - в длинах массивов)int a[sum()];int b[countdown(10)];
А программа НАМ, составленная из правил с неоднородными строками, — очевидно, неоднородная.
Поэтому вместо циклов придётся использовать рекурсивные функции. И не только!
Из этого следует
Третье: глубина рекурсии
Рекурсия в КТ — это ценный ресурс компилятора. Будь то непосредственная работа по выведению типа шаблона, или рекурсивная constexpr-функция
template<int N> struct deep_type : deep_type<N-1> {};template<> struct deep_type<0> {};constexpr int deep_call(int n) { if (n == 0) return 0; else return deep_call(n-1);}// доиграемся до ошибки компилятора (и спасибо, что не до ICE)using D = deep_type<1000>;constexpr int d = deep_call(1000);
Да, у компиляторов есть разные флаги, регулирующие эту глубину, — например, у gcc это -fconstexpr-depth=N и -ftemplate-depth=N . Если задать значения побольше, то вместо диагностики рискуем переполнить стек.
Так что сразу озаботимся техниками развёртывания циклов.
Последнее: смешивание КТ и РТ
Но это уже совсем детская проблема. Если мы напишем систему constexpr-функций, то вызвать их в РТ и с рантаймовыми аргументами (сохраняющими логику инстанцирования) будет несложно. Надо только продумать точки кастомизации.
А отобразить КТ-строки в РТ — это вообще просто. Но об этом ниже.
Неочевидное: система типов
То, что у нас будут разнотипные строки, уже понятно. И разнотипные правила (каждое элементарное правило определяется парой строк, а каждая подпрограмма — набором правил, из которых она состоит). И — это я уже анонсирую — разнотипная аугментация данных более-менее произвольной пользовательской нагрузкой.
Да и всякие служебные типы тоже, конечно же, шаблонные.
Конечно, язык шаблонов C++ необычайно слабо типизирован, почти как лямбда-исчисление, но это, с одной стороны, даёт гибкость, а с другой — нечитаемость («что значит T? а что значит auto?») и ошибкоопасность (а ошибки в коде времени компиляции обычно приводят к гигантским портянкам диагностики).
Тут у нас есть три пути:
-
писать везде auto / class и длинные_мнемонические_идентификаторы, и блюсти чистоту рук
-
разворачивать формальные параметры шаблонов
-
концепты
// пусть шаблон класса и шаблон функции имеют дело с типами, инстанцированными из шаблонаtemplate<size_t N> struct str { ..... };// первый путьtemplate<class S> struct foo { ..... };auto bar(auto s) { ..... }// второй путьtemplate<class> struct foo;template<size_t N> struct foo<str<N>> { using S=str<N>; ..... };template<size_t N> auto bar(str<N> s) { ..... }// третий путьtemplate<class T> concept Str = .....; // true только для str<N>template<Str S> struct foo { ..... };Str auto bar(Str s) { ..... } // заодно и тип результата можем уточнить до str<N1>
Поскольку циклы зависят от строк, строки — от типов, а типы — от концептов, — котёнок по имени Гав начнёт спуск на первую ступеньку именно с них.
Концепты
Концептов понадобится много. Причём сами по себе концепты тоже систематизированы. Чтобы не писать одинаковый код руками, я сделаю набор макросов.
Концепт задаёт семейство типов. Обычно это
-
«тип T принадлежит семейству F»
-
«тип T является обёрткой W над типом U»
-
причём тип U сам по себе может не быть конкретным, а относиться к некоему семейству.
-
Так как у меня пользовательские типы — это разнообразные структуры, то удобно описывать семейство типов в связке: объявление концепта + пометка структуры, как принадлежащей семейству этого концепта.
CONCEPT(FooBar);// обявляет концепт FooBar и необходимую обвязку для него,// в частности, метафункцию is_FooBar, которая понадобится нижеstruct foo { REPRESENTS(FooBar)};struct bar { REPRESENTS(FooBar)};
Если структура шаблонная, то она сама уже задаёт семейство типов — воплощений шаблона. Но в C++ нет красивого синтаксиса «из коробки» проверить этот факт. Есть некрасивый синтаксис со специализациями.
template<class U> struct buz {.....}; // семейство buztemplate<class U> void f(buz<U> arg);template<class T> struct xyz;template<class U> struct xyz<buz<U>> {.....};
Естественно, можем руками написать концепт
// специализация спрятана под капотtemplate<class T> constexpr bool is_Buz_v = false;template<class U> constexpr bool is_Buz_v<buz<U>> = true;template<class T> concept Buz = is_Buz_v<T>;
или что-то в таком роде.
Но потом захотим прикрутить туда проверку BuzOfType<bar> и даже BuzOfFamily<FooBar>…
Чтобы избавиться от такой однотипной работы, — есть, опять же, набор макросов и конвенций.
CONCEPT_WITH_TYPE(Buz)// объявил сразу три концепта// Buz<T>// BuzOfType<T, U> где U - конкретный тип// BuzOfTraits<T, C> где C - метафункция проверки свойств типаtemplate<class T> struct buz { REPRESENTS(Buz); using type = T; // CONCEPT_WITH_TYPE требует, чтобы параметр T был записан в имя type};using buz_of_foo = buz<foo>;static_assert(Buz<buz_of_foo>);static_assert(BuzOfType<buz_of_foo, foo>); // проверка U == foostatic_assert(BuzOfTraits<buz_of_foo, is_FooBar>); // проверка is_FooBar<foo>
Тут стоит сказать,
Как можно параметризовать концепты
Концепт сам по себе является неполноценной сущностью. Мы можем применять концепт к типу плюс дополнительным параметрам, — то есть, это такой вид булевой константы, но не можем использовать в качестве параметра шаблона (в том числе параметра концепта).
И вообще, в качестве параметра шаблона можно использовать
-
типы (в том числе алиасы)
-
константы
-
шаблоны типов (в том числе шаблоны алиасов)
Шаблоны констант (is_FooBar_v) — нельзя. А тем более, концепт (FooBar).
Но в метапрограммировании давно уже известны несколько приёмов:
-
завернуть метафункцию внутрь конкретного типа-обёртки
-
завернуть её в шаблон алиаса, который подставит аргумент куда хочет как хочет
-
результатом будет не константа, а тип-обёртка константы, — тогда алиас резолвится в этот тип.
Что, собственно, и сделано
template<class T> concept FooBar = ......;// DEFINE_TYPECHECKER(FooBar)template<class T> constexpr bool is_FooBar_v = FooBar<T>;template<class T> using is_FooBar = std::bool_constant<FooBar<T>>;// соответственно, определения концептов BuzOf...template<class T, class U>concept BuzOfType = Buz<T> && HasTypeOfType<T, U>;template<class T, template<class>class C>concept BuzOfTraits = Buz<T> && HasTypeOfTraits<T, C>;// где вспомогательные концепты, проверяющие вложенный тип T::typetemplate<class T, class U>concept HasTypeOfType = std::same_as<typename std::remove_cvref_t<T>::type, U>;template<class T, template<class>class C>concept HasTypeOfTraits = C<typename std::remove_cvref_t<T>::type>::value;
Как населять концепт представителями
Сразу же поясню, почему выбрал дизайн с объявлением REPRESENTS внутри структуры.
По большому счёту, у нас есть два варианта — объявлять внутри и снаружи.
Снаружи кажется более универсальным (можно подселить к концепту произвольный тип, например, int). Но если структура шаблонная, то придётся делать детектор типа весьма многословным:
template<class T> constexpr bool is_FooBar_v = false; // по дефолтуtemplate<class T> concept FooBar = is_FooBar_v<T>;template<class X, int N, auto Something>struct ohoho { ..... };// частичная специализация детектораtemplate<class X, int N, auto Something>constexpr bool is_FooBar_v<ohoho<X, N, Something>> = true;
И там ещё кое-какие тонкости вылезают, даже связанные с возможностью нарушения ODR.
В частности, мы не можем использвать этот концепт внутри структуры, без ещё большей писанины.
А REPRESENTS устроен следующим образом (упрощённо)
// объявляем тэгstruct FooBar_concept_probe{};// объявляем концептtemplate<class T> concept FooBar = requires { std::remove_cvref_t<T>::represents_concept(FooBar_concept_probe{}); };// вносим метку в структуруtemplate<class X, int N, auto Something>struct ohoho { static void represents_concept(FooBar_concept_probe) {};};
Таким образом, можно объявлять одну и ту же структуру представителем нескольких семейств. Просто напишем внутри несколько REPRESENTS.
Кстати, обратите внимание, почему имя пробного тэга сделано именно с суффиксом, а не с префиксом. Дело в том, что концепты и их представители могут лежать в разных пространствах имён.
namespace ns1 { CONCEPT(FooBar) }namespace ns2 { struct foo { REPRESENTS(ns1::FooBar) };}
Впрочем, если всё-таки нужен концепт, населённый произвольными типами, то сделать это руками не составит сложности.
template<class T> concept ArbitraryFamily = /* делаем, что хотим */;DEFINE_TYPECHECKER(ArbitraryFamily)// определяет is_ArbitraryFamily для дальнейшей интеграции с моими концептами
Но и это ещё не всё, что связано с концептами.
Как поступать со ссылками?
Концепты встречаются в синтаксисе 4 способами:
-
в выражениях, как шаблон булевой константы
-
в requires, как уточнение типа результата
-
уточнение параметра-типа в шаблоне
-
уточнение auto-значения в шаблоне
static_assert(FooBar<foo> && !FooBar<int>);foo f();static_assert(requires{ {f()} -> FooBar; });// нельзя потребовать, что f возвращает именно foo, но можно - "тип, такой же, как foo"// вот зачем нужен same_as наряду с is_same_vstatic_assert(requires{ {f()} -> std::same_as<foo>; });template<FooBar T> struct foobarwrapper {};// эквивалентноtemplate<class T> requires FooBar<T> struct foobarwrapper {};void g(FooBar auto x, FooBar auto const& y, FooBar auto&& z);// эквивалентноtemplate<class X, class Y, class Z>auto g(X x, Y const& y, Z&& z)requires FooBar<X> && FooBar<Y> && FooBar<Z>;
Видите, где подвох? Если X и Y — это типы значения, то Z — универсальная ссылка.
И, чтобы наш шаблон FooBar работал, он должен принимать универсальную ссылку.
Правда, в таком случае ограничение на параметр foobarwrapper окажется слишком широким: помимо структур foo или bar туда можно будет подсунуть foo& или bar&&.
Ну, тут уж выходов два:
-
или заводить связанную пару концептов, скажем,
-
FooBarGeneric (всеядный, можно использовать с auto / decltype(auto))
-
FooBarValue (строго для типов foo, bar),
-
-
или оставить один и надеяться на чистоту рук и на дополнительные проверки
-
requires (!std::is_reference_v<T>) -
static_assert(!std::is_reference_v<T>)
-
Я пошёл вторым, простым путём. Из-за этого во вспомогательных концептах делается std::remove_cvref_t<T>.
На следующих ступеньках нас ждут строки… И новые неприятности со строками!
ссылка на оригинал статьи https://habr.com/ru/articles/1047886/