Вопрос или проблема
Работая над сгенерированным на этапе компиляции 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++.