重要澄清:一些评论者似乎认为我是从工会复制的。仔细查看memcpy
,它会从一个普通的uint32_t
的地址中复制,该地址不包含在一个联合中。此外,我正在(通过memcpy
)复制到联盟的特定成员(u.a16
或&u.x_in_a_union
,而不是直接复制到整个联盟(&u
)
C ++对联盟非常严格 - 只有当这是成员的最后一个成员时才应该从成员中读取:
9.5 Unions [class.union] [[c ++ 11]] 在一个联合中,最多一个非静态数据成员可以随时处于活动状态,即最多一个非静态数据成员的值可以随时存储在一个联合中。
(当然,编译器不会跟踪哪个成员是活动的。它取决于开发人员以确保他们自己跟踪这个)
更新:以下代码块是主要问题,直接反映问题标题中的文本。如果这段代码没问题,我会对其他类型进行跟进,但我现在意识到第一块代码本身很有趣。
#include <cstdint>
uint32_t x = 0x12345678;
union {
double whatever;
uint32_t x_in_a_union; // same type as x
} u;
u.whatever = 3.14;
u.x_in_a_union = x; // surely this is OK, despite involving the inactive member?
std::cout << u.x_in_a_union;
u.whatever = 3.14; // make the double 'active' again
memcpy(&u.x_in_a_union, &x); // same types, so should be OK?
std::cout << u.x_in_a_union; // OK here? What's the active member?
紧接其上方的代码块可能是评论和答案中的主要问题。事后看来,我并不需要在这个问题中混合类型!基本上,假设类型相同,u.a = b
与memcpy(&u.a,&b, sizeof(b))
相同吗?
首先,一个相对简单的memcpy
允许我们将uint32_t
读作uint16_t
的数组:
#include <cstdint> # to ensure we have standard versions of these two types
uint32_t x = 0x12345678;
uint16_t a16[2];
static_assert(sizeof(x) == sizeof(a16), "");
std:: memcpy(a16, &x, sizeof(x));
准确的行为取决于平台的字节顺序,您必须注意陷阱表示等。但是,我们普遍同意这一点(我认为?反馈意见!),在谨慎避免有问题的价值观的情况下,上述代码可以在正确的平台上在适当的背景下完美地进行标准投诉。
(如果您对上述代码有疑问,请相应地评论或编辑问题。我希望确保我们在进入&#34;有趣&#之前有一个无争议的上述版本。 34;代码如下。)
如果,且仅当,以上两个代码块都不是-UB,那么我想将它们组合如下:
uint32_t x = 0x12345678;
union {
double whatever;
uint16_t a16[2];
} u;
u.whatever = 3.14; // sets the 'active' member
static_assert(sizeof(u.a16) == sizeof(x)); //any other checks I should do?
std:: memcpy(u.a16, &x, sizeof(x));
// what is the 'active member' of u now, after the memcpy?
cout << u.a16[0] << ' ' << u.a16[1] << endl; // i.e. is this OK?
工会的哪个成员u.whatever
或u.a16
是“活跃成员”&#39;?
最后,我自己的猜测是,在实践中,我们关心这一点的原因是优化编译器可能没有注意到memcpy
发生了因此做出错误的假设(但允许的假设,由标准)关于哪个成员处于活动状态以及哪些数据类型处于活动状态,因此会导致别名错误。编译器可能会以奇怪的方式重新排序memcpy
。 这是我们关心此事的原因的适当摘要吗?
答案 0 :(得分:5)
我对标准的解读是std::memcpy
只要类型可以复制就是安全的。
从9个类中,我们可以看到union
是类类型,因此可复制适用于它们。
union 是使用 class-key 联合定义的类;它一次只保存一个数据成员(9.5)。
平易可复制的类是一个类:
- 没有非平凡的副本构造函数(12.8),
- 没有非平凡的移动构造函数(12.8),
- 没有非平凡的复制赋值运算符(13.5.3,12.8),
- 没有非平凡的移动赋值运算符(13.5.3,12.8)和
- 有一个简单的析构函数(12.4)。
trivially copyable 的确切含义在3.9类型中给出:
对于普通可复制类型
T
的任何对象(基类子对象除外),无论对象是否包含类型T
的有效值,底层字节(1.7)组成该对象可以复制到char
或unsigned char
的数组中。如果将char
或unsigned char
数组的内容复制回对象,则该对象应随后保持其原始值。对于任何简单的可复制类型
T
,如果指向T
的两个指针指向不同的T
对象obj1
和obj2
,则obj1
}obj2
是基类子对象,如果构成obj1
的基础字节(1.7)被复制到obj2
,obj2
随后将保持与{相同的值{1}}。
该标准还给出了两者的明确例子。
因此,如果您正在复制整个联盟,答案肯定是肯定的,活动成员将与数据一起“复制”。 (这是相关的,因为它表明obj1
必须被视为更改联合的活动元素的有效方法,因为明确允许使用它来进行整个联合复制。)
现在,您正在复制到联盟的成员。该标准似乎不需要任何特定的分配给工会成员的方法(因此使其处于活动状态)。它只是指定(9.5)
[注意:通常,必须使用显式析构函数类和放置新运算符来更改联合的活动成员。 - 结束说明]
当然,它说,因为C ++ 11允许联合中非平凡类型的对象。注意前面的“一般”,这清楚地表明在特定情况下允许更换活动成员的其他方法;我们已经知道这是因为明确允许转让。当然,没有禁止使用std::memcpy
,否则其使用将无效。
所以我的答案是肯定的,这是安全的,是的,它改变了活跃的成员。
答案 1 :(得分:2)
联盟中的一个成员最多可以处于活动状态,并且在其生命周期内处于活动状态。
在C ++ 14标准(草案中的第9.3节或第9.5节)中,所有非静态联合成员都被分配,就像它们是struct
的唯一成员一样,并共享相同的地址。这不是从生命周期开始的,而是一个非平凡的默认构造函数(只有一个联盟成员可以拥有)。有一个特殊规则,即分配给工会成员激活它,即使您通常不能对生命周期尚未开始的对象执行此操作。如果工会是微不足道的,那么它及其成员就不会担心任何非平凡的破坏者。否则,您需要担心活动成员的生命周期何时结束。从标准(第3.8.5节):
程序可以通过重用对象占用的存储或通过使用非平凡的析构函数显式调用类类型的对象的析构函数来结束任何对象的生命周期。 [...]如果没有对析构函数的显式调用,或者如果没有使用delete-expression来释放存储,则不应该隐式调用析构函数,并且依赖于析构函数产生的副作用的任何程序都有未定义的行为。
一般来说,明确调用当前活动成员的析构函数,并使用展示位置new
激活另一个成员更安全。标准给出了例子:
u.m.~M();
new (&u.n) N;
您可以在编译时检查std::is_trivially_destructible
是否需要第一行。通过严格阅读标准,您只能通过初始化联合,分配联合或放置new
来开始联盟成员的生命周期,但是一旦拥有,您就可以安全地复制一个简单的可复制对象另一个使用memcpy()
。 (第3.9.3,3.8.8节)
对于可复制的类型,值表示是对象表示中用于确定值的一组位, T 的对象解释是sizeof(T)
{{的序列1}}对象。 unsigned char
函数复制此对象表示。所有非静态联合成员都具有相同的地址,并且在分配之后和对象的生命周期开始之前(第3.8.6节),您可以将该地址用作存储的memcpy()
,因此您可以将其传递给当成员处于非活动状态时void*
。如果union是标准布局联合,则union本身的地址与其第一个非静态成员的地址相同,因此它们都是相同的。 (如果不是,则它与memcpy()
可互换。)
如果是类型static_cast
,它是可以轻易复制的,并且没有两个不同的值共享相同的对象表示;也就是说,没有位填充。
如果类型为has_unique_object_representations
(普通旧数据),那么它可以轻易复制并具有标准布局,因此其地址也与其第一个非静态成员的地址相同。
在 C 中,我们保证可以将兼容类型的非活动联盟成员读取到最后写入的联合成员。在 C ++ 中,我们没有。它有一些特殊情况,例如包含相同类型对象地址的指针,相同宽度的有符号和无符号整数类型,以及布局兼容结构。但是,您在示例中使用的类型有一些额外的保证:如果它们一直存在,is_pod
和uint16_t
具有精确的宽度而没有填充,则每个对象表示都是唯一值,并且所有数组元素在内存中是连续的,因此uint32_t
的任何对象表示也是某些uint32_t
的有效对象表示,即使此对象表示在技术上未定义。你得到的东西取决于字节顺序。 (如果您确实想要安全地分割32位,可以使用位移和位掩码。)
要概括,如果源对象uint16_t[2]
,那么它可以严格地通过其对象表示复制并放置在新地址处的另一个布局兼容对象上,并且如果目标对象具有相同的大小并且{ {1}},它也是可以轻易复制的,不会丢弃任何位 - 但是,可能存在陷阱表示。如果您的联合不是微不足道的,则需要删除活动成员(只有一个非平凡联合的成员可以拥有一个非平凡的默认构造函数,并且默认情况下它将处于活动状态)并使用placement is_pod
来使目标成员活跃。
无论何时在C或C ++中复制数组,总是要检查缓冲区溢出。在这种情况下,您接受了我的建议并使用了has_unique_object_representations
。这没有运行时开销。您也可以使用new
:如果源和目标是POD(可以通过标准布局轻松复制)并且联合具有标准布局,则static_assert()
将起作用。它永远不会溢出或下溢一个联盟。它将用零填充联合的任何剩余字节,这可以使你担心的许多错误可见且可重复。
答案 2 :(得分:2)
在联合中,如果非静态数据成员的名称是指生命周期已开始但尚未结束的对象([basic.life]),则该成员处于活动状态。联合类型对象的至多一个非静态数据成员在任何时候都可以是活动的,也就是说,任何时候最多一个非静态数据成员的值都可以存储在一个联合中。 / p>
联盟中最多只能有一名成员在任何时候都有效。
活跃成员是指其生命周期已开始但尚未结束的成员。
因此,如果你结束了工会成员的生命周期,它就不再有效了。
如果您没有活动成员,则会导致联盟中其他成员的生命周期在标准下明确定义,并使其变为活动状态。
工会已为其所有成员分配了足够的存储空间。它们都被分配,好像它们独自在一起,并且它们是指针可互换的。 [class.union]/2
在对象的生命周期开始之前但是在对象将占用的存储之后已经分配了 40 ,或者在对象的生命周期结束之后以及对象占用的存储之前是重用或释放,可以使用任何指示对象将位于或位于的存储位置的地址的指针,但仅限于有限的方式。对于正在构建或销毁的对象,请参阅 [class.cdtor] 。否则,这样的指针指的是已分配的存储( [basic.stc.dynamic.deallocation] ),并且使用指针就像指针的类型为void *一样,是明确定义的。
因此,您可以获取指向union成员的指针,并将其视为指向已分配存储的指针。如果这样的结构是合法的,那么这样的指针可以用于在那里构造一个对象。
Placement new是在那里构造对象的有效方法。 memcpy
的简单可复制类型(包括POD类型)是在那里构造对象的有效方法。
但是,构建一个对象只有有效的,如果它没有违反联盟中有一个活跃成员的规则。
如果在某些条件[class.union]/6
下分配给联盟的成员,它首先结束当前活动成员的生命周期,然后启动已分配成员的生命周期。因此,即使工会中有另一个成员活动(u.u32_in_a_union = 0xaaaabbbb;
处于活动状态),您的u32_in_a_union
也是合法的。
这不是放置new或memcpy
的情况,联合规范中没有明确的“活动成员结束的生命周期”。我们必须到别处寻找:
程序可以通过重用对象占用的存储或通过使用非平凡的析构函数显式调用类类型的对象的析构函数来结束任何对象的生命周期。
问题是,是否正在开始联盟的另一个成员“重用存储”的生命周期,从而结束其他工会成员的生命?实际上,显然(它们是指针可互换的,它们共享相同的地址等)。 [class.union]/2
所以我认为是的。
因此,通过void*
指针创建另一个对象(如果该类型合法,则放置新的,或memcpy
)将终止union
的替代成员的生命周期(如果有的话)(不要调用他们的析构函数,但这通常是好的),并使指向的对象立即生效和活着。
通过存储开始double
或int16_t
或类似的memcpy
数组的生命周期是合法的。
复制两个uint16_t
数组而不是uint32_t
或者反之亦然的合法性我将留给其他人争论。显然它在C ++ 17中是合法的。但是这个对象是一个联盟与这个合法性无关。
这个答案是基于与@Lorehead讨论的答案。我觉得我应该提供一个直接针对我认为问题核心的答案。
答案 3 :(得分:0)
memcpy(&u.x_in_a_union, &x); // same types, so should be OK?
std::cout << u.x_in_a_union; // OK here? What's the active member?|
如果x_in_a_union处于非活动状态,因为您无法获取抽象机上不存在的对象的地址,我有99%的肯定第一行是未定义的行为。在抽象机上,指针不仅是地址,而且还专门指向对象,并且使用指针仅在该对象的生存期内有效。
不活动的联合成员不作为对象存在,恰好在该成员对象的生存期内,该联合的一个成员对象是活动的。
只有四种创建对象的方法,
通过定义(6.1)通过以下方式创建对象: 当隐式更改的活动成员时,则为new-expression(8.3.4) 联合(12.3),或创建临时对象时(7.4,15.2) [intro.object]
memcpy不在其中。
可能更重要的是,在联合的标准描述中没有任何地方提及memcpy作为隐式更改活动成员的方式,它仅在将赋值操作与非活动成员一起使用时才赋予这种权力。
话虽如此,gcc允许联合类型的类型修剪,即,它允许以定义良好的方式访问非活动类型,但严格来讲这不是C ++。
答案 4 :(得分:-1)
房间里的大象:完全严格的C ++完全不支持联盟,这是您尝试应用失败尝试的所有标准子句形式化时获得的“语言” C ++的直觉称为标准。
这是因为:
x.m
)是任何类或联合的常规左值,所以简单地使用像这样的联合:
union {
char c;
int i;
} u;
u.i = 1;
没有定义的行为,因为u.i
的求值结果不能引用任何int
对象,因为求值时没有这样的对象。
C ++委员会的任务失败。
实际上,没有人出于任何目的使用完全严格的C ++,人们需要忽略标准的整个部分,或者根据书面文字来构成整个虚构的条款,或者从文字回到他们想像的意图,然后将意图重新形式化,以使其有意义。
不同的人解雇了不同的部分,最终得到了完全不同的形式主义。
我的建议是取消生命周期规则,并在可能包含该对象的任何地址处都有一个对象。这就解决了整个问题,没有人对这种方法提出过有效的反对意见(模糊的断言“这破坏了所有不变量”不是有效的反对意见)。在任何有效地址处都有一个对象只会创建无限数量的潜在对象(尤其是所有指针类型int*
,int**
,int***
...),但是这些对象不能用于读取没有写入有效值。
请注意,没有放宽生命周期规则或左值的定义,您甚至不能拥有不平凡的“严格别名规则” ,因为该规则不适用于油井。没有该规则的已定义程序。按照目前的解释,“严格的别名规则”是没有用的。 (而且写得如此糟糕,没人知道它到底意味着什么。)
或者也许有人会告诉我,为了理解严格的别名规则,int
的左值表示对象,只是类型不同。那将是非常令人惊讶和愚蠢的,即使您以这种方式对标准进行了一致的解释,我仍然会说它已被破坏。