Лупхолы — это техника, позволяющая манипулировать глобальным состоянием компилятора, добавляя значения и считывая их.

Эта техника позволяет решать многие задачи, некоторые из которых будут рассмотрены в статье:

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

  2. Узнать, с какими шаблонными параметрами вызывался метод/функция с ADL

  3. Как сделать метапрограммирование с типами более похожим на обычный код, где есть состояние

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

    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, Max, 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, то в первый раз функция инстанцируется и как-бы добавится в конец кода, ей будет доступно всё что было до этого момента, но не то что после, а если снова попытаться её инстанцировать, то нового инстанса произведено не будет, а возьмётся тот, что уже есть выше.

Такая особенность это пример того, как имплементаця использует более широкую гарантию. Если бы шаблоны инстанцировались в разное от одного набора аргументов, то IFNDR.

Проще понять такую особенность можно на примере простой функции GetUnique(Ps: Не используйте лупхолы для таких функций, что реализуются через fold expressions, тимлид вам спасибо не скажет), которая убирает дубликаты из списка типов

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

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

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;

Заключение

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

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

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

Комментарии (3)


  1. NeoCode
    26.06.2024 11:38
    +1

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


    1. eao197
      26.06.2024 11:38
      +3

      Даже не знаю что написать в комментарии:)

      Что пока в стандарте C++ нет чего-то полезного всегда можно попробовать сделать это через Ж дендро-фекальным методом, чтобы благодарные потомки, кому не повезет сопровождать подобный код в будущем, вспоминали автора этих наворотов незлым тихим словом.


    1. sha512sum Автор
      26.06.2024 11:38
      +1

      Ну вот иногда сишные макросы всё же не подходят. Я писала свой фреймворк с DI и там возникла потребность выявлять зависимости компонентов(Использовался Service Locator с данными нужными в шаблоне).

      Если выносить информацию из тела, то это нужно писать бойлерплейт, +меньше диагностируется на КТ, +легко можно ошибиться, а проверка только на рантайме, в сишные макросы это адекватно и не запихнуть.

      Пришлось вспоминать про лупхолы и придумывать им такое применение.