memset之后的C++放置新
假设有一个结构体,其构造函数没有初始化所有成员变量:
struct Foo {
int x;
Foo() {}
}
如果我将某个缓冲区设置为 0,则在该缓冲区上使用放置 new 创建一个 Foo 实例,然后从该实例中读取 x,这是定义的行为吗?
void bar(void* buf) {
memset(buf, 0, sizeof(Foo));
Foo* foo = new(buf) Foo;
std::cout << foo.x; // Is this undefined behavior?
}
回答
这是教科书未定义的行为。x构造函数后未初始化成员,读取未初始化的变量是未定义行为。
这个记忆之前被其他东西填满的事实是无关紧要的。
- @AlexanderDyagilev - 好吧......直到你的程序在一个悲惨的布尔值上崩溃...... /sf/ask/3788460371/
- @AlexanderDyagilev 你错了。读取未初始化的变量是教科书未定义的行为。
- @AlexanderDyagilev 你是什么意思它会一直工作?UB 并不意味着无法编译。UB 意味着你无法知道行为会是什么。在这个例子中,placement new 可以将缓冲区初始化为某种调试表示,这意味着 `x` 的值可以在不同的实现中不同。此外,还有一个直接说明它是 UB 的标准:https://timsong-cpp.github.io/cppwp/basic#indet-2
- @AlexanderDyagilev——“未定义的行为”并不意味着“会发生不好的事情”。它只是意味着 C++ 语言定义不会告诉您包含该行为的程序将做什么。通常,这样的程序将“工作”得很好。直到您向最重要的客户提供演示时,它才会崩溃。
回答
作为另一个答案的补充:
如果有人觉得将其视为“技术上未定义的行为,但对我来说足够安全”,请允许我演示结果代码的破坏程度。
如果x被初始化:
struct Foo {
int x = 0;
Foo() {}
};
// slightly simpler bar()
int bar(void* buf) {
std::memset(buf, 0, sizeof(Foo));
Foo* foo = new(buf) Foo;
return foo->x;
}
g++-11 with-O3产生以下结果:
bar(void*):
mov DWORD PTR [rdi], 0 <----- memset(buff, 0, 4) or int x = 0?
They are redundant, so it's only done once.
xor eax, eax <----- Set the return value to 0
ret
这很好。事实上,它甚至没有表现出人们希望通过就地未初始化构造消除的任何开销。编译器很聪明。
与此相反,当x未初始化时:
struct Foo {
int x;
Foo() {}
};
// ... same bar
我们得到,使用相同的编译器和设置:
bar(void*):
mov eax, DWORD PTR [rdi] <----- Just dereference buf as the result ?!?
ret
嗯,它当然更快,但是发生了memset()什么?
编译器认为,由于我们int在新的 memset 内存之上放置了一个未初始化的(又名垃圾),它甚至不必memset()首先考虑 。它可以“回收”之前存在的垃圾。
anything -> 0 -> anythinganything毕竟倒塌了。所以不改变指向的内存的函数buff是对代码的合理解释。
您可以在此处在 Godbolt 上试用这些示例。
- 脚注:我*认为*编译器有权将 `eax` 保留为第二种情况。但是我可以看到返回存储在对象存储中的值如何与 gcc 为祖父合并添加的额外别名安全一致。