Учитывая переменный список типов Ts...
, можем ли мы создать объект объединения, который содержит именно объединение этих типов (и без дополнительных данных)?
Конечно, это возможно с std::variant
. Однако критическим моментом здесь является то, что для каждого экземпляра этого объединения я всегда буду знать, какой тип оно содержит, поэтому тратить место на тег std::variant
кажется расточительным.
Чтобы подробнее рассказать о данном варианте использования, я пытаюсь выполнить эту работу, представляющую собой кортеж, к которому А) можно получить доступ со стороны элемента и Б) можно из него перемещать.
Если каждый экземпляр всегда относится к определенному типу и вы всегда знаете, какой это тип, то зачем вообще использовать объединение? Какую реальную проблему, по вашему мнению, решит такой союз? Это звучит как возможная Проблема XY.
Если вы знаете свои типы, вам не нужен вариант.
Что-то в этом роде , возможно.
@RemyLebeau извиняюсь - вариант использования немного прояснен.
@IgorTandetnik с таким примером, как мы могли бы эффективно извлекать элементы (за O (1))
@ Thornsider3 ваш вариант использования не ясен. Можете ли вы предоставить реальный код, с которым у вас возникли проблемы?
С -O2 вызов get
полностью оптимизирован
@RemyLebeau: по крайней мере, как я это читал, контейнер с вещами. В любой момент времени все вещи в контейнере будут одного типа, поэтому нет необходимости хранить тип отдельно для каждого.
@JerryCoffin до сих пор не ясно, почему это должен быть контейнер союзов.
@RemyLebeau: Не уверен, что это действительно так. В этом отношении я не уверен, что моя догадка верна. Я как бы надеялся, что ОП подтвердит или опровергнет это...
@JerryCoffin Представлен фрагмент того, что я пытаюсь сделать (с некоторыми раздражающими деталями TMP, исключенными для удобства чтения), чтобы это было более понятно. Спасибо за ответ.
Да, очевидно, что это возможно, потому что 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 static_cast
, используемый для приведения указателей в иерархии наследования, и reinterpret_cast
совершенно разные. Они не являются взаимозаменяемыми, и в большинстве ситуаций только один из них является правильным и не будет иметь неопределенного поведения. reinterpret_cast
из указателя (в данном случае после распада массива на указатель &buffer[0]
с типом std::byte*
) на любой тип указателя U*
, соответствующий типу U
, гарантированный здесь с помощью alignas(Ts...)
, создает другой указатель с тем же значением указателя, но типа U*
.
@Thornsider3 Это означает, что reinterpret_cast<U*>(buffer)
является выражением типа U*
с точно таким же значением, что и &buffer[0]
, т. е. оно указывает на первый элемент массива. В частности, адрес как часть значения указателя также не изменяется. Поскольку существует несоответствие между типом указателя и типом объекта, на который фактически указывает указатель, прямое использование результата почти всегда является неопределенным поведением.
@Thornsider3 Однако, если вы ранее создали объект U
в &buffer[0]
, который затем вложен в буфер, и этот объект U
все еще находится в своем жизненном состоянии, то вы можете использовать std::launder
, чтобы изменить значение указателя с указателя на начало буфера. массив в указатель, указывающий на объект U
, расположенный по тому же адресу.
@Thornsider3 Если вы не создали объект U
, например. new(static_cast<void*>(buffer)) U;
внутри буфера или если вы впоследствии создали в нем еще один объект другого типа, то вызов std::launder
будет иметь неопределенное поведение. Также обратите внимание, что буфер, очевидно, должен иметь достаточный размер в дополнение к выравниванию, иначе новое размещение будет иметь неопределенное поведение. Кроме того, буфер должен иметь тип std::byte
или unsigned char
, поскольку это единственные типы, которые указаны для обеспечения хранения для других объектов.
@Thornsider3 static_cast
между типами указателей делает что-то совершенно другое и, в отличие от reinterpret_cast
, может изменять адрес значения указателя, например. при приведении типа производного класса к типу базового класса, который смещается в хранилище производного класса.
@Thornsider3 «Когда мы приводим буфер к U*, расположение результирующего указателя определенно зависит от U*»: Нет, поскольку вам нужно было бы заранее разместить объект U
точно в &buffer[0]
, невозможно, чтобы потребовалось смещение адреса. и reinterpret_cast
никогда не будет добавлять или вычитать какое-либо смещение адреса. Любое другое использование этой конструкции, например. размещение объекта производного класса и последующая попытка получить указатель на базовый класс из buffer
вместо этого будет иметь, как и в случае union
, неопределенное поведение.
Итак, если я правильно понимаю, *std::launder(reinterpret_cast<U*>(buffer))
работает только в том случае, если U
является нашим первым типом, и если мы хотим получить доступ к третьему типу, о существовании которого мы знаем и который, как мы знаем, содержит V
, нам понадобится *std::launder(reinterpret_cast<V*>(&buffer[2]))
?
@ 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)...); }
@Thornsider3 Любой из возможных типов в Ts...
будет расположен по начальному адресу массива, точно так же, как любой из типов Ts...
в случае объединения будет расположен по начальному адресу объединения. Массив как объект сам по себе не важен. Он используется только для хранения, чтобы в объекте tagless_variant
было достаточно места для хранения любого из типов Ts...
, которые ему могут понадобиться для хранения.
Спасибо, что поделились своим наблюдением. Какой у Вас вопрос?