使用std::memcpy在整个union上是否保证保留活动的union成员?

5
在C++中,从最近写入的联合成员中读取是被明确定义的,也就是说,它是“活动”联合成员。
我的问题是,使用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] = '\0';

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`?

2
现代C++代码将使用类型安全的std::variant而不是联合,从而使整个问题无意义。 - Sam Varshavchik
5
即使假设那是真的,我仍然认为这个问题是有意义的。 - cigien
1
常识告诉我们它应该适用于仅包含可平凡复制类型的联合体,但我并不完全确定。 - HolyBlackCat
3
既然您打了 undefined-behavior 这个标签,我想指出,根据语言规范来说,malloc 并不能真正地创建对象,因此你应该使用定位 new,另外在访问时可能还需要加上 std::launder 才能完全清除潜在的问题。我不记得 C++20 中有多少改变,但我知道没有足够的时间来实现 Richard 的完整提案。(此外,您至少应该可以用 reinterpret_cast<T&> 替换 *reinterpret_cast<T*>。) - chris
1
@SepiaColor,我觉得我在你的示例中可怕地误读了memcpy为malloc,天哪。尽管情况类似,但并不仅限于调用构造函数,而是告诉编译器它可以做出什么样的假设。至于“launder”,我总是需要重新研究它的细节,这就是我说“可能”的原因。理论上,您在该地址处有一个std::byte数组,现在您想要一个A,这与const使用类似。逻辑是告诉编译器不要假设只有std::byte[],但是细节还是有点模糊。 - chris
显示剩余5条评论
4个回答

0
在回答你的问题之前,我认为你的代码应该添加这个:
static_assert(std::is_trivial<A>());

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


由于您的联合是微不足道的,因此您的代码在memcpy之前都很好。问题出在*reinterpret_cast<A*>(buf);

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

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

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

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

它不能获得该类型的适当对齐方式。你需要将那一行改为:

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

0
你所拥有的任务还好,因为非类成员变量a.y的赋值"开始其生命周期"。然而,你的std::memcpy并没有这样做,所以对a2成员的任何访问都是无效的。因此,你依赖于未定义行为的后果。从技术上讲,在实践中,大多数工具链对基本类型联合成员的别名和生命周期都相当宽松。

不幸的是,这里有更多的UB问题,因为您正在违反联合本身的别名:您可以假装一个T是一堆字节,但是无论您如何使用reinterpret_cast,都不能假装一堆字节是T。您可以正常实例化A a2,并从a上进行std::copy/std::memcpy,然后您只需回到联合成员生命周期问题(如果您关心的话)。但是,我想,如果您有这个选项,您首先会编写A a2 = a


1
@SepiaColor 它是根据这些规则来提供访问权限的。我不知道你的情况是否有任何豁免。联合体实际上并不是非常有用;它们在几十年前编译器只是汇编程序的包装时更为有用,C 是抽象概念还有点笑话...但是,特别是对于 C++ 来说,现在情况并非如此。 - Asteroids With Wings
1
@SepiaColor 我会的。但我承认,没有索引检查是无法使用它的。我认为这方面的争论通常没有向向使用向量那样强烈(在那里需要快速访问可能有大量元素,并且当您知道这些访问之间的范围不会改变时)。但我想理论上它可能有一些紧密循环的变体用途... - Asteroids With Wings
1
@SepiaColor 我无法完全回答为什么它没有作为一个选项给出:可能是因为有一些复杂性,或者他们只是认为在这种情况下不需要提供一个危险的访问机制。如果你能找到原始提案,可能会有答案。 - Asteroids With Wings
1
@SepiaColor http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0088r3.html 祝阅读愉快;) - Asteroids With Wings
1
@SepiaColor 我同意委员会似乎在想要所有东西都像费舍尔价格婴儿安全一样摇摆不定(“我们必须称之为std::move,即使它并没有这样做,因为大多数人会这样使用,他们不应该关心细节!”),以及想要所有东西都是晦涩难懂和超级危险的(与模板有关的一切都是如此)。但这就是你从委员会得到的:骆驼。 - Asteroids With Wings
显示剩余10条评论

0
我的问题是,将整个联合对象进行std::memcpy,而不是复制特定的联合成员到未初始化的内存区域,是否会保留活动联合成员。
它将按预期进行复制。
您读取结果的方式可能会导致您的程序具有未定义的行为。

使用std::memcpy从一个源复制char到目标地址。原始内存复制是可以的。读取未初始化的内存作为其它类型是不行的。


0
据我所知,在intfoo的大小恰好相同的平台上(通常情况下是这样),C++标准对以下两个函数没有区别。
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);
}

如果一个可平凡复制类型的联合体也是一个可平凡复制类型,那么这就意味着在 test1 中 memcpy 后 u1 的活动成员应该是 b。然而,在等价的函数 test2中,把一个 int 对象的所有字节复制到作为活动联合体成员的 s1.a 的对象中,应该会留下活动联合体成员 a。
在我看来,这个问题可以很容易地解决,只需认识到一个联合体可能有多个“潜在活动”的成员,并允许对任何至少是潜在活动成员的成员执行某些操作(而不是限制它们在一个特定的活动成员上)。这将允许共同初始序列规则更清晰、更实用,而不会过分抑制优化,通过指定获取联合体成员的地址时,“至少是潜在”活动状态,直到下一次通过非字符型访问写入联合体,并提供允许检查或按字节写入潜在活动联合体成员的共同初始序列检查,但不改变活动成员。

不幸的是,当标准第一次编写时,没有努力探索所有相关的边角情况,更不用说达成关于如何处理它们的共识了。当时,我认为正式容纳多个潜在活动成员的想法不会遭到反对,因为大多数编译器设计自然而然地能够容纳这种情况。不幸的是,一些编译器已经发展成一种使得支持这种构造比从一开始就容纳要困难得多的方式,他们的维护者会阻止任何与他们的设计决策相矛盾的变化,即使标准从一开始就没有打算允许这样的决策。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接