【问题标题】:memcpy/memmove to a union member, does this set the 'active' member?memcpy/memmove 到工会成员,这是否设置了“活动”成员?
【发布时间】:2017-02-07 09:08:23
【问题描述】:

重要的澄清:一些评论者似乎认为我是从工会复制的。仔细查看memcpy,它从一个普通的旧uint32_t 的地址复制而来,该地址不包含在联合中。另外,我正在(通过memcpy)复制到工会的特定成员(u.a16&u.x_in_a_union,而不是直接复制到整个工会本身(&u

C++ 对联合非常严格 - 只有当该成员是最后写入的成员时,您才应该从该成员中读取:

9.5 联合[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?

紧接在此上方的代码块可能是 cmets 和答案中的主要问题。事后看来,我不需要在这个问题中混合类型!基本上,假设类型相同,u.a = b 是否与memcpy(&amp;u.a,&amp;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));

确切的行为取决于您平台的字节序,您必须注意陷阱表示等。但这里普遍同意(我认为?感谢反馈!),注意避免有问题的值,上面的代码可以在正确的平台上的正确上下文中完全符合标准。

(如果您对上述代码有疑问,请相应地评论或编辑问题。我想确保在继续执行下面的“有趣”代码之前,我们有上述的无争议版本。 )


如果,且仅当,以上两段代码都不是-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.whateveru.a16 是“活跃成员”?


最后,我自己的猜测是,我们在实践中关心这一点的原因是优化编译器可能没有注意到memcpy 的发生,因此做出了错误的假设(但根据标准,这是允许的假设)关于哪个成员是活动的以及哪些数据类型是“活动的”,因此会导致别名错误。编译器可能会以奇怪的方式重新排序memcpy这是对我们为什么关心这个问题的恰当总结吗?

【问题讨论】:

  • 由您来跟踪“活跃”成员是什么。编译器不会为你做这件事。
  • 您的代码违反了语言律师规则,无论如何都不可移植;例如,它将在大端或小端机器上产生不同的输出,
  • 什么是“语言律师”“规则”,@Lorehead
  • 到目前为止,有很多天真的 cmets 和答案。天真的答案当然是“当然联合是不变的:它是一样的,init”。这个好问题出奇地深。由于可能读取未初始化的内存,我并不完全相信您甚至可以memcpy 联合。请参阅stackoverflow.com/questions/33393569/…,尽管那是在 C 标签上。
  • 我认为因为标准对这种行为非常模糊,所以没有“正确”的答案。每个编译器都可能允许它,结果可能相同,但它不“符合标准”。

标签: c++ c++11 language-lawyer unions object-lifetime


【解决方案1】:

我对标准的解读是,只要类型可轻松复制std::memcpy 就是安全的。

从 9 个类中,我们可以看到 unions 是类类型,因此可简单复制适用于它们。

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)可以是复制到charunsigned char 的数组中。如果charunsigned char 数组的内容被复制回对象中,则对象随后应保持其原始值。

对于任何可简单复制的类型T,如果指向T 的两个指针指向不同的T 对象obj1obj2,其中obj1obj2 都不是基类子对象,如果构成obj1 的底层字节(1.7)被复制到obj2,则obj2 应随后保持与obj1 相同的值。

该标准还给出了两者的明确示例。

因此,如果您要复制整个联合,答案肯定是肯定的,活动成员将与数据一起“复制”。 (这是相关的,因为它表明 std::memcpy 必须 被视为更改联合的活动元素的有效方法,因为明确允许使用它进行整个联合复制。)

现在,您改为复制到联合的成员中。该标准似乎不需要任何特定的方法来分配给工会成员(并因此使其处于活动状态)。它所做的只是指定(9.5)

[ 注意: 通常,必须使用显式析构函数类和放置新运算符来更改联合的活动成员。 ——尾注]

当然,因为 C++11 允许在联合中使用非平凡类型的对象。请注意前面的“一般”,它非常清楚地表明在特定情况下允许使用其他更改活动成员的方法;我们已经知道是这种情况,因为明确允许分配。当然没有禁止使用std::memcpy,否则它的使用是有效的。

所以我的回答是肯定的,这是安全的,是的,它会改变活动成员。

【讨论】:

  • 当提供不同大小的类型时,union 的实现是否会在较小大小处于活动状态时使用超出末尾的额外空间作为魔术令牌,然后继续检查它在运行时如果失败会导致错误,是联合的合法实现吗? (请注意,所有新的分配和安置也将执行此任务...)
  • @Lorehead 1) 成员访问表达式本身不涉及[defns.access] 意义上的“访问”。 2) [class.union]/5 仅适用于“当赋值运算符的左操作数涉及指定联合成员的成员访问表达式 ([expr.ref]) 时”。 *p 不是成员访问表达式。
  • @Lorehead 我不了解其他人,但这是我看待这些事情的方式:在实践中,您需要了解标准所说的内容以及您的实现允许的内容。知道一个构造是非标准的,可以让您对是否使用它做出有根据的决定,并防止您做出危险的推论,例如“如果 A 有效,那么 B 肯定也必须有效”;如果 A 是非标准的,则实现可能会选择允许它,但同时严格遵循关于 B 的标准。更重要的是,您不太可能找到不允许的实现......跨度>
  • ...你要做T* p = (T*)malloc(...);,其中T是一个平凡的类型,然后把*p当作一个左值,指定一个T类型的对象;这样做的代码太多了。但是,memcpy 进入非活动联合成员,或将指向此类成员的指针传递给函数并期望分配给该 *p 将切换活动成员......在某些严格的设置下可能会产生意外,也许现在,也许在未来。
  • @Lorehead 对于您的最后一个问题:,它不是,这是未定义的行为,正如我们之前在讨论中解释的那样。阅读[class.union]/5的第一句。
【解决方案2】:

一个联合最多可以有一个成员处于活动状态,并且在其生命周期内处于活动状态。

在 C++14 标准(第 9.3 节或草案中的 9.5)中,所有非静态联合成员都被分配,就好像它们是 struct 的唯一成员一样,并且共享相同的地址。这不会开始生命周期,而是一个重要的默认构造函数(只有一个联合成员可以拥有)。有一个特殊的规则是分配给联合成员会激活它,即使您通常不能对生命周期尚未开始的对象执行此操作。如果联合是微不足道的,那么它及其成员就无需担心非微不足道的析构函数。否则,您需要担心活动成员的生命周期何时结束。来自标准(§ 3.8.5):

程序可以通过重用对象占用的存储空间或通过显式调用具有非平凡析构函数的类类型对象的析构函数来结束任何对象的生命周期。 [... I]如果没有显式调用析构函数,或者如果没有使用删除表达式来释放存储,则不应隐式调用析构函数,并且任何依赖于析构函数产生的副作用的程序都有未定义的行为。

一般来说,显式调用当前活动成员的析构函数,并使用位置new 使另一个成员处于活动状态会更安全。标准给出了例子:

u.m.~M();
new (&u.n) N;

您可以在编译时使用std::is_trivially_destructible 检查第一行是否必要。通过严格阅读标准,您只能通过初始化联合、分配给它或放置 new 来开始联合成员的生命周期,但是一旦您拥有了,您就可以安全地将一个可简单复制的对象复制到另一个使用memcpy()。 (§ 3.9.3、3.8.8)

对于普通可复制类型,值表示是对象表示中确定值的一组位,T的对象解释是sizeof(T)unsigned char对象的序列. memcpy() 函数复制此对象表示。所有非静态联合成员都具有相同的地址,并且您可以将该地址用作void* 在分配之后和对象的生命周期开始之前进行存储(第 3.8.6 节),因此您可以将其传递给@987654336 @当成员不活动时。如果联合是标准布局联合,则联合本身的地址与其第一个非静态成员的地址相同,因此也是所有成员的地址。 (如果不是,它可以与static_cast相互转换。)

如果一个类型has_unique_object_representations,它是可平凡复制的,并且没有两个不同的值共享相同的对象表示;也就是说,没有位是填充。

如果是is_pod(Plain Old Data)类型,那么它是可平凡复制的并且具有标准布局,因此它的地址也与其第一个非静态成员的地址相同。

C 中,我们保证可以读取到最后写入的兼容类型的非活动联合成员。在 C++ 中,我们没有。它在一些特殊情况下起作用,例如包含相同类型对象地址的指针、相同宽度的有符号和无符号整数类型以及布局兼容的结构。但是,您在示例中使用的类型有一些额外的保证:如果它们存在,uint16_tuint32_t 具有精确的宽度并且没有填充,每个对象表示都是一个唯一值,并且所有数组元素在内存中都是连续的,因此uint32_t 的任何对象表示也是某些uint16_t[2] 的有效对象表示,即使此对象表示在技术上是未定义的。你得到什么取决于字节顺序。 (如果你真的想安全地分割 32 位,你可以使用位移位和位掩码。)

概括地说,如果源对象is_pod,那么它可以通过其对象表示严格复制并覆盖在新地址的另一个布局兼容对象上,并且如果目标对象的大小与has_unique_object_representations相同,它也是可简单复制的,并且不会丢弃任何位 - 但是,可能存在陷阱表示。如果你的联合不是平凡的,你需要删除活动成员(非平凡联合的只有一个成员可以有一个非平凡的默认构造函数,默认情况下它将是活动的)并使用放置new使目标成员活跃。

每当您在 C 或 C++ 中复制数组时,您总是希望检查缓冲区溢出。在这种情况下,您接受了我的建议并使用了static_assert()。这没有运行时开销。你也可以使用memcpy_s()memcpy_s( &amp;u, sizeof(u), &amp;u32, sizeof(u32) ); 将在源和目标是 POD(标准布局的可简单复制)并且联合具有标准布局的情况下工作。它永远不会溢出或下溢联合。它将用零填充联合的任何剩余字节,这会使您担心的许多错误可见且可重现。

【讨论】:

  • @Yakk 是的,根据[basic.life]/7.1,因为它是accessing 一个对象在其生命周期之外的值。
  • @AaronMcDaid u.u32_in_a_union = 3; 是明确定义的,因为 LHS 使用 [class.union]/5 指定的构造来启动联合成员的生命周期(如果它尚未处于活动状态)。只有一组非常具体的表达式可以做到这一点; memcpy 使用的那些不在该集合中 - 请参阅 Yakk 的答案中的我的 cmets。
  • @xaxxon eel.is/c++draft/basic.types#3 但是 [basic.life] 似乎确实说即使是简单可构造的联合成员也不会在初始化时开始其生命周期。如果您想绝对安全,您可以先使用 [class.union] 中的任一方法显式将要复制的成员设为活动成员,然后将 memcpy() 置于活动成员之上。
  • @Lorehead 如果指向 T 的两个指针指向不同的 T 对象 obj1 和 obj2 - 这是该段落中的先决条件。 obj2 需要存在才能memcpy 进入它;如果它的生命周期还没有开始,那么就没有对象。该段落讨论的是更改现有对象的值,而不是开始对象的生命周期。
【解决方案3】:

[class.union]/5:

在联合中,如果一个非静态数据成员的名称引用一个生命周期已经开始但尚未结束的对象 ([basic.life]),则它是活动的。 union 类型的对象的非静态数据成员在任何时候最多可以有一个是活动的,即任何时候最多可以将一个非静态数据成员的值存储在一个 union 中。

任何时候最多只能有一个工会成员处于活动状态。

活跃成员是指其生命周期已经开始但尚未结束的成员。

因此,如果您结束工会成员的生命周期,它就不再有效。

如果您没有活跃的成员,则导致工会的另一个成员的生命周期开始在标准下是明确定义的,并导致它成为活跃的。

联合已为其所有成员分配了足够的存储空间。它们都被分配,就好像它们单独存在一样,并且它们是指针可相互转换的。 [class.union]/2.

[basic.life]/6

在对象的生命周期开始之前但在分配对象将占用的存储空间之后40 或者,在对象的生命周期结束之后并且在对象占用的存储空间之前重用或释放时,任何表示对象将要或曾经位于的存储位置的地址的指针都可以使用,但只能以有限的方式使用。对于正在构造或销毁的对象,请参阅 [class.cdtor]。否则,这样的指针指向已分配的存储空间 ([basic.stc.dynamic.deallocation]),并且像使用 void* 类型的指针一样使用指针是明确定义的。

因此,您可以将指向联合成员的指针视为指向已分配存储的指针。如果这样的构造是合法的,这样的指针可以用来在那里构造一个对象。

Placement new 是在那里构造对象的有效方式。 memcpy 的普通可复制类型(包括 POD 类型)是在那里构造对象的有效方法。

但是,只有在不违反联合体只有一个活跃成员的规则时,才能在其中构造一个对象。

如果您在特定条件下分配给联合成员[class.union]/6,它首先会结束当前活动成员的生命周期,然后开始分配给成员的生命周期。所以你的u.u32_in_a_union = 0xaaaabbbb; 是合法的,即使工会中有另一个活跃的成员(它使u32_in_a_union 活跃)。

placement new 或memcpy 不是这种情况,联合规范中没有明确的“活动成员结束的生命周期”。我们必须在别处寻找:

[basic.life]/5

程序可以通过重用对象占用的存储空间或通过显式调用具有非平凡析构函数的类类型对象的析构函数来结束任何对象的生命周期。

问题是,是否开始联合其他成员的生命周期“重用存储”,从而结束其他联合成员的生命周期?在实践中,显然(它们是指针可互换的,它们共享相同的地址等)。 [class.union]/2.

所以我认为是的。

因此,通过void* 指针创建另一个对象(放置新的,或memcpy,如果该类型合法)结束union 的替代成员的生命周期(如果有)(不调用它们的析构函数,而是这通常没问题),并立即使指向的对象处于活动状态。

通过memcpy over storage 开始doubleint16_t 或类似数组的生命周期是合法的。

复制两个uint16_tuint32_t 的数组的合法性,反之亦然,我将留给其他人争论。显然它在 C++17 中是合法的。但是这个对象是一个联合与合法性无关。


此答案基于与@Lorehead 在其答案下方的讨论。我觉得我应该提供一个直接针对我认为问题核心的答案。

【讨论】:

  • 简单可复制类型(包括 POD 类型)的 memcpy 是在那里构造对象的有效方法 - 我不同意。 memcpy 接受 void*,是的,但是那些指针需要指向对象 - 参见 N1570(C11 草案)中的 7.24.2.1:memcpy 函数将 n 个字符从 s2 指向的对象复制到s1 指向的对象。 7.24.1/3 还指定它通过unsigned char 类型的左值访问这些对象。它会更改目标对象的值,但不会启动任何对象的生命周期。
  • 我在 C++ 标准中找不到任何段落说memcpy 开始任何对象的生命周期;只是您可以使用它在其生命周期内访问现有对象的值(否则它将违反[basic.life]/7.1)。 [intro.object]/1 给出了一个清晰、详尽的启动对象生命周期的方法列表; memcpy 不在其中。
  • [class.union]/5 定义了可用于通过赋值启动联合成员生命周期的构造;通过指向unsigned char 的指针间接获得的左值进行分配,这是memcpy 指定要做的事情,不在其中。
  • @aaron 那里的对象生命周期从int j; 开始:它的状态是未指定的。更有趣的是std::aligned_storage_t&lt; sizeof(int), alignof(int )&gt; b; memcpy(&amp;b,i,sizeof(i));——*(int*)&amp;b 中有int 吗?看起来答案可能是“否”,因为开始生命周期的唯一方法似乎是在类似 pod 的联合上声明该类型的非联合变量、新的(放置与否)或某些操作(如赋值)成员字段。哦,有争议的memcpy 的整个工会可能会将活动对象设置在目的地。工会成员的memcpy 不是这些。
  • 通过memcpy将整个联合对象设置为活动成员到另一个相同类型的对象的问题是一个非常好的问题。我倾向于“不”(不要恨我:-))。 “是”意味着memcpy 将能够创建新对象,因为更改活动成员涉及创建新(子)对象 - 据我所知,目前没有办法开始其他对象的生命周期而不是创造一个。我想说结果类似于memcpyint 转换为floatfloat 可能最终包含int 的对象表示,但这不会创建...
【解决方案4】:

房间里的大象:在完全严格的 C++ 中根本不支持联合,当你尝试应用所有标准条款时,你会得到一种“语言”,而失败的尝试形式化C++的直觉称为标准。

这是因为:

  • 左值指的是一个对象,
  • 成员访问 (x.m) 是任何类或联合的普通左值,
  • 实时班级或工会的所有成员都可以通过成员访问随时指定,
  • 根据严格的生命周期规则,联合中只能有一个成员对象处于活动状态,
  • 标准中未定义指代即将创建的对象的左值概念。

所以一个简单的联合使用如下:

union {
  char c;
  int i;
} u;

u.i = 1;

没有定义的行为,因为u.i 的评估结果不能引用任何int 对象,因为在评估时没有这样的对象。

C++ 委员会未能完成其使命

事实上,没有人出于任何目的使用完全严格的 C++,人们需要摒弃标准的整个部分,或者根据书面文本的灵感来编造整个虚构的条款,或者从文本回到他们想象的意图,然后重新形式化意图,理解它

不同的人会忽略不同的部分,最终得到完全不同的形式。

我的建议是取消生命周期规则,并在任何可能持有此类对象的地址处拥有一个对象。这解决了整个问题,并且没有人对这种方法提出过有效的反对意见(“这会破坏所有不变量”的模糊断言不是有效的反对意见)。在任何有效地址处拥有一个对象只会创建无限数量的潜在对象(尤其是所有指针类型,int*int**int***...),但由于没有写入有效值,因此这些不可用于读取.

请注意,如果没有放宽生命周期规则或左值定义,您甚至不能有一个重要的“严格别名规则”,因为该规则不适用于井没有该规则的定义程序。按照目前的解释,“严格的别名规则”是没有用的。 (而且写得太烂了,反正没人知道是什么意思。)

或者也许有人会告诉我,为了理解严格的别名规则,int 的左值指的是一个对象,只是一个不同的类型。这太令人惊讶和愚蠢了,即使你以这种方式对标准做出一致的解释,我仍然会说它已经被破坏了。

【讨论】:

  • 我认为更公平的说法是,标准的作者认为没有必要禁止低质量实现可能会破坏本应可预测的代码的所有愚蠢行为。我认为单个“活动成员”的概念被打破了,而需要的是更接近读者/作者锁的东西。引用联合作为一个整体(包括作为成员访问左值)是唯一释放锁的操作。通过任何方式读取或写入成员都需要获取“读取器”或“写入器”锁。冲突的锁获取调用 UB。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-06-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-09
  • 1970-01-01
相关资源
最近更新 更多