让我们从一些上下文开始
自定义内存池使用的代码类似于以下代码:
struct FastInitialization {};
template <typename T>
T* create() {
static FastInitialization const F = {};
void* ptr = malloc(sizeof(T));
memset(ptr, 0, sizeof(T));
new (ptr) T(F);
return reinterpret_cast<T*>(ptr);
}
其思想是,当使用FastInitialization
调用时,构造函数可以假设存储已经初始化为零,因此只初始化那些需要不同值的成员。
GCC(至少是6.2和6.3)有一个"有趣"的优化开始了
struct Memset {
Memset(FastInitialization) { memset(this, 0, sizeof(Memset)); }
double mDouble;
unsigned mUnsigned;
};
Memset* make_memset() {
return create<Memset>();
}
编译为:
make_memset():
sub rsp, 8
mov edi, 16
call malloc
mov QWORD PTR [rax], 0
mov QWORD PTR [rax+8], 0
add rsp, 8
ret
但是:
struct DerivedMemset: Memset {
DerivedMemset(FastInitialization f): Memset(f) {}
double mOther;
double mYam;
};
DerivedMemset* make_derived_memset() {
return create<DerivedMemset>();
}
编译为:
make_derived_memset():
sub rsp, 8
mov edi, 32
call malloc
mov QWORD PTR [rax], 0
mov QWORD PTR [rax+8], 0
add rsp, 8
ret
也就是说,只有struct
的前16个字节,即对应于其基的部分,已经被初始化。调试信息确认对memset(ptr, 0, sizeof(T));
的调用已被完全忽略。
另一方面,ICC和Clang都在全尺寸上调用memset
,这是Clang的结果:
make_derived_memset(): # @make_derived_memset()
push rax
mov edi, 32
call malloc
xorps xmm0, xmm0
movups xmmword ptr [rax + 16], xmm0
movups xmmword ptr [rax], xmm0
pop rcx
ret
因此,GCC和Clang的行为不同,问题变成了:GCC是正确的并产生更好的组装,还是Clang是正确的和GCC有缺陷?
或者,就语言律师而言:
构造函数在什么情况下可以依赖于存储在其分配的存储中的上一个值
注意:我认为这只与放置new
有关,但我很高兴以其他方式显示。
放置
new
是否依赖于底层存储值?
不,可能不会。来自[dcl.init]:
如果没有为对象指定初始值设定项,则默认初始化该对象。存储对象时在获得自动或动态存储持续时间的情况下,对象具有不确定的值,并且如果没有对对象执行初始化,则该对象保持不确定值,直到该值被替换为止(5.18)
Indeterminate值的意思就是不确定。这并不意味着,在放置新表达式的情况下,必须保持以前的记忆。编译器可以对内存做任何它想做的事情,包括但不限于什么都不做。
构造函数在什么情况下可以依赖存储在其分配的存储中的上一个值?
[dcl.init]中的后续段落列出了在生成不确定值时行为是而不是未定义的情况,但它们只与无符号窄字符类型有关。
所以,在任何情况下。
GCC和Clang都是正确的-一旦访问了DerivedMemset
的(自己的)数据成员的值,就将导致未定义的行为。因此,编译器有权在调用构造函数之前保留存储范围内这些字段将占用的任何位模式。