Type Loopholes: решая нерешаемое. Рефлексия времени компиляции

—

от автора

Узнать, какие параметры принимает конструктор типа

  • Узнать, с какими шаблонными параметрами вызывался метод/функция с Как сделать метапрограммирование с типами более похожим на обычный код, где есть состояние

    Пример работы лупхолов:

    static_assert((std::ignore = Injector<0, 42>(), true)); static_assert(Magic(Getter<0>{}) == 42);

    А вот определение Injector и Getter:

    template <auto I> struct Getter {   friend constexpr auto Magic(Getter<I>); };  template <auto I, auto Value> struct Injector {   friend constexpr auto Magic(Getter<I>) {return Value;}; };
  • Принцип работы лупхолов
    1. В C++ можно объявлять дружественные функции:

    struct S {   friend auto F() -> void; };

    Но не всем известно, что их вместе с тем можно ещё и определять:

    struct S {   friend auto F() -> void {}; };

    Если объявление такой функции было доступно отдельно, то порядок поиска его сущности обычен:

    auto F() -> void;  struct S {   friend auto F() -> void {}; };  auto main() -> int {   F(); // Well Formed };
    1. Также функции можно объявлять без уточнения типа возвращаемого значения, используя auto:

    auto F();  auto main() -> int {   F(); // Ill Formed };

    Использовать такую функцию до уточнения этого типа запрещается.
    Уточнить этот тип допустимо последующим определением, где тип выведется из ретурна:

    auto F();  auto F() {   return 42; };  auto main() -> int {   F(); // Well Formed };

    3. Объединение оснований первого и второго пунктов дает такой пример:

    auto F();  struct S {   friend auto F() {     return 42;   }; };  auto main() -> int {   F(); // Well Formed };

    Теперь добавляем сюда шаблоны: дружественное определение инстанцируется (будет зарегистрировано компилятором) только с инстанцированием содержащего его шаблона. То-есть:

    auto F();  template <typename> struct S {   friend auto F() {     return 42;   }; };  auto A() -> void {   F(); // Ill Formed };  template struct S<void>;  auto B() -> void {   F(); // Well Formed };

    Проявилось то-самое состояние компиляции.

    4. Теперь можно пронаблюдать это состояние программно. Для этого нужно обеспечить для него зависимость. То-есть сделать валидность вызова неразрешимым без уточнения некоторой передаваемой информации, например аргумента:

    struct U {};  auto F(U);   template <typename> struct S {   friend auto F(U) {     return 42;   }; };

    Так мы исключаем возможность вычисления корректности выражения вызова в шаблоне без информации о передаваемом типе (которое иначе неизбежно бы произошло):

    template <typename T> constexpr bool kTest = requires {F(T{});};

    Не будь этого аргумента, kTest мог бы быть вычислен статически, т.е. requires { F(); } просто давал бы IF, поскольку с т.з. языка там написано нечто, что не сможет быть корректным никогда, на что тестировать ошибочно.
    Тем не менее, этот пример все еще не даст нам пронаблюдать состояние:

    static_assert(!kTest<U>); // passes template struct S<void>; static_assert(!kTest<U>); // passes again

    По причине мемоизации: kTest<U> всегда должны называть одну и ту же сущность, т.е. она не может быть различной по определению. Будучи false в первый раз, она обязана быть false и во второй.
    5. Остается лишь сделать так, чтобы одна и та же синтаксически конструкция (kTest<U>) называла разные сущности в разных контекстах. Мы используем то, как взаимодействуют лямбды и аргументы по умолчанию. Переопределяем kTest<>:

    template <typename T, auto = []{}> constexpr bool kTest = requires { F(T{}); };

    Тестируем:

    static_assert(!kTest<U>); // passes template struct S<void>; static_assert(kTest<U>); // passes again

    Реализация используемых функций, типов(не про лупхолы)
    template <auto I> struct Wrapper {};   template <typename... Ts> struct TypeList {};  template <typename T> struct TypeList<T> {   using Type = T; };   template <typename... Ts, typename... TTs> consteval auto operator==(const TypeList<Ts...>&, const TypeList<TTs...>&) -> bool {   return false; };  template <typename... Ts> consteval auto operator==(const TypeList<Ts...>&, const TypeList<Ts...>&) -> bool {   return true; }; template <typename... Ts> inline constexpr TypeList<Ts...> kTypeList;  namespace impl {  template <std::size_t I, typename T> struct IndexedType {};  template <typename...> struct Caster {};  template <std::size_t... Is, typename... Ts> struct Caster<std::index_sequence<Is...>, Ts...> : IndexedType<Is, Ts>... {};  } // namespace impl  template <std::size_t I, typename... Ts> consteval auto Get(TypeList<Ts...>) -> decltype(   []<typename T>(impl::IndexedType<I, T>&&) -> T { }(impl::Caster<std::index_sequence_for<Ts...>, Ts...>{}));

    Интроспекция входный параметров конструктора

    Мотивация

    При реализации классического Dependency Injection, нам нужно узнавать, от каких компонентов зависит наш компонент. В нём они указываются в конструкторе,

    struct SomeInterface {   virtual auto SomeFunction() -> int = 0; };  struct SomeInterface2 {   virtual auto SomeFunction2() -> void = 0; };  class SomeStruct { public:   SomeStruct(SomeInterface& some, SomeInterface2& other) :                    some(some),                    other(other) {     this->some.SomeFunction();   };  private:   SomeInterface& some;   SomeInterface2& other; };  static_assert(Reflect<SomeStruct>()                == kTypeList<SomeInterface, SomeInterface2>);

    Без лупхолов, такой код на чистом C++ был бы невозможен, т.к. в C++ не получится получить указатель на конструктор, как это возможно сделать с методом. Поэтому остаётся решать такое лупхолами.

    Как это работает

    Идея в том, чтобы через простую рекурсию в начале узнать кол-во входных параметров конструктора, а затем передать в конструктор N объектов, которые скастуются в необходимые для конструктора типы благодаря шаблонному оператору, а в операторе они запишут это в глобальное состояние.

    В начале нам нужно определить кол-во аргументов, для этого нам поможет простая структура SimpleCaster:

    struct SimpleCaster {   template <typename T>   constexpr operator T&&();    template <typename T>   constexpr operator T&(); };

    Далее используя эту структуру мы простой рекурсивной функций посмотрим кол-во аргументов. Если requires{T{(Is, SimpleCaster)…};} не сработало, то значит нужно увеличивать размер пака и так до тех пор, пока не найдёт нужный размер. 256 это верхний потолок, сколько можно будет делать аргументов. 0, 0, которое передаётся в GetArgsCount, это начальные значения, чтобы начинать оно искало с 2х аргументов и дальше, т.к. с одним аргументом оно работает только с агрегатами из-за того, что оно будет инстанцировать копи, мув конструкторы.

    template <typename T, std::size_t Max, std::size_t... Is> consteval auto GetArgsCountImpl() {   if constexpr(requires{T{(Is, SimpleCaster{})...};}) {     return sizeof...(Is);   } else {     static_assert(sizeof...(Is) != Max, "Not found counstructor");     return GetArgsCountImpl<T, Is..., 0>();   }; };  template <typename T, std::size_t Max = 256> consteval auto GetArgsCount() {   return GetArgsCountImpl<T, Max, 0, 0>(); };

    А теперь собственно сам класс, который будет записывать данные. Он записывает данные при вызове оператора преобразования.

    template <typename Main, auto I> struct Caster {   template <typename T, auto = Injector<TypeList<Main, Wrapper<I>>{}, TypeList<T>{}>{}>   constexpr operator T&&();     template <typename T, auto = Injector<TypeList<Main, Wrapper<I>>{}, TypeList<T>{}>{}>   constexpr operator T&();  };

    Ну и теперь нам осталось лишь вызвать конструктор с объектами типа Caster<T, Is>, а затем прочитать записанные данные. Внутри концепта лямбды с созданным паком Is мы добавляем данные в глобальный стейт, а в теле читаем. При этом остаётся возможность передать свой размер, если подход с GetArgsCount не справляется, или хочется ускорить компиляцию.

    template <typename T, std::size_t I = GetArgsCount<T>()> consteval auto Reflect() {   return [&]<auto... Is>(std::index_sequence<Is...>) requires requires {T{Caster<T, Is>{}...};} {     return TypeList<typename decltype(Magic(Getter<TypeList<T, Wrapper<Is>>{}>{}))::Type...>{};   }(std::make_index_sequence<I>()); };
    Что получилось
    #include <utility>  template <auto> struct Wrapper {};  template <typename... Ts> struct TypeList {};  template <typename T> struct TypeList<T> {   using Type = T; };  template <typename... Ts> consteval auto operator==(const TypeList<Ts...>&, const TypeList<Ts...>&) -> bool {   return true; };  template <typename... Ts, typename... TTs> consteval auto operator==(const TypeList<Ts...>&, const TypeList<TTs...>&) -> bool {   return false; };    template <typename... Ts> inline constexpr TypeList<Ts...> kTypeList;   template <auto I> struct Getter {   friend constexpr auto Magic(Getter<I>); };  template <auto I, auto Value> struct Injector {   friend constexpr auto Magic(Getter<I>) {return Value;}; };  template <typename Main, auto I> struct Caster {   template <typename T, auto = Injector<TypeList<Main, Wrapper<I>>{}, TypeList<T>{}>{}>   constexpr operator T&&();     template <typename T, auto = Injector<TypeList<Main, Wrapper<I>>{}, TypeList<T>{}>{}>   constexpr operator T&();  };   struct SimpleCaster {   template <typename T>   constexpr operator T&&();    template <typename T>   constexpr operator T&(); };  template <typename T, std::size_t Max, std::size_t... Is> consteval auto GetArgsCountImpl() {   if constexpr(requires{T{(Is, SimpleCaster{})...};}) {     return sizeof...(Is);   } else {     static_assert(sizeof...(Is) != Max, "Not found counstructor");     return GetArgsCountImpl<T, Is..., 0>();   }; };  template <typename T, std::size_t Max = 256> consteval auto GetArgsCount() {   return GetArgsCountImpl<T, Max, 0, 0>(); };    template <typename T, std::size_t I = GetArgsCount<T>()> consteval auto Reflect() {   return [&]<auto... Is>(std::index_sequence<Is...>) requires requires {T{Caster<T, Is>{}...};} {     return TypeList<typename decltype(Magic(Getter<TypeList<T, Wrapper<Is>>{}>{}))::Type...> {};   }(std::make_index_sequence<I>()); };

    Constexpr счётчик

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

    template <typename Tag, auto Value> struct TagWithValue {};  template <typename Tag = void, std::size_t I = 0> consteval auto CounterImpl() -> std::size_t {   if constexpr(requires{Magic(Getter<TagWithValue<Tag, I>{}>{});}) {     return CounterImpl<Tag, I + 1>();   };   return (std::ignore = Injector<TagWithValue<Tag, I>{}, 0>{}, I); };

    Такой подход работать не будет из-за того, что компиляторы C++ могут просто не вычислять значение после следующего вызова, а просто взять старое, из-за чего он и не будет работать.

    Решается это просто прокидыванием извне уникального пака Ts, который будет гарантироваться, что будет заново вычисляться значение.

    template <typename Tag, std::size_t I = 0, typename... Ts> consteval auto CounterImpl() -> std::size_t {   if constexpr(requires{Magic(Getter<TagWithValue<Tag, I>{}>{});}) {     return CounterImpl<Tag, I + 1, Ts...>();   };   return (std::ignore = Injector<TagWithValue<Tag, I>{}, 0>{}, I); };

    Ну и обернуть в интерфейс без I

    template <typename Tag = void, typename... Ts, auto R = CounterImpl<Tag, 0, Ts...>()> consteval auto Counter() -> std::size_t {   return R; };

    Проверяем:

    static_assert(Counter<void>() == 0); static_assert(Counter<void, int>() == 1); static_assert(Counter<void, void>() == 2);

    Но вот это работать не будет:

    static_assert(Counter() == 0); static_assert(Counter() == 1); static_assert(Counter() != Counter());

    Но это нужно не везде, а там где такое нужно, легко достигается нужного эффекта.

    Если вы инстанцируете шаблон функции Foo с параметром int, то в первый раз функция инстанцируется и как-бы добавится в конец кода, ей будет доступно всё что было до этого момента, но не то что после, а если снова попытаться её инстанцировать, то нового инстанса произведено не будет, а возьмётся тот, что уже есть выше.

    Такая особенность это пример того, как имплементаця использует более широкую гарантию. Если бы шаблоны инстанцировались в разное от одного набора аргументов, то template <typename Tag, std::size_t Index> struct GetUniqueKey {};

    Далее нужно реализовать саму функцию, для этого в начале нам нужно добавить в глобальный стейт нужные нам значения:

       ([]{       constexpr auto I = Counter<TypeList<Ts...>, Ts>();       std::ignore = Injector<GetUniqueKey<TypeList<Ts...>, I>{}, kTypeList<Ts>>{};     }(), ...);

    Ну и затем получить пак индексов для нашего нового пака типов, размер которого мы получаем от Counter с тем же тегом, но без параметров в Ts, а они там всегда были, поэтому мы гарантируем, что это будет новый инстанс и счётчик инкрементируется, а потому мы можем просто взять значение счётчика.

    return []<std::size_t... Is>(std::index_sequence<Is...>) { }(std::make_index_sequence<Counter<TypeList<Ts...>>()>());

    А затем мы просто читаем данные с помощью этого пака индексов, вот что получилось:

    template <typename... Ts> consteval auto GetUnique(TypeList<Ts...>) {   ([]{     constexpr auto I = Counter<TypeList<Ts...>, Ts>();     std::ignore = Injector<GetUniqueKey<TypeList<Ts...>, I>{}, TypeList<Ts>{}>{};   }(), ...);   return []<std::size_t... Is>(std::index_sequence<Is...>) {     return TypeList<typename decltype(Magic(Getter<GetUniqueKey<TypeList<Ts...>, Is>{}>{}))::Type...>{};   }(std::make_index_sequence<Counter<TypeList<Ts...>>()>()); };

    Интроспекция тел функций

    Мотивация

    Такое может пригодится при использовании паттерна Service Locator в Dependency Injection. С помощью такого можно определять зависимости классов, что пригодится для раннего диагностирования кольцевых зависимостей, применения топологической сорировки, или же при реализации параллельной инициализации компонентов.

    В случае с использованием Stackfull корутин, можно легко подождать инициализации зависимого класса прямо в методе GetComponent, но со Stackless из C++20, конструктор не может быть корутиной и приходится ждать заранее, но на тот момент нам были неизвестны зависимости класса.

    Решить это можно следующими путями:

    • Использовать кодогенерацию и парсеры C++(LLVM), но есть минусы:
      — Усложняется сборка
      — Сложно реализовывать, если в команде нет людей, что работали с этими вещами

    • Не интроспектировать и вынести информацию из тела

    Но с лупхолами есть ещё один способ, хоть и ограниченный. Про ограничения:

    • Всё что можно интроспектировать должно использовать входные шаблонные параметры и чтобы от них зависило, какая функция будет вызвана (ADL, методы)

    • Функция должна быть объявлена как constexpr(При этом мочь обязательно выполниться в constexpr не обязательно)

    • Можно инстроспектировать только то, что работает через шаблоны

    Вот пример как это будет работать с лупхолами:

    struct SomeImplA {   template <typename T, typename... Args>   auto Method(Args...) {}; // Интерфейс };  struct Foo {   constexpr Foo(auto& a) {     a.template Method<int>(42);     std::println("");   }; };  static_assert((Inject<Foo>(), 1));  static_assert(std::same_as<decltype(GetTFromMethod<Foo>()), TypeList<int>>);  static_assert(std::same_as<decltype(GetArgsFromMethod<Foo>()), TypeList<TypeList<int>>>);

    Вот что получится с Foo, если не использовать лупхолы, а вынести из класса:

    struct Foo {   static constexpr auto kMethodData = kTypeList<TypeList<int, int>>;   constexpr Foo(auto& a) {     a.template Method<int>(42);     std::println("");   }; };

    Минусы такого подхода очевидны: очень легок ошибиться. Человек может добавить вызов в конструктор, забыв про kMethodData. Проверка же подобного будет только на рантайме, а об ошибках хотелось бы знать до запуска рантайма. Это лишний бойлерплейт, чего можно избежать.

    Как это работает

    Работает оно следующим образом: мы подменяем то что ожидала получить функция своим объектом. Этот объект будет записывать информацию, которая появляется при вызове метода(T и Args…), чтобы затем её считать и выцепить из неё нужное.

    Для того чтобы сохранять информацию, нам понадобится ключ, с помощью которого мы будем сохранять значения

    template <typename Current, std::size_t I> struct InfoKey {};

    Затем реализация самого класса, который будет отвечать за отправку данных в глобальное состояние. Благодаря лямбде и той её особенности, что её тип всегда уникален, можно сделать, чтобы оно записывало даже тогда, когда типы не уникальны.

    Тут мы читаем эти T и Args…, а затем через счётчик смотрим, сколько данных уже было записано и куда нужно записать следующие, для этого мы передаём его тип(как тег) и считанные данные, чтобы было новое инстанцирование. А затем на это место через InfoKey мы записываем объединённые данные в виде T и Args… как список типов.

    template <typename Current, bool GetUnique = true> struct InfoInjector;  template <typename Current> struct InfoInjector<Current, true> {   template <     typename T,     typename... Args,     std::size_t I = Counter<Current, T, Args...>(),     //                T, Args... должно быть      auto = Injector<InfoKey<Current, I>{}, kTypeList<T, Args...>>{}  //  уникально, иначе не запишет   >   static auto Method(Args...) -> void; };  template <typename Current> struct InfoInjector<Current, false> {   template <     typename T,     typename... Args,     auto f = []{},     std::size_t I = Counter<Current, decltype(f)>(),      auto = Injector<InfoKey<Current, I>{}, kTypeList<T, Args...>>{}  // запишет всё   >   static auto Method(Args...) -> void; };

    Затем нам нужно собственно инстанцировать конструктор с нашим объектом, который запишет информацию

    template <typename T, auto... Args> inline constexpr auto Use() {   std::ignore = T{Args...}; };  template <typename...> consteval auto Ignore() {};  template <typename T, bool GetUnique = true> consteval auto Inject() {   Ignore<decltype(Use<T, InfoInjector<T, GetUnique>{}>)>(); };

    После того как мы добавили информацию в глобальное состояние, нам нужно его прочитать, читаем T, а для этого мы создаём пак из индексов, которые соответсвуют тем, которые использовались для записи данных, кол-во их мы получаем через Counter с тегом. Это значение, которое мы тут получили, будет такое же и в другой функции, вне зависимости от порядка вызова

      []<std::size_t... Is>(std::index_sequence<Is...>){   }(std::make_index_sequence<Counter<T>()>());

    Далее же нам остаётся просто получить данные из глобального состояния, а затем достать первый элемент из тайплиста, т.к. именно он отвечает за нужный нам T.

    template <typename T> consteval auto GetTFromMethod() {   return []<std::size_t... Is>(std::index_sequence<Is...>){      return kTypeList<decltype(Get<0>(Magic(Getter<InfoKey<T, Is>{}>{})))...>;   }(std::make_index_sequence<Counter<T>()>()); };

    Принцип работы GetArgsFromMethod аналогичен, только он берёт не T, а то, что отвечает за Args — всё что после T, для этого мы выкидываем T и берём всё остальное.

    template <typename T, typename... Ts> consteval auto DropHead(TypeList<T, Ts...>) -> TypeList<Ts...> {   return {}; };  template <typename T> consteval auto GetArgsFromMethod() {   return []<std::size_t... Is>(std::index_sequence<Is...>) {     return TypeList<decltype(DropHead(Magic(Getter<InfoKey<T, Is>{}>{})))...>{};   }(std::make_index_sequence<Counter<T>()>()); };
    Что получилось
    #include <tuple>  template <typename... Ts> struct TypeList {};  template <typename... Ts> inline constexpr TypeList<Ts...> kTypeList;  namespace impl {  template <std::size_t I, typename T> struct IndexedType {};  template <typename...> struct Caster {};  template <std::size_t... Is, typename... Ts> struct Caster<std::index_sequence<Is...>, Ts...> : IndexedType<Is, Ts>... {};    } // namespace impl  template <std::size_t I, typename... Ts> consteval auto Get(TypeList<Ts...>) -> decltype(   []<typename T>(impl::IndexedType<I, T>&&) -> T { }(impl::Caster<std::index_sequence_for<Ts...>, Ts...>{}));   template <auto I> struct Getter {   friend constexpr auto Magic(Getter<I>); };  template <auto I, auto Value> struct Injector {   friend constexpr auto Magic(Getter<I>) {return Value;}; };  template <typename Tag, auto Value> struct TagWithValue {};  template <typename Tag, std::size_t I = 0, typename... Ts> consteval auto CounterImpl() -> std::size_t {   if constexpr(requires{Magic(Getter<TagWithValue<Tag, I>{}>{});}) {     return CounterImpl<Tag, I + 1, Ts...>();   };   return (std::ignore = Injector<TagWithValue<Tag, I>{}, 0>{}, I); };  template <typename Tag = void, typename... Ts, auto R = CounterImpl<Tag, 0, Ts...>()> consteval auto Counter() -> std::size_t {   return R; };  template <typename T, auto... Args> inline constexpr auto Use() {   std::ignore = T{Args...}; };  template <typename...> consteval auto Ignore() {};  template <typename Current, std::size_t I> struct InfoKey {};   template <typename Current, bool GetUnique = true> struct InfoInjector;  template <typename Current> struct InfoInjector<Current, true> {   template <     typename T,     typename... Args,     std::size_t I = Counter<Current, T, Args...>(),     //                T, Args... должно быть      auto = Injector<InfoKey<Current, I>{}, kTypeList<T, Args...>>{}  //  уникально, иначе не запишет   >   static auto Method(Args...) -> void; };  template <typename Current> struct InfoInjector<Current, false> {   template <     typename T,     typename... Args,     auto f = []{},     std::size_t I = Counter<Current, decltype(f)>(),      auto = Injector<InfoKey<Current, I>{}, kTypeList<T, Args...>>{}  // запишет всё   >   static auto Method(Args...) -> void; };  template <typename T, bool GetUnique = true> consteval auto Inject() {   Ignore<decltype(Use<T, InfoInjector<T, GetUnique>{}>)>(); };  template <typename T, typename... Ts> consteval auto GetTFromMethod() {   return []<std::size_t... Is>(std::index_sequence<Is...>){      return TypeList<decltype(Get<0>(Magic(Getter<InfoKey<T, Is>{}>{})))...>{};   }(std::make_index_sequence<Counter<T>()>()); };  template <typename T, typename... Ts> consteval auto DropHead(TypeList<T, Ts...>) -> TypeList<Ts...> {   return {}; };  template <typename T> consteval auto GetArgsFromMethod() {   return []<std::size_t... Is>(std::index_sequence<Is...>) {     return TypeList<decltype(DropHead(Magic(Getter<InfoKey<T, Is>{}>{})))...>{};   }(std::make_index_sequence<Counter<T>()>()); };

    О том, как метапрограммирование сделать более похожим на рантайм код

    Мотивация

    constexpr auto array = std::array{kTypeId<int>, kTypeId<void>} | std::views::reverse;  static_assert(std::is_same_v<GetType<array[0]>, void>);

    Так же мы можем использовать любые функции, которые созданы для обычных диапазонов, да и в целом писать обычный код, который будет работать с типами. (Хотя сама я предпочитаю функциональную парадигму для такого перекладывания чего-либо, а в следствии и отсутствие стейта)

    Реализация подобного очень проста — мы при добавлении типа присваиваем ему уникальный Id через счётчик, а через Id становится можно получить обратно информацию о типе:

    struct Types {};   template <std::size_t Id> struct MetaInfoKey {};   template <typename T> struct MetaInfo {   static constexpr std::size_t kTypeId = Counter<Types, T>();   using Type = T; private:    static constexpr auto _ = Injector<MetaInfoKey<kTypeId>{}, TypeList<T>{}>{}; };  template <typename T> inline constexpr std::size_t kTypeId = MetaInfo<T>::kTypeId;  template <std::size_t Id> using GetMetaInfo = MetaInfo<typename decltype(Magic(Getter<MetaInfoKey<Id>{}>{}))::Type>;  template <std::size_t Id> using GetType = GetMetaInfo<Id>::Type;
    Что получилось
    #include <tuple>  template <typename... Ts> struct TypeList {};  template <typename T> struct TypeList<T> {   using Type = T; };  template <auto I> struct Getter {   friend constexpr auto Magic(Getter<I>); };  template <auto I, auto Value> struct Injector {   friend constexpr auto Magic(Getter<I>) {return Value;}; };  template <typename Tag, auto Value> struct TagWithValue {};  template <typename Tag, std::size_t I = 0, typename... Ts> consteval auto CounterImpl() -> std::size_t {   if constexpr(requires{Magic(Getter<TagWithValue<Tag, I>{}>{});}) {     return CounterImpl<Tag, I + 1, Ts...>();   };   return (std::ignore = Injector<TagWithValue<Tag, I>{}, 0>{}, I); };  template <typename Tag = void, typename... Ts, auto R = CounterImpl<Tag, 0, Ts...>()> consteval auto Counter() -> std::size_t {   return R; };  struct Types {};   template <std::size_t Id> struct MetaInfoKey {};   template <typename T> struct MetaInfo {   static constexpr std::size_t kTypeId = Counter<Types, T>();   using Type = T; private:    static constexpr auto _ = Injector<MetaInfoKey<kTypeId>{}, TypeList<T>{}>{}; };  template <typename T> inline constexpr std::size_t kTypeId = MetaInfo<T>::kTypeId;  template <std::size_t Id> using GetMetaInfo = MetaInfo<typename decltype(Magic(Getter<MetaInfoKey<Id>{}>{}))::Type>;  template <std::size_t Id> using GetType = GetMetaInfo<Id>::Type; 

    Заключение

    На самом деле, это далеко не всё, что можно сказать про лупхолы, но то, что показывает главное и основное.

    Код из статьи на годболте

    Статья по этой теме, достойная внимания: «Неконстантные константные выражения«


    ссылка на оригинал статьи https://habr.com/ru/articles/824574/


    Комментарии

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *