假设您有一个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];
};
编译器可以在x
的任何部分将缓冲区的y
和buffer
存储在缓冲区内,甚至可以在buffer
内的随机偏移处存储,只要它们是x
。正确对齐并且不重叠。如果编译器希望,y
和x
的偏移量甚至可以随每个结构随机变化。 (y
如果编译器希望x
可以使用,因为标准只要求相同访问说明符的成员按顺序排列,y
和memcpy
具有不同的访问权限-specifiers。)
这符合可轻易复制的要求; x
将复制隐藏的偏移字段,因此新副本可以正常工作。但有些事情是行不通的。例如,在memcpy
上方指向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
的指针会中断:
px
但是,编译器是否真的允许以这种方式实现一个简单的可复制类?如果a.x
的生命周期结束,解除引用T
应该只是未定义的行为。有吗? N3797标准草案的相关部分在这个问题上并不十分清楚。这是 [basic.life] / 1 :
对象的生存期是对象的运行时属性。一个 如果对象属于类,则称其具有非平凡的初始化 或者聚合类型,它或它的一个成员由a初始化 除了一个普通的默认构造函数之外的构造函数。 [注意: 通过简单的复制/移动构造函数进行初始化是非常重要的 初始化。 - 结束注释]
T
类型对象的生命周期 从以下时间开始:
- 获得具有
T
类型的正确对齐和大小的存储,并且- 如果对象具有非平凡的初始化,则其初始化完成。
类型
T
的对象的生命周期在以下时间结束:
- 如果
T
是具有非平凡析构函数的类类型( [class.dtor] ),则析构函数调用将启动,或者- 对象占用的存储空间被重用或释放。
这是 [basic.types] / 3 :
对于任何对象(基类子对象除外)的简单 可复制类型
T
,无论对象是否包含有效值 键入char
,组成的基础字节( [intro.memory] ) 可以将对象复制到unsigned char
或char
的数组中。如果 将unsigned char
或memcpy
数组的内容复制回来 进入物体后,物体应随后保持其原始状态 值。 示例省略
然后问题是,Meow_internal
是否覆盖了一个可复制的类实例“复制构造”或“复制分配”?这个问题的答案似乎决定了Meow
是否是编译器实现可复制的类memcpy
的有效方式。
如果Meow_internal
是“复制构造”,则答案是memcpy
有效,因为复制构造正在重用内存。如果Meow_internal
是“copy-assignment”,那么答案是memcpy
不是有效的实现,因为赋值不会使指向实例化类的成员的指针无效。如果两者都是{{1}},我不知道答案是什么。
答案 0 :(得分:6)
我很清楚,使用std::memcpy
不会导致构造或分配。它不是构造,因为不会调用构造函数。也不是赋值,因为不会调用赋值运算符。鉴于一个简单的可复制对象具有简单的析构函数,(复制/移动)构造函数和(复制/移动)赋值运算符,这一点非常没有用。
你似乎引用了§3.9[basic.types]中的¶2。在¶3,它声明:
对于任何简单的可复制类型
T
,如果指向T
的两个指针指向不同的T
对象obj1
和obj2
,则obj1
如果构成obj2
的基础字节(1.7)被复制到obj1
, 41obj2
,则obj2
是基类子对象随后应保持与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 无符号字符对象的序列,其中 N 等于sizeof(T)
。对象的值表示是保存类型T
的值的位集。对于简单的可复制类型,值表示是对象表示中的一组位,用于确定一个值,该值是实现定义的一组值的一个离散元素。 42
42)意图是C ++的内存模型与ISO / IEC 9899编程语言C的内存模型兼容。
在两个定义的术语前面使用 这个词意味着任何给定的类型只有 一个 对象表示和给定对象只有 一个 值表示。您的假设变形内部类型不应存在。脚注清楚地表明,对于具有与C兼容的存储器布局,其意图是对于具有非标准布局的对象,即使是具有非标准布局的对象,复制它仍然允许它可用。
答案 1 :(得分:2)
在同一稿中,您还可以直接在您引用的文字后面找到以下文字:
对于任何简单的可复制类型
T
,如果指向T
的两个指针指向不同的obj1
个对象obj2
和obj1
,其中 如果复制构成obj2
的基础字节(1.7),则obj1
和obj2
都不是基类子对象 进入obj2
,obj1
随后应与obj2
保持相同的值。
请注意,这说明obj2
的值的更改,而不是销毁对象{{1}}并在其位置创建新对象。由于不是对象,而只是其值被更改,因此对其成员的任何指针或引用都应保持有效。