在整个联合上使用 std::memcpy 是否保证保留活跃的联合成员?

Is using std::memcpy on a whole union guaranteed to preserve the active union member?

在 C++ 中,从最近写入的联合成员读取是明确定义的,也就是 active 联合成员。

我的问题是 std::memcpy 整个 联合对象而不是将特定联合成员复制到未初始化的内存区域是否会保留活动联合成员.

union A {
    int x;
    char y[4];
};

A a;
a.y[0] = 'U';
a.y[1] = 'B';
a.y[2] = '?';
a.y[3] = '[=11=]';

std::byte buf[sizeof(A)];
std::memcpy(buf, &a, sizeof(A));

A& a2 = *reinterpret_cast<A*>(buf);

std::cout << a2.y << '\n'; // is `A::y` the active member of `a2`?

你的作业没问题,因为 the assignment to non-class member a.y "begins its lifetime"。但是,您的 std::memcpy 不会这样做,因此对 a2 成员的任何访问都是无效的。因此,您依赖于未定义行为的后果。从技术上讲。在实践中,大多数工具链对原始类型联合成员的别名和生命周期相当宽松。

不幸的是,这里有更多的 UB,因为你违反了联合本身的别名:你可以假装 T 是一堆字节,但是 you can't pretend that a bunch of bytes is a T,无论如何reinterpret_cast你做的很多。您可以正常实例化一个 A a2 并从 a 实例化一个 std::copy/std::memcpy,然后您就回到了联合成员生命周期问题,如果您关心的话.但是,我想,如果这个选项对你开放,你首先会写 A a2 = a

My question is whether std::memcpying a whole union object, as opposed to copying a particular union member, to an uninitialized memory area will preserve the active union member.

它将按预期复制。

您读取结果的方式可能会或可能不会使您的程序出现未定义的行为。


使用 std::memcpychar 从一个源复制到一个目标。原始内存复制没问题。从 内存 读取 作为 未初始化的内容是不正确的。

据我所知,C++ 标准在 intfoo 的大小恰好相同的平台上不区分以下两个函数 [通常是案例]

struct s1 { int x; };
struct s2 { int x; };
union foo { s1 a; s2 b; } u1, u2;
void test1(void)
{
  u1.a.x = 1;
  u2.b.x = 2;
  std::memcpy(&u1, &u2, sizeof u1);
}
void test2(void)
{
  u1.a = 1;
  u2.b = 2;
  std::memcpy(&u1.a.x, &u2.b.x, sizeof u1.a.x);
}

如果平凡可复制类型的联合是平凡可复制类型,则表明 test1memcpy 之后的 u1 的活动成员应该是 b。但是,在等效函数 test2 中,将 int 对象中的所有字节复制到作为活动联合成员 s1.a 的一部分的对象中应该将活动联合成员保留为 a .

恕我直言,这个问题可以很容易地解决,只要认识到一个工会可能有多个 "potentially active" 成员,并允许对任何至少可能处于活动状态的成员执行某些操作(而不是将它们限制为一个特定的活跃成员)。这将使通用初始序列规则变得更加清晰更有用,而不会过度抑制优化,方法是指定获取联合成员地址的行为使其 "at least potentially" 处于活动状态,直到下一次联合是通过非基于字符的访问编写的,并且允许对潜在的活动联合成员进行公共初始序列检查或按字节写入,但不会更改活动成员。

不幸的是,当标准首次编写时,没有努力探索所有相关的极端情况,更不用说就如何处理它们达成共识了。当时,我认为不会有人反对正式容纳多个潜在活跃成员的想法,因为大多数编译器设计自然会毫无困难地容纳它。不幸的是,一些编译器的发展方式使得对此类结构的支持比从一开始就适应的更加困难,并且他们的维护者会阻止任何与他们的设计决策相矛盾的更改,即使标准从未打算使用首先允许这样的决定。

在我回答你的问题之前,我认为你的代码应该添加:

static_assert(std::is_trivial<A>());

因为为了保持与 C 的兼容性,琐碎的类型得到了额外的保证。例如,运行在使用一个对象的构造函数之前(参见 https://eel.is/c++draft/class.cdtor)的要求仅适用于其构造函数不简单的对象。


因为您的联合是微不足道的,所以您的代码在包括 memcpy 之前都没有问题。你 运行 遇到麻烦的地方是 *reinterpret_cast<A*>(buf);

具体来说,您在 A 对象的生命周期开始之前使用它。

https://eel.is/c++draft/basic.life 中所述,当已获得具有适当对齐方式和类型大小的存储并且其初始化完成时,生命周期开始。普通类型具有“空洞的”初始化,所以没有问题,但是存储是个问题。

当您的示例为 buf 获取存储时,

std::byte buf[sizeof(A)];

它没有获得类型的正确对齐。您需要将该行更改为:

alignas(A) std::byte buf[sizeof(A)];