【问题标题】:Are there any guarantees for unions that contain a wrapped type and the type itself?对包含包装类型和类型本身的联合有任何保证吗?
【发布时间】:2018-06-12 00:23:07
【问题描述】:

我可以将T 和包装好的T 放在union 中,然后随意检查它们吗?

union Example {
    T value;
    struct Wrapped { 
       T wrapped;
    } wrapper;
};
// for simplicity T = int

Example ex;
ex.value = 12;
cout << ex.wrapper.wrapped; // ?

C++11 标准仅保证对通用初始序列进行保存检查,但value 不是struct。我答案是,因为wrapped types aren't even guaranteed to be memory compatible to their unwrapped counterpartaccessing inactive members is only well-defined on common initial sequences

【问题讨论】:

    标签: c++ struct types language-lawyer unions


    【解决方案1】:

    我相信这是未定义的行为。

    [class.mem] 给我们:

    两种标准布局结构类型的公共初始序列是按声明顺序排列的最长的非静态数据成员和位域序列,从每个结构中的第一个此类实体开始,使得对应的实体具有布局兼容的类型,或者两个实体都不是位域,或者两者都是具有相同宽度的位域。 [...]

    在具有结构类型T1 的活动成员的标准布局联合中,允许读取结构类型T2 的另一个联合成员的非静态数据成员m,前提是m 是T1T2的共同初始序列;行为就像T1 的相应成员被提名一样。

    如果T 不是标准布局结构类型,这显然是未定义的行为。 (请注意,int 不是标准布局结构类型,因为它根本不是类类型)。

    但即使对于标准布局结构类型,构成“公共初始序列”的内容也严格基于非静态数据成员。也就是说,Tstruct { T val; } 没有共同的初始序列 - 根本没有共同的数据成员!

    因此,这里:

    template <typename T>
    union Example {
        T value;
        struct Wrapped { 
           T wrapped;
        } wrapper;
    };
    
    
    Example<int> ex;
    ex.value = 12;
    cout << ex.wrapper.wrapped; // (*)
    

    您正在访问工会的非活动成员。这是未定义的。

    【讨论】:

      【解决方案2】:

      访问不是最后一个写入的成员时,联合行为未定义。所以不,你不能依赖这种行为。

      原理上与使用联合从整数中提取特定字节的想法相同;但额外的风险是,您现在依赖于编译器,而不是在您的结构中添加任何填充。详情请见Accessing inactive union member and undefined behavior?

      【讨论】:

      • 您的第一段不适用于常见的初始序列。另外,我已经在我的问题中链接了问答。
      • 我认为来自@Steeve 的答案并强制与 union alignas(sizeof(T)) Example { T value; 对齐结构包裹 { T 包裹; } 包装器 };应该工作
      • @Zeta 我很确定它确实成立。是的,你可能已经链接了这个问题,但重新阅读它(注意那里的答案同时包含 C11 和 C++11)你会看到它指出 value of at most one of the non-static data members can be stored in a union at any time - 并且陷阱表示的概念已经已删除。
      • @UKMonkey 不知道:/。此问题的每个答案都至少有一个反对票(包括已删除的一个)。
      【解决方案3】:

      它应该可以工作,因为ExampleWrapped 都是标准布局 类,并且C++14 标准有足够的要求来保证在这种情况下valuewrapper.wrapped 是位于同一地址。 n4296 草案在 9.2 类成员 [class.mem] §20 中说:

      如果标准布局类对象有任何非静态数据成员,其地址与地址相同 它的第一个非静态数据成员。

      一个注释甚至说:

      [ 注意:因此在标准布局结构中可能存在未命名的填充 对象,但不是在其开始时,作为实现适当对齐所必需的。 ——尾注]

      这意味着您至少尊重 3.10 左值和右值 [basic.lval] §10 中的严格别名规则

      如果一个程序试图通过 Glvalue 访问一个对象的存储值,而不是其中一个 以下类型的行为未定义
      — 对象的动态类型,
      ...
      — 在其元素中包含上述类型之一或非静态的聚合或联合类型 数据成员(递归地包括子聚合的元素或非静态数据成员) 或包含联合),

      所以这是完美定义的:

      cout << *(&ex.wrapper.wrapped) << endl
      

      因为&amp;ex.wrapper.wrapped 必须与&amp;ex.value 相同,并且指向的对象具有正确的类型。 . 但由于标准仅对公共子序列是明确的。所以我的理解是cout &lt;&lt; ex.wrapper.wrapped &lt;&lt; endl 调用未定义的行为,因为 1.3.24 [defns.undefined] 中有一条关于 未定义的行为 说(强调我的):

      当本国际标准省略任何明确的定义时,可能会出现未定义的行为 行为...

      TL/DR:我敢打赌,即使不是所有常见实现,大多数常见实现都会接受它,但由于 1.3.24 [defns.undefined] 中的注释,我永远不会在生产代码中使用它,但会使用 @改为 987654329@。


      在最新的 C++17 草案 n4659 中,相关概念是inter-convertibility([basic.compound] §4)。

      【讨论】:

      • 呃,我不知道 [defns.undefined]。谢谢你提出来。 C+11 的 9.2§20 中的措辞非常相似,但明确提到了指针:“指向标准布局结构对象的指针,使用 reinterpret_cast 进行适当转换,指向其初始成员(或者如果该成员是位域,然后到它所在的单元),反之亦然”。因此,如果您想在答案中包含 C++11,可以引用 n3337。
      • 解除对具有正确地址和类型的指针的引用,不会使其引用的对象可访问。
      • @SergeBallesta Literaly: reinterpret_cast&lt;union_A*&gt;(&amp;union_A.member_a)-&gt;member_b 假设 member_b 是活跃成员,member_a 不需要活跃。
      • @Oliv:这就是我使用指针的原因。由于value 刚刚被分配,它是工会的活跃成员。因为valuewrapper.wrapped 具有相同的地址和相同的类型,所以&amp;ex.wrapper.wrapped 实际上是指向ex.value 的指针,它是有效成员。这就是它可以被取消引用的原因。
      • @SergeBallesta,在标准中,从未说过具有正确值和类型的指针是指向对象的指针 [basic.compound],并且给出了许多示例,其中具有正确地址和类型的指针是无效指针。例如int arr[10]{}; *reinterpret_cast&lt;int*&gt;(&amp;arr)=1是UB,即使数组的地址和它的第一个元素的地址相同。
      猜你喜欢
      • 1970-01-01
      • 2019-10-17
      • 1970-01-01
      • 2021-12-07
      • 1970-01-01
      • 1970-01-01
      • 2021-08-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多