memcpy 是可简单复制的类型构造还是赋值?
Posted
技术标签:
【中文标题】memcpy 是可简单复制的类型构造还是赋值?【英文标题】:Is memcpy of a trivially-copyable type construction or assignment? 【发布时间】:2014-11-28 02:12:06 【问题描述】:假设您有一个T
类型的对象和一个适当对齐的内存缓冲区alignas(T) unsigned char[sizeof(T)]
。如果您使用std::memcpy
将T
类型的对象复制到unsigned char
数组中,这算作复制构造还是复制赋值?
如果一个类型是普通可复制但不是标准布局,那么可以想象这样的类:
struct Meow
int x;
protected: // different access-specifier means not standard-layout
int y;
;
可以这样实现,因为编译器不会被强制使用标准布局:
struct Meow_internal
private:
ptrdiff_t x_offset;
ptrdiff_t y_offset;
unsigned char buffer[sizeof(int) * 2 + ANY_CONSTANT];
;
编译器可以将 Meow 的 x
和 y
存储在 buffer
的任何部分的缓冲区中,甚至可能在 buffer
内的随机偏移处,只要它们正确对齐并且不重叠。如果编译器愿意,x
和 y
的偏移量甚至可以随每个构造随机变化。 (如果编译器愿意,x
可以在 y
之后使用,因为标准只要求相同访问说明符的成员按顺序排列,而 x
和 y
具有不同的访问说明符。)
这将满足可简单复制的要求; memcpy
将复制隐藏的偏移量字段,因此新副本将起作用。但有些事情是行不通的。例如,在 memcpy
上持有指向 x
的指针会中断:
Meow a;
a.x = 2;
a.y = 4;
int *px = &a.x;
Meow b;
b.x = 3;
b.y = 9;
std::memcpy(&a, &b, sizeof(a));
++*px; // kaboom
但是,真的允许编译器以这种方式实现一个可简单复制的类吗?仅当 a.x
的生命周期结束时,取消引用 px
才应该是未定义的行为。有吗? N3797 标准草案的相关部分在这个主题上不是很清楚。这是[basic.life]/1部分:
对象的生命周期是对象的运行时属性。一个 如果对象属于一个类,则称该对象具有非平凡的初始化 或聚合类型,并且它或其成员之一由 构造函数而不是普通的默认构造函数。 [ 注意: 由平凡的复制/移动构造函数初始化是不平凡的 初始化。 — 结束说明 ]
获得了与T
类型对象的生命周期 开始时间:T
类型正确对齐和大小的存储,并且 如果对象有非平凡的初始化,它的初始化就完成了。如果
T
类型对象的生命周期结束于:T
是具有非平凡析构函数 ([class.dtor]) 的类类型,则析构函数调用开始,或者 对象占用的存储空间被重用或释放。
这是[basic.types]/3:
对于平凡的任何对象(基类子对象除外) 可复制类型
T
,对象是否拥有有效值 键入T
,构成底层字节([intro.memory]) 对象可以复制到char
或unsigned char
的数组中。如果char
或unsigned char
数组的内容被复制回来 进入该对象,该对象随后应保持其原始 价值。 示例省略
那么问题就变成了,memcpy
是否覆盖了可简单复制的类实例“复制构造”或“复制分配”?问题的答案似乎决定了Meow_internal
是否是编译器实现可简单复制的类Meow
的有效方式。
如果memcpy
是“复制构造”,那么答案是Meow_internal
有效,因为复制构造正在重用内存。如果memcpy
是“复制赋值”,那么答案是Meow_internal
不是一个有效的实现,因为赋值不会使指向类的实例化成员的指针无效。如果memcpy
两者都是,我不知道答案是什么。
【问题讨论】:
如果你使用memcpy
那么它不是任何类型的构造或赋值。
既然你可以将memcpy
不是T
的东西变成T
- 这绝对算作存储的“重用”并结束了T
对象的生命周期 - 我明白了没有理由为什么 memcpy
将 T
转换为 T
也不算“重用”。我同意@brianbeuning 的观点,即讨论一个没有理智的人会编写或使用的假设编译器的标准合规性是毫无意义的。
@T.C.我问这个问题的原因是,如果Meow_internal
是非法实现,则意味着标准对offsetof
需要standard-layout 结构的限制没有技术基础。有可能正式证明可简单复制足以支持offsetof
,并因此证明标准更改其定义是合理的。
@dyp 我怀疑它会破坏这一点。 px
没有指向 T
类型的对象;它指向一个子对象,据我所知,不能保证当您重用对象的存储时,指向其子对象的指针仍然有效(当然,它也确实重用了*px
的存储,但是没有保证这种重用也满足 [basic.life]/7) 中的其他要求。
标准中可能没有完全明确定义。考虑 UB 邮件列表中的 N3751 和 related discussion。
【参考方案1】:
我很清楚,使用std::memcpy
既不会导致构造也不会分配。这不是构造,因为不会调用构造函数。也不是赋值,因为不会调用赋值运算符。鉴于一个可简单复制的对象具有简单的析构函数、(复制/移动)构造函数和(复制/移动)赋值运算符,这一点是没有实际意义的。
您似乎引用了 §3.9 [basic.types] 中的 ¶2。在 ¶3 中,它指出:
对于任何可简单复制的类型
T
,如果指向T
的两个指针指向不同的T
对象obj1
和obj2
,其中obj1
和obj2
都不是基类子对象,如果构成obj1
的底层字节 (1.7) 被复制到obj2
,41obj2
随后将保持与obj1
相同的值。 [ 示例:T* t1p;
T* t2p;
// 前提是t2p
指向一个已初始化的对象...std::memcpy(t1p, t2p, sizeof(T));
// 此时,每个可复制类型的子对象都在*t1p
包含 // 与相应子对象中的值相同*t2p
—结束示例]41) 通过使用,例如,库函数 (17.6.1.2)std::memcpy
或std::memmove
。
显然,旨在允许*t1p
以各种方式使用*t2p
的标准将是。
继续到¶4:
T
类型对象的对象表示是T
类型对象占用的 N 个 unsigned char 对象的序列,其中 N 等于sizeof(T)
。对象的值表示是一组保存T
类型值的位。对于普通可复制类型,值表示是对象表示中确定值的一组位,该值是实现定义的一组值的一个离散元素。42 42) 意图是 C++ 的内存模型与 ISO/IEC 9899 编程语言 C 的内存模型兼容。
在两个定义的术语前面使用单词 the 意味着任何给定类型只有 一个 对象表示,并且给定对象只有 一个 值表示。您假设的变形内部类型不应该存在。脚注清楚地表明,其目的是让普通可复制类型具有与 C 兼容的内存布局。期望即使是具有非标准布局的对象,复制它仍然可以使其可用。
【讨论】:
Given that a trivially copyable object has trivial destructors, constructors, and assignment operators
只要求复制和移动构造函数是微不足道的。平凡可复制的类型可以具有非平凡的“普通”构造函数。您可能会想到 POD,它不能有构造函数,但它是可简单复制的更严格的超集。
@jxh 好吧,我要澄清一下,虽然您说“微不足道的构造函数”但没有指定哪个构造函数,但只有 copy 和 move 构造函数必须是微不足道的用于微不足道的可复制状态。 “正常”的非平凡构造函数(我承认我不确定是否有官方术语),即非复制/移动签名允许用于平凡可复制的类型。它是聚合的,因此 POD 类型不能有 any 非平凡的构造函数。是否将其编辑到答案中取决于您,但我认为这样做会有所改善。
@jxh 很酷,并且很好地了解了分配操作如何遵循相同的模式;我立即开始谈论构造函数并忽略了赋值!这很奇怪,因为我大量使用了可复制类型的转换赋值。如果我是迂腐的,我会去掉括号,因为有些人可能会将其解释为“包括”而不是“仅”:)【参考方案2】:
在同一个草稿中,您还可以找到以下文字,直接跟在您引用的文字后面:
对于任何可简单复制的类型
T
,如果指向T
的两个指针指向不同的T
对象obj1
和obj2
,其中 如果复制了构成obj1
的底层字节(1.7),则obj1
和obj2
都不是基类子对象 进入obj2
,obj2
应随后保持与obj1
相同的值。
请注意,这是关于 obj2
值的更改,而不是销毁对象 obj2
并在其位置创建一个新对象。由于不是对象,而只是其值被更改,因此对其成员的任何指针或引用都应保持有效。
【讨论】:
这意味着Meow_internal
不是Meow
的符合标准的实现。我同意这种解释。然而,这样做的结果是标准在“可简单复制”和“标准布局”之间的区别有点模糊。据我所知,offsetof
''must'' 从概念上讲,除了标准布局类型之外,还可以使用可简单复制的类型,或者由于其他原因,该实现明显不合规。以上是关于memcpy 是可简单复制的类型构造还是赋值?的主要内容,如果未能解决你的问题,请参考以下文章