Учитывая союз:
#include <iostream>
#include <memory>
#include <type_traits>
#include <vector>
#include <cassert>
#include <cstdlib>
struct A { int a; };
struct B { int b; };
template< typename X >
struct S
{
std::size_t tag;
std::unique_ptr< X > x;
};
union U
{
S< A > a;
S< B > b;
U(A x) : a{0, std::make_unique< A >(x)} { ; }
U(B x) : b{1, std::make_unique< B >(x)} { ; }
std::size_t tag() { return a.tag; }
~U()
{
switch (tag()) {
case 0 : {
a.~S< A >();
break;
}
case 1 : {
b.~S< B >();
break;
}
default : assert(false);
}
}
void
swap(U & u) noexcept
{
a.x.swap(u.a.x);
std::swap(a.tag, u.a.tag);
}
};
static_assert(std::is_standard_layout< U >{});
int
main()
{
U a{A{ 0}};
U b{B{~0}};
assert((a.tag() == 0) && (a.a.x->a == 0));
assert((b.tag() == 1) && (b.b.x->b == ~0));
a.swap(b);
assert((a.tag() == 1) && (a.b.x->b == ~0));
assert((b.tag() == 0) && (b.a.x->a == 0));
return EXIT_SUCCESS;
}
U::tag()
Функция верна, так как она позволяет проверять общую начальную подпоследовательность альтернативных элементов данных в U
подобные союзы.
U::swap()
работает, но это законно для std::unique_ptr
s? Можно ли поменять местами неактивные std::unique_ptr
альтернативные данные членов U
подобные союзы?
Это кажется допустимым из-за простой природы std::unique_ptr< X >
: это просто обертка X *
и для любого A
а также B
я уверен static_assert((sizeof(A *) == sizeof(B *)) && (alignof(A *) == alignof(B *)));
Расположение трюмов и указателей одинаково для всех типов (кроме указателей на члены-данные и функции-члены классов). Это правда?
Пример кода работает нормально. Но очень вероятно, что есть UB, если мы читаем стандарт.
от
§ 9.5 Unions
в частности, примечание о стандартных типах макетов:
… Одна специальная гарантия
сделано для того, чтобы упростить использование объединений: если объединение стандартных макетов содержит несколько стандартных макетов
структуры, которые имеют общую начальную последовательность (9.2), и если объект этого типа объединения стандартного макета
содержит одну из структур стандартного макета, разрешается проверять общую начальную последовательность любого из
члены структуры стандартного макета …
Таким образом, разрешается использовать общую начальную последовательность для любого члена объединения.
В вашем случае общая начальная последовательность определенно std::size_t tag
, Тогда нам нужно знать, если std::unique_ptr<T>
будет одинаковым для всех T
поэтому его также можно рассматривать как часть общей начальной последовательности:
§ 20.8.1 Шаблон класса
unique_ptr
[1] Уникальный указатель — это объект, который владеет другим объектом и управляет этим другим объектом через указатель.
Точнее, уникальный указатель — это объектu
который хранит указатель на второй объектp
…
Ага. Но откуда мы знаем, что все указатели будут представлены одинаково? Ну в твоем случае
§ 3.9.2 Типы соединений
[3] … представление значений типов указателей
определяется реализацией. Указатели на cv-квалифицированную и cv-неквалифицированную версии (3.9.3) совместимой с компоновкой
типы должны иметь одинаковые значения представления и требования выравнивания …
Таким образом, мы можем положиться на значение указателя, хранящегося в std::unique_ptr
быть представителем в другом члене союза.
Так что нет, здесь нет неопределенного поведения.
ИМХО, у вас есть формальное Неопределенное Поведение, потому что вы всегда получаете доступ к части союзов, даже если последняя написанная была б.
Конечно, это работает, потому что кроме его управления, unique_ptr просто содержит необработанный указатель и сохраненный удалитель. Указатели на любой тип имеют одинаковое представление, и, за исключением вопроса выравнивания, безопасно преобразовать указатель на X в указатель на Y и обратно. Так на низком уровне если безопасно поменять местами сырые указатели. Это может быть в большей степени зависеть от реализации, но я предполагаю, что безопасно также менять местами сохраненные средства удаления, потому что то, что на самом деле хранится, обычно является адресом. И вообще, для типов struct A
а также struct B
Деструкторы просто не работают.
Единственное, что может привести к сбою в вашем коде, — это если бы компилятор ввел в действие правило, согласно которому только последний записанный член объединения может быть доступен, за исключением общей начальной подпоследовательности. Что касается современных компиляторов, я уверен, что ни один из них не обеспечит это, поэтому он должен работать.
Но в вопросе, который я однажды задаю другой возможный случай UB, Ганс Пассант дал ссылка на сайт исследовать работу над продвинутыми компиляторами, способными обнаруживать переполнения буфера. Я действительно думаю, что эту же технику можно использовать для обеспечения соблюдения правил доступа к членам объединения, поэтому такие компиляторы могут вызывать исключения во время выполнения с вашим кодом.
TL / DR: этот код должен работать со всеми известными в настоящее время компиляторами, но, поскольку он не является строго стандартным, он может быть пойман будущими компиляторами. Как таковой я называю это формальное неопределенное поведение.