【问题标题】:Is using std::memcpy on a whole union guaranteed to preserve the active union member?是否在整个工会上使用 std::memcpy 可以保证保留活动的工会成员?
【发布时间】:2020-06-08 00:38:09
【问题描述】:

在 C++ 中,从最近编写的联合成员(即 活动 联合成员)中读取是明确定义的。

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

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

【问题讨论】:

  • 现代 C++ 代码将使用类型安全的 std::variant 而不是联合,从而使整个问题变得毫无意义。
  • @SamVarshavchik 即使假设这是真的,我仍然认为这个问题是有效的。
  • 常识说它应该只适用于可简单复制类型的联合,但我不完全确定。
  • @SepiaColor 如果没有类型索引,编写类似的东西不会很困难。当然它不能直接复制/移动,但如果你知道里面是什么类型,它仍然可以被分配。
  • 由于您使用undefined-behavior 标记,我将指出您应该使用placement new 而不是malloc,因为就语言而言,malloc 实际上并不创建对象。您可能还必须在访问权限上输入std::launder 才能完全清楚。我不记得在 C++20 中有多少变化,但我知道没有足够的时间来获取 Richard 的全文。(另外,您至少应该能够将 *reinterpret_cast&lt;T*&gt; 替换为 reinterpret_cast&lt;T&amp;&gt; .)

标签: c++ undefined-behavior unions memcpy


【解决方案1】:

因为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_casting 你做。你可以正常地实例化一个A a2std::copy/std::memcpya ,然后你就回到工会成员的生命周期问题,如果你关心的话。但是,我想,如果这个选项对你开放,你一开始只会写A a2 = a……

【讨论】:

  • @SepiaColor 根据these rules提供访问权限。我不知道您的案件有任何此类豁免。联合实际上并不是很有用。几十年前,当编译器被美化为汇编的包装器时,它们更有用,而 C 是抽象的概念有点开玩笑……但是,特别是对于 C++,情况不再如此了。
  • @SepiaColor 我会的。但我承认,没有索引检查就无法使用它。我认为对此的论点通常不如向量那么强(需要快速访问可能加载的元素,并且当您知道这些访问之间的范围不会改变时)。但我想理论上可能在紧密循环中存在一些变体用法......
  • @SepiaColor 我不能完全回答为什么它没有作为一个选项给出:要么有些复杂,要么他们只是没有看到在这种情况下需要提供危险的访问机制。如果你能找到,最初的提案可能会回答这个问题。
  • @SepiaColor 我同意委员会似乎在希望所有东西都对 Fisher Price 婴儿安全(“我们必须称它为 std::move,即使它没有这样的事情,因为大多数人就是这样会使用它,他们不应该关心细节!”)并希望一切都是深奥和超级危险的(字面意思是与模板有关的一切)。但这就是你从委员会那里得到的:骆驼。
【解决方案2】:

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

它会按预期复制。

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


使用std::memcpychar 从一个源复制到一个目标。原始内存复制是可以的。读取 内存 as 未初始化的东西是不行的。

【讨论】:

    【解决方案3】:

    据我所知,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

    恕我直言,这个问题可以通过认识到工会可能有多个“潜在活跃”成员并允许对至少潜在活跃的任何成员执行某些操作(而不是将它们限制为一个特定的活跃成员)来轻松解决成员)。通过指定获取联合成员地址的行为使其“至少潜在地”处于活动状态,直到下次联合是通过非基于字符的访问写入的,并且允许对潜在的活动联合成员进行公共初始序列检查或按字节写入,但不会更改活动成员。

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

    【讨论】:

      【解决方案4】:

      在我回答您的问题之前,我认为您的代码应该添加以下内容:

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

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


      因为你的联合是微不足道的,所以你的代码在 memcpy.你遇到麻烦的地方是*reinterpret_cast&lt;A*&gt;(buf);

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

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 2017-02-07
        • 2018-09-21
        • 2011-08-29
        • 1970-01-01
        • 1970-01-01
        • 2013-05-13
        • 1970-01-01
        • 1970-01-01
        • 2011-09-23
        相关资源
        最近更新 更多