заменить неактивные члены данных std::unique_ptr на объединение

Дан союз:

#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_ptrs? Разрешено ли обменивать неактивные std::unique_ptrs альтернативные элементы данных U подобных объединений?

Это кажется допустимым из-за простой природы std::unique_ptr< X >: это просто оболочка над X * и для любых A и B я уверен, что static_assert((sizeof(A *) == sizeof(B *)) && (alignof(A *) == alignof(B *))); выполняется, а расположение указателей идентично для всех типов (кроме указателей на данные-члены и функции-члены классов). Это правда?

Пример кода работает нормально. Но скорее всего есть УБ, если мы читаем стандарт.


person Tomilov Anatoliy    schedule 23.09.2015    source источник
comment
Вы уверены, что std::unique_ptr имеет стандартный макет? Не могли бы вы добавить статическое утверждение?   -  person Kerrek SB    schedule 23.09.2015
comment
@KerrekSB См. правки (clang 3.7 съешь).   -  person Tomilov Anatoliy    schedule 23.09.2015
comment
@Orient, однако у вас есть неопределенное поведение в tag().   -  person RamblingMad    schedule 23.09.2015
comment
@CoffeeandCode Вот описание некоторых фонов.   -  person Tomilov Anatoliy    schedule 23.09.2015


Ответы (2)


с 1_

в частности, примечание о стандартных типах макетов:

... Одна специальная гарантия делается для того, чтобы упростить использование объединений: если объединение стандартного макета содержит несколько структур стандартного макета, которые имеют общую начальную последовательность (9.2), и если объект этого типа объединения стандартного макета содержит одну из структур стандартного макета, разрешено проверять общую начальную последовательность любого из элементов структуры стандартного макета...

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

В вашем случае общая начальная последовательность определенно std::size_t tag. Затем нам нужно знать, будет ли std::unique_ptr<T> одинаковым для всех T, чтобы его также можно было рассматривать как часть общей начальной последовательности:

§ 20.8.1 Шаблон класса unique_ptr
[1] Уникальный указатель — это объект, который владеет другим объектом и управляет этим другим объектом через указатель. Точнее, уникальный указатель — это объект u, в котором хранится указатель на второй объект p...

Ага. Но откуда мы знаем, что все указатели будут представлены одинаково? Ну, в вашем случае:

§ 3.9.2 Составные типы
[ 3 ] ... Представление значений типов указателей определяется реализацией. Указатели на cv-qualified и cv-unqualified версии (3.9.3) совместимых с компоновкой типов должны иметь одинаковые требования к представлению значений и выравниванию...

Таким образом, мы можем полагаться на то, что значение указателя, хранящегося в std::unique_ptr, может быть представлено в другом члене объединения.

Так что нет, здесь нет неопределенного поведения.

person RamblingMad    schedule 23.09.2015
comment
Общее правило начальной последовательности применяется только к объединениям стандартного макета. U не обязательно будет стандартным макетом, потому что std::unique_ptr не обязательно. Кроме того, даже если упрощенное описание представления unique_ptr будет верным, указатели на разные произвольные типы объектов не будут совместимы с макетом, что является требованием для определения общей исходной последовательности. - person K-ballo; 25.09.2015

ИМХО, у вас есть формальное неопределенное поведение, потому что вы всегда получаете доступ к части союзов, даже если последней записью была b.

Конечно, это работает, потому что, кроме управления, unique_ptr содержит только необработанный указатель и хранимое средство удаления. Указатели на любой тип имеют одинаковое представление, и, за исключением вопроса о выравнивании, безопасно преобразовать указатель на X в указатель на Y и обратно. Таким образом, на низком уровне безопасно менять необработанные указатели местами. Это может быть больше зависит от реализации, но я предполагаю, что также безопасно менять местами сохраненные средства удаления, потому что на самом деле сохраняется адрес. И вообще, для типов struct A и struct B деструкторы просто не работают.

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

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

TL/DR: этот код должен работать со всеми текущими известными компиляторами, но, поскольку он не является строго стандартным, будущие компиляторы могут столкнуться с ним. Поэтому я называю это формальным неопределенным поведением.

person Serge Ballesta    schedule 23.09.2015