Объединение параметров шаблона с переменным числом вариантов

Учитывая переменный список типов Ts..., можем ли мы создать объект объединения, который содержит именно объединение этих типов (и без дополнительных данных)?

Конечно, это возможно с std::variant. Однако критическим моментом здесь является то, что для каждого экземпляра этого объединения я всегда буду знать, какой тип оно содержит, поэтому тратить место на тег std::variant кажется расточительным.

Чтобы подробнее рассказать о данном варианте использования, я пытаюсь выполнить эту работу, представляющую собой кортеж, к которому А) можно получить доступ со стороны элемента и Б) можно из него перемещать.

Спасибо, что поделились своим наблюдением. Какой у Вас вопрос?

3CxEZiVlQ 29.06.2024 20:11

Если каждый экземпляр всегда относится к определенному типу и вы всегда знаете, какой это тип, то зачем вообще использовать объединение? Какую реальную проблему, по вашему мнению, решит такой союз? Это звучит как возможная Проблема XY.

Remy Lebeau 29.06.2024 20:14

Если вы знаете свои типы, вам не нужен вариант.

Pepijn Kramer 29.06.2024 20:15

Что-то в этом роде , возможно.

Igor Tandetnik 29.06.2024 20:15

@RemyLebeau извиняюсь - вариант использования немного прояснен.

Thornsider3 29.06.2024 20:21

@IgorTandetnik с таким примером, как мы могли бы эффективно извлекать элементы (за O (1))

Thornsider3 29.06.2024 20:23

@ Thornsider3 ваш вариант использования не ясен. Можете ли вы предоставить реальный код, с которым у вас возникли проблемы?

Remy Lebeau 29.06.2024 20:24

С -O2 вызов get полностью оптимизирован

Igor Tandetnik 29.06.2024 20:25

@RemyLebeau: по крайней мере, как я это читал, контейнер с вещами. В любой момент времени все вещи в контейнере будут одного типа, поэтому нет необходимости хранить тип отдельно для каждого.

Jerry Coffin 29.06.2024 20:27

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

Remy Lebeau 29.06.2024 20:32

@RemyLebeau: Не уверен, что это действительно так. В этом отношении я не уверен, что моя догадка верна. Я как бы надеялся, что ОП подтвердит или опровергнет это...

Jerry Coffin 29.06.2024 20:44

@JerryCoffin Представлен фрагмент того, что я пытаюсь сделать (с некоторыми раздражающими деталями TMP, исключенными для удобства чтения), чтобы это было более понятно. Спасибо за ответ.

Thornsider3 30.06.2024 16:35
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
12
84
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Да, очевидно, что это возможно, потому что std::variant реализуемо.

Однако основной язык, к сожалению, не предоставляет для него какого-либо хорошего синтаксического сахара.

По сути, вам нужно сделать что-то вроде

template<typename T, typename... Ts>
struct variadic_union {
    union {
        T t;
        variadic_union<Ts...> ts;
    };
};

template<typename T>
struct variadic_union<T> {
    T t;
};

а затем аналогичным образом определите рекурсивные функции-члены для доступа к содержимому.

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

Однако вложение не является проблемой во время выполнения. Даже если, например. методу get необходимо рекурсивно вызывать другие функции-члены get, результирующая цепочка вызовов будет оптимизирована до одного вызова базы get посредством встраивания. Доступ к элементам будет возможен в постоянное время, за исключением режима отладочной компиляции или если ваш компилятор не справляется с реализацией встраивания. Этот вид встраивания необходим для работы абстракций C++. Без этого многие конструкции C++, которые должны быть «абстракциями с нулевой стоимостью», не будут таковыми.


Существует также гораздо более простое решение для типа variant, для которого union вообще не требуется:

template<typename T, typename... Ts>
struct tagless_variant {
    alignas(Ts...) std::byte buffer[std::max({sizeof(Ts)...})];
};

Здесь вы можете затем использовать Place-new и явные вызовы деструктора для реализации создания/уничтожения элементов любого из типов. Чтобы получить элемент, вам нужно будет вернуть *std::launder(reinterpret_cast<U*>(buffer)), где U — хранимый тип.

Проблема с этим решением в том, что его нельзя сделать constexpr-дружественным.


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

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

Спасибо за ваш ответ. Я хочу узнать больше о том, почему работает второе решение. Когда мы приводим buffer к U*, расположение результирующего указателя, безусловно, зависит от U*, так как же C++ узнает, что нужно «привести» его в правильное место? Я также видел, как возникла структура с множественным наследованием, указатель this которой может быть статически приведен к любому из ее родительских типов, и я хотел знать, как это возможно.

Thornsider3 30.06.2024 16:38

@ Thornsider3 static_cast, используемый для приведения указателей в иерархии наследования, и reinterpret_cast совершенно разные. Они не являются взаимозаменяемыми, и в большинстве ситуаций только один из них является правильным и не будет иметь неопределенного поведения. reinterpret_cast из указателя (в данном случае после распада массива на указатель &buffer[0] с типом std::byte*) на любой тип указателя U*, соответствующий типу U, гарантированный здесь с помощью alignas(Ts...), создает другой указатель с тем же значением указателя, но типа U*.

user17732522 30.06.2024 18:59

@Thornsider3 Это означает, что reinterpret_cast<U*>(buffer) является выражением типа U* с точно таким же значением, что и &buffer[0], т. е. оно указывает на первый элемент массива. В частности, адрес как часть значения указателя также не изменяется. Поскольку существует несоответствие между типом указателя и типом объекта, на который фактически указывает указатель, прямое использование результата почти всегда является неопределенным поведением.

user17732522 30.06.2024 19:01

@Thornsider3 Однако, если вы ранее создали объект U в &buffer[0], который затем вложен в буфер, и этот объект U все еще находится в своем жизненном состоянии, то вы можете использовать std::launder, чтобы изменить значение указателя с указателя на начало буфера. массив в указатель, указывающий на объект U, расположенный по тому же адресу.

user17732522 30.06.2024 19:04

@Thornsider3 Если вы не создали объект U, например. new(static_cast<void*>(buffer)) U; внутри буфера или если вы впоследствии создали в нем еще один объект другого типа, то вызов std::launder будет иметь неопределенное поведение. Также обратите внимание, что буфер, очевидно, должен иметь достаточный размер в дополнение к выравниванию, иначе новое размещение будет иметь неопределенное поведение. Кроме того, буфер должен иметь тип std::byte или unsigned char, поскольку это единственные типы, которые указаны для обеспечения хранения для других объектов.

user17732522 30.06.2024 19:07

@Thornsider3 static_cast между типами указателей делает что-то совершенно другое и, в отличие от reinterpret_cast, может изменять адрес значения указателя, например. при приведении типа производного класса к типу базового класса, который смещается в хранилище производного класса.

user17732522 30.06.2024 19:08

@Thornsider3 «Когда мы приводим буфер к U*, расположение результирующего указателя определенно зависит от U*»: Нет, поскольку вам нужно было бы заранее разместить объект U точно в &buffer[0], невозможно, чтобы потребовалось смещение адреса. и reinterpret_cast никогда не будет добавлять или вычитать какое-либо смещение адреса. Любое другое использование этой конструкции, например. размещение объекта производного класса и последующая попытка получить указатель на базовый класс из buffer вместо этого будет иметь, как и в случае union, неопределенное поведение.

user17732522 30.06.2024 19:10

Итак, если я правильно понимаю, *std::launder(reinterpret_cast<U*>(buffer)) работает только в том случае, если U является нашим первым типом, и если мы хотим получить доступ к третьему типу, о существовании которого мы знаем и который, как мы знаем, содержит V, нам понадобится *std::launder(reinterpret_cast<V*>(&buffer[2]))?

Thornsider3 30.06.2024 19:11

@ Thornsider3 Нет, совсем нет. Ваша функция get будет выглядеть просто как template<typename U> U& get() { return *std::launder(reinterpret_cast<U*>(buffer)); }, а ваша функция emplace — как template<typename U, typename... Args> void emplace(Args&&... args) { ::new(static_cast<void*>(buffer)) U(std::forward<Args>(args)...); } или проще template<typename U, typename... Args> void emplace(Args&&... args) { std::construct_at(reinterpret_cast<U*>(buffer), std::forward<Args>(args)...); }

user17732522 30.06.2024 19:15

@Thornsider3 Любой из возможных типов в Ts... будет расположен по начальному адресу массива, точно так же, как любой из типов Ts... в случае объединения будет расположен по начальному адресу объединения. Массив как объект сам по себе не важен. Он используется только для хранения, чтобы в объекте tagless_variant было достаточно места для хранения любого из типов Ts..., которые ему могут понадобиться для хранения.

user17732522 30.06.2024 19:16

Другие вопросы по теме