Узнать, какие параметры принимает конструктор типа
Узнать, с какими шаблонными параметрами вызывался метод/функция с Как сделать метапрограммирование с типами более похожим на обычный код, где есть состояние
Пример работы лупхолов:
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;}; };
Принцип работы лупхолов
-
В 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 };
-
Также функции можно объявлять без уточнения типа возвращаемого значения, используя 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 {};
Далее нужно реализовать саму функцию, для этого в начале нам нужно добавить в глобальный стейт нужные нам значения: Ну и затем получить пак индексов для нашего нового пака типов, размер которого мы получаем от Counter с тем же тегом, но без параметров в Ts, а они там всегда были, поэтому мы гарантируем, что это будет новый инстанс и счётчик инкрементируется, а потому мы можем просто взять значение счётчика. А затем мы просто читаем данные с помощью этого пака индексов, вот что получилось: Такое может пригодится при использовании паттерна Service Locator в Dependency Injection. С помощью такого можно определять зависимости классов, что пригодится для раннего диагностирования кольцевых зависимостей, применения топологической сорировки, или же при реализации параллельной инициализации компонентов. В случае с использованием Stackfull корутин, можно легко подождать инициализации зависимого класса прямо в методе GetComponent, но со Stackless из C++20, конструктор не может быть корутиной и приходится ждать заранее, но на тот момент нам были неизвестны зависимости класса. Решить это можно следующими путями: Использовать кодогенерацию и парсеры C++(LLVM), но есть минусы: Не интроспектировать и вынести информацию из тела Но с лупхолами есть ещё один способ, хоть и ограниченный. Про ограничения: Всё что можно интроспектировать должно использовать входные шаблонные параметры и чтобы от них зависило, какая функция будет вызвана (ADL, методы) Функция должна быть объявлена как constexpr(При этом мочь обязательно выполниться в constexpr не обязательно) Можно инстроспектировать только то, что работает через шаблоны Вот пример как это будет работать с лупхолами: Вот что получится с Foo, если не использовать лупхолы, а вынести из класса: Минусы такого подхода очевидны: очень легок ошибиться. Человек может добавить вызов в конструктор, забыв про kMethodData. Проверка же подобного будет только на рантайме, а об ошибках хотелось бы знать до запуска рантайма. Это лишний бойлерплейт, чего можно избежать. Работает оно следующим образом: мы подменяем то что ожидала получить функция своим объектом. Этот объект будет записывать информацию, которая появляется при вызове метода(T и Args…), чтобы затем её считать и выцепить из неё нужное. Для того чтобы сохранять информацию, нам понадобится ключ, с помощью которого мы будем сохранять значения Затем реализация самого класса, который будет отвечать за отправку данных в глобальное состояние. Благодаря лямбде и той её особенности, что её тип всегда уникален, можно сделать, чтобы оно записывало даже тогда, когда типы не уникальны. Тут мы читаем эти T и Args…, а затем через счётчик смотрим, сколько данных уже было записано и куда нужно записать следующие, для этого мы передаём его тип(как тег) и считанные данные, чтобы было новое инстанцирование. А затем на это место через InfoKey мы записываем объединённые данные в виде T и Args… как список типов. Затем нам нужно собственно инстанцировать конструктор с нашим объектом, который запишет информацию После того как мы добавили информацию в глобальное состояние, нам нужно его прочитать, читаем T, а для этого мы создаём пак из индексов, которые соответсвуют тем, которые использовались для записи данных, кол-во их мы получаем через Counter с тегом. Это значение, которое мы тут получили, будет такое же и в другой функции, вне зависимости от порядка вызова Далее же нам остаётся просто получить данные из глобального состояния, а затем достать первый элемент из тайплиста, т.к. именно он отвечает за нужный нам T. Принцип работы GetArgsFromMethod аналогичен, только он берёт не T, а то, что отвечает за Args — всё что после T, для этого мы выкидываем T и берём всё остальное. Так же мы можем использовать любые функции, которые созданы для обычных диапазонов, да и в целом писать обычный код, который будет работать с типами. Реализация подобного очень проста — мы при добавлении типа присваиваем ему уникальный Id через счётчик, а через Id становится можно получить обратно информацию о типе: На самом деле, это далеко не всё, что можно сказать про лупхолы, но то, что показывает главное и основное. Статья по этой теме, достойная внимания: «Неконстантные константные выражения« ([]{ constexpr auto I = Counter<TypeList<Ts...>, Ts>(); std::ignore = Injector<GetUniqueKey<TypeList<Ts...>, I>{}, kTypeList<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...>>()>()); };Интроспекция тел функций
Мотивация
— Усложняется сборка
— Сложно реализовывать, если в команде нет людей, что работали с этими вещами
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>>>);struct Foo { static constexpr auto kMethodData = kTypeList<TypeList<int, int>>; constexpr Foo(auto& a) { a.template Method<int>(42); std::println(""); }; };Как это работает
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, 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>{}>)>(); }; []<std::size_t... Is>(std::index_sequence<Is...>){ }(std::make_index_sequence<Counter<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>()>()); };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>);(Хотя сама я предпочитаю функциональную парадигму для такого перекладывания чего-либо, а в следствии и отсутствие стейта)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/
Добавить комментарий