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

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

本文关键字:交换 数据成员 非活动 std unique ptr      更新时间:2023-10-16

给定一个并集:

#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_ptrU类联合的替代数据成员?

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

示例代码工作良好。但如果我们阅读标准,很可能存在UB。

from§ 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]……指针类型的值表示形式是由实现定义的。指向layout-compatible的cv-qualified和cv-unqualified版本(3.9.3)的指针类型应具有相同的值表示和对齐要求…

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

这里没有未定义行为

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

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

唯一可能导致代码失败的情况是,编译器强制执行了这样的规则,即除了公共初始子序列之外,只能访问联合的最后一个写入成员。对于当前的编译器,我很确定没有强制执行,所以它应该可以工作。

但是在我曾经问过的关于另一种可能的UB情况的问题中,Hans Passant给出了能够检测缓冲区溢出的高级编译器的研究工作的链接。我真的认为同样的技术可以用来强制访问联合成员的规则,这样这样的编译器就可以在运行时对你的代码引发异常。

TL/DR:这段代码应该在所有当前已知的编译器上工作,但由于它不是严格的标准一致性,未来的编译器可能会被它困住。因此,我称之为正式未定义行为