Время компиляции “карта” для данных типа

Вопрос или проблема

Работая над сгенерированным на этапе компиляции switch-case без потерь производительности, я столкнулся с необходимостью ассоциировать (неследовательные) идентификаторы с типами. Не так уж плохо, я вникнул и сделал что-то вроде этого:

// Даны следующие:
enum class Id { Type1 = 5, Type2 = 8, Type3 = 10 };
class Type1, Type2, Type3;

template <Id ID>
struct IdToType{}; // или просто определение... или лучше статическая проверка

template <>
struct IdToType<Id::Type1> { using type = Type1; };
template <>
struct IdToType<Id::Type2> { using type = Type2; };
template <>
struct IdToType<Id::Type3> { using type = Type3; };

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

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

Я взглянул на этот код и попытался найти альтернативный подход. Я часто прибегаю к std::tuple, когда у меня есть сомнения, поэтому решил переписать данные следующим образом:

struct Type1Metadata {
    using type = Type1;
    constexpr Id id{Id::Type1};
    constexpr bool isValidInput{false};
    // ... дополнительные свойства
};

struct Type2Metadata {
    using type = Type2;
    constexpr Id id{Id::Type2};
    constexpr bool isValidInput{true};
    // ... дополнительные свойства
};

// Продолжайте...

using MetadataDictionary = std::tuple<Type1Metadata, Type2Metadata, ...>;

Уже есть несколько недостатков:

  • после определения нового типа и его метаданных вам нужно вручную включить его в MetadataDictionary. У меня нет идеи, как это решить, и я на 90% уверен, что это невозможно решить.
  • уникальность данных, от уникальных типов метаданных в MetadataDictionary до уникальных Id среди всех типов, не обеспечивается. Я мог бы, вероятно, решить это с помощью статических проверок на правильные конструкции.

Но прежде чем волноваться об этом, мне нужен способ просканировать словарь и найти тип, который удовлетворяет какому-то условию. Другими словами, мне нужно написать, заданный типовой трейт, “компиляционный find_if” для кортежей.

Поскольку у меня не так много опыта с шаблонами, это моя попытка (C++17):

template <template <typename> typename TraitPredicate, typename Tuple>
struct tuple_find_if;

template <template <typename> typename TraitPredicate, typename Head, typename... Tail>
struct tuple_find_if<TraitPredicate, std::tuple<Head, Tail...>>
{
    using type = std::conditional_t<TraitPredicate<Head>::value, Head, typename tuple_find_if<TraitPredicate, std::tuple<Tail...>>::type>;
};

template <template <typename> typename TraitPredicate>
struct tuple_find_if<TraitPredicate, std::tuple<>>
{
    using type = void; // не совсем то, что я ищу
    // static_assert(always_false_v<T>, "Тип не найден"); НЕ РАБОТАЕТ
};

Как вы уже можете заметить по комментариям, этот tuple_find_if должен вызывать ошибку компиляции, если тип, удовлетворяющий предикату, не найден. Я вынужден был заменить статическую проверку на тип-заметку, так как шаблон явно пытается скомпилировать всё до последнего типа в кортеже, поэтому он всегда терпит неудачу. Другими словами, если я проверяю Type1 в std::tuple<Type1, Type2, Type3>, проверка продолжается до Type3, а не останавливается в начале.

Как мне это исправить и/или достичь более эффективного поиска?

Ответ или решение

Компиляция "мап" для данных типов в C++

В процессе работы над проектом, связанным с компиляцией генерации конструкции switch-case без потерь производительности, возникла необходимость в ассоциации не последовательно упорядоченных идентификаторов с типами. Вот как я подошёл к этой задаче.

Исходные данные

Для начала я определил перечисление классов идентификаторов (Id), как показано ниже:

enum class Id { Type1 = 5, Type2 = 8, Type3 = 10 };
class Type1; 
class Type2; 
class Type3;

Затем я создал механизм для сопоставления идентификаторов с типами через шаблоны:

template <Id ID>
struct IdToType{};

template <>
struct IdToType<Id::Type1> { using type = Type1; };
template <>
struct IdToType<Id::Type2> { using type = Type2; };
template <>
struct IdToType<Id::Type3> { using type = Type3; };

Таким образом, я создал способ сопоставления типов с их идентификаторами. Однако это решение стало достаточно громоздким, особенно когда мне потребовалось добавить дополнительные данные, связанные с каждым типом.

Реализация метаданных

Я оптимизировал структуру, используя std::tuple для хранения метаданных, соответствующих каждому типу:

struct Type1Metadata {
    using type = Type1;
    constexpr Id id{Id::Type1};
    constexpr bool isValidInput{false};
    // ... остальные свойства
};

struct Type2Metadata {
    using type = Type2;
    constexpr Id id{Id::Type2};
    constexpr bool isValidInput{true};
    // ... остальные свойства
};

// И так далее...

using MetadataDictionary = std::tuple<Type1Metadata, Type2Metadata, ...>;

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

Поиск в tuple

Одной из важных задач стало написание механизма поиска типа в кортежах, который будет работать на этапе компиляции. Я создал шаблон tuple_find_if, как показано ниже:

template <template <typename> typename TraitPredicate, typename Tuple>
struct tuple_find_if;

template <template <typename> typename TraitPredicate, typename Head, typename... Tail>
struct tuple_find_if<TraitPredicate, std::tuple<Head, Tail...>> {
    using type = std::conditional_t<TraitPredicate<Head>::value, Head, typename tuple_find_if<TraitPredicate, std::tuple<Tail...>>::type>;
};

template <template <typename> typename TraitPredicate>
struct tuple_find_if<TraitPredicate, std::tuple<>> {
    using type = void; 
};

Проблема и её решение

Как следует из комментариев, указанный подход вызывает сложности с попыткой использования static_assert для проверки наличия типа, соответствующего предикату. Template метапрограммирование в стандартном C++ позволяет работать с шаблонами во время компиляции, и если шаблон не найдет соответствующего типа, он продолжит проверять оставшиеся элементы вплоть до последнего. Это стало причиной, почему конструкция не срабатывала корректно.

Для улучшения этого механизма необходимо изменить структуру tuple_find_if. Вместо простого применения type для void можно использовать трюк с static_assert и вспомогательной структурой для проведения проверки:

template <template <typename> typename TraitPredicate, typename Tuple>
struct tuple_find_if;

template <template <typename> typename TraitPredicate, typename Head, typename... Tail>
struct tuple_find_if<TraitPredicate, std::tuple<Head, Tail...>> {
    static constexpr bool found = TraitPredicate<Head>::value;
    using type = std::conditional_t<found, Head, typename tuple_find_if<TraitPredicate, std::tuple<Tail...>>::type>;

    // Если не найдено, то статическая ассерция
    static_assert(found || sizeof...(Tail) == 0, "Type not found in tuple");
};

template <template <typename> typename TraitPredicate>
struct tuple_find_if<TraitPredicate, std::tuple<>> {
    // Не нужно ничего делать
};

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

Заключение

Такой подход к реализации "мапа" идентификаторов к типам предоставляет мощный инструмент для работы с метаданными в C++. С помощью шаблонов и кортежей можно эффективно управлять структурированием кода, минимизируя рукопашные ошибки. В результате повышается как читаемость, так и надежность кода, что имеет решающее значение в разработке на C++.

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

Капча загружается...