将非活动 std::unique_ptr 数据成员交换为联合

swap non-active std::unique_ptr data members for union

给定一个联合:

#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 合法吗?是否允许交换非活动 std::unique_ptrU 的替代数据成员?

由于 std::unique_ptr< X > 的简单性质,它似乎是允许的:它只是 X * 的包装器,对于任何 AB 我确定 static_assert((sizeof(A *) == sizeof(B *)) && (alignof(A *) == alignof(B *))); 保留和指针排列对于所有类型都是相同的(指向数据成员和 类 的成员函数的指针除外)。是真的吗?

示例代码运行良好。但是如果我们阅读标准,很可能有UB。

恕我直言,你有正式的未定义行为,因为你总是访问联合的 a 部分,即使最后写入的是 b。

当然可以,因为除了管理之外,unique_ptr只包含一个原始指针和一个存储的删除器。指向任何类型的指针都具有相同的表示形式,除了对齐问题外,将指向 X 的指针转换为指向 Y 的指针并返回是安全的。所以 在低级别 交换原始指针是安全的。它可能更依赖于实现,但我认为交换存储的删除器也是安全的,因为实际存储的通常是一个地址。无论如何,对于类型 struct Astruct B,析构函数只是空操作。

唯一可能导致您的代码失败的是,如果编译器强制执行只能访问联合的最后一个写入成员的规则,公共初始子序列除外。对于当前的编译器,我很确定 none 会执行此操作,因此它应该可以工作。

但在一个问题中,我曾经问过 , Hans Passant gave a link 研究能够检测缓冲区溢出的高级编译器的工作。我真的认为可以使用相同的技术来强制执行有关访问联合成员的规则,因此此类编译器可以在 运行 时使用您的代码引发异常。

TL/DR:这段代码应该适用于所有当前已知的编译器,但由于它不严格符合标准,未来的编译器可能会陷入困境。因此,我称此为正式的未定义行为

来自 § 9.5 Unions

特别是关于标准布局类型的注释:

... One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence (9.2), and if an object of this standard-layout union type contains one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of standard-layout struct members ...

所以公共初始序列被允许用于任一联合成员​​。

在你的情况下,常见的初始序列肯定是 std::size_t tag。然后我们需要知道 std::unique_ptr<T> 是否对所有 T 都相同,因此它也可以被视为公共初始序列的一部分:

§ 20.8.1 Class template unique_ptr
[1] A unique pointer is an object that owns another object and manages that other object through a pointer. More precisely, a unique pointer is an object u that stores a pointer to a second object p ...

是的。但是我们怎么知道所有的指针都将被表示为相同的呢?那么,在你的情况下:

§ 3.9.2 Compound types
[ 3 ] ... The value representation of pointer types is implementation-defined. Pointers to cv-qualified and cv-unqualified versions (3.9.3) of layout-compatible types shall have the same value representation and alignment requirements ...

因此我们可以依赖存储在 std::unique_ptr 中的指针的值是联合的另一个成员中可表示的值。

所以不,这里没有未定义的行为。