包含包装类型和实际类型的联合体是否有任何保证?

11

我可以将一个 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。我猜测答案是否定的,因为包装类型甚至不能保证与其未包装的对应类型兼容,而访问非活动成员仅在共同初始序列上定义良好

3个回答

4
我认为这是未定义行为。 [class.mem] 给出了以下内容:
两个标准布局结构类型的公共初始序列是非静态数据成员和位域的声明顺序中最长的序列,从每个结构体的第一个这样的实体开始,这些对应实体具有布局兼容类型,并且两个实体都不是位域或者都是带有相同宽度的位域。[...]
在具有结构体类型 T1 的活动成员的标准布局联合中,允许读取另一个结构体类型 T2 的联合成员的非静态数据成员 m,前提是 m 是 T1 和 T2 的公共初始序列的一部分;行为就好像提名了 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; // (*)

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


-1

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

原理上与使用联合从整数中提取特定字节的想法相同;但是,现在您还要冒险依赖编译器不在您的结构体中添加任何填充。有关更多详细信息,请参见访问非活动联合成员和未定义行为?


你的第一段不适用于常见的初始序列。此外,我已经在我的问题中链接了问答。 - Zeta
我认为@Steeve的答案并强制使用union alignas(sizeof(T))示例{ T值; 结构包装{ T包装; }包装器};应该可以解决问题。 - StPiere
@Zeta 我非常确定它是正确的。是的,你可能已经链接了问题,但请重新阅读它(请注意,那里的答案既有C11又有C++11)。你会看到它指出“在联合中最多只能存储一个非静态数据成员的值”,并且陷阱表示的概念已被删除。 - UKMonkey
@UKMonkey 没有头绪 :/. 这个问题的每个答案都至少有一个踩 (包括已删除的答案)。 - Zeta

-1

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

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

甚至有一个注释说:

[注意:因此,在标准布局结构对象内可能会存在未命名的填充,但不在其开头,以实现适当的对齐。 —end note]

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

如果程序尝试通过非以下类型之一的glvalue访问对象的存储值,则行为未定义: - 对象的动态类型, - 包括上述类型之一在其元素或非静态数据成员中的聚合体或联合体类型(包括递归地子聚合体或包含的联合体的元素或非静态数据成员)。
因此,这是完全定义的:
cout << *(&ex.wrapper.wrapped) << endl

因为要求&ex.wrapper.wrapped&ex.value相同,而指向的对象具有正确的类型。 但是,由于标准仅对共同子序列明确规定。因此,我理解cout << ex.wrapper.wrapped << endl会导致未定义行为,因为1.3.24 [defns.undefined]中关于未定义行为的注释如下(强调我的):

当国际标准省略任何明确的行为定义时,可以预期出现未定义行为...

简而言之:我敢打赌,如果不是全部,大多数常见的实现都会接受它,但由于1.3.24[defns.undefined]的备注,我永远不会在生产代码中使用它,而会使用*(&ex.wrapper.wrapped)代替。


在C++17的最新草案n4659中,相关概念是可互相转换性([basic.compound] §4)。

哦,我不知道[defns.undefined]。感谢您提出这个问题。C + 11的9.2§20中的措辞非常相似,但明确提到了指针:“使用reinterpret_cast适当地转换的指向标准布局结构对象的指针指向其初始成员(或如果该成员是位域,则指向其中所在的单元),反之亦然”。因此,如果您想在答案中包含C ++ 11,则可以引用n3337。 - Zeta
对具有正确地址和类型的指针进行解引用并不会使其所引用的对象可访问。 - Oliv
假设member_b是活动成员,member_a不需要处于活动状态,则@SergeBallesta的代码意思是:将union_A的地址强制转换为union_A*类型,并访问其中的member_a成员,再访问其中的member_b成员。 - Oliv
@SergeBallesta,是哪个版本的标准?我在最新版本中没有找到它。 - Oliv
@Oliv n4296 适用于 C++14。同样的句子在 n3337 中适用于 C++11。在 C++17 的 n4659 中,§4 基本复合类型中的概念是指针的可互换性。数组指针和其第一个元素的指针不可互换,因为它们具有不同的类型。 - Serge Ballesta
显示剩余7条评论

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