C++ memset后使用placement new

5
假设有一个结构体,其构造函数没有初始化所有成员变量:
struct Foo {
  int x;
  Foo() {}
}

如果我使用memset将某个缓冲区设置为0,并在该缓冲区上使用placement 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?
}

2
这里有一个很棒的CppCon视频展示了这个兔子洞有多深。 - Marek R
2个回答

14
作为对其他答案的补充:
如果有人想把这个问题当做“技术上未定义的行为,但对我来说足够安全”而草率地解决掉,那么请允许我演示一下结果代码是多么彻底地破碎。
如果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; 
}

使用 -O3 的 g++-11 会产生以下结果:

bar(void*):
        mov     DWORD PTR [rdi], 0   <----- memset(buff, 0, 4) and/or int x = 0 
        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()时费心。它可以只是“回收”以前存在的垃圾。

anything -> 0 -> anything最终会折叠成anything。因此,函数不改变由buff指向的内存是代码的合理解释。

您可以在godbolt 这里上尝试这些示例。


2
注:我认为编译器在第二种情况下只保留eax是可以的。但我可以理解返回存储在对象存储中的值可能与gcc为祖先联合添加的附加别名安全性一致。 - user4442671
1
另一个可以玩的东西:Foo* foo = new (std::launder((Foo*)buf)) Foo; - Evg
@Evg std::launder 需要一个对象已经在那个位置上了,不是吗?这个“工作”的原因是编译器必须假设这是正确的(即使它并不是),但我不确定除非 IOC 以某种方式介入,否则这是否被正式定义。 - user4442671
@Evg 标准中 std::launder 的名称是 指针优化屏障。它强制编译器假定新的 Foobuf 不会相互别名,尽管这显然是正确的。因此,memset() 应用于 buf,而 Foo 构造应用于新的 Foo 指针,并且编译器被迫将它们视为不同的地址。 - user4442671
@Evg 我认为这可能就是gcc对于未初始化的整数没有提供明确定义存储的情况所做的事情:https://gcc.godbolt.org/z/Mo7exn4zq(当然,它也可能是其他无数种情况)。 - user4442671
显示剩余3条评论

12

这是典型的未定义行为。在构造函数后,成员变量x没有被初始化,读取未初始化的变量是未定义行为。

之前填充该内存的内容是无关紧要的。


1
@alagner,是的,在对象生命周期开始后没有初始化。构造函数被调用,但成员变量没有被初始化。 - SergeyA
3
@AlexanderDyagilev你是错的。读取未初始化的变量是经典的未定义行为。 - SergeyA
2
@AlexanderDyagilev 你说的“它总是有效”的意思是什么?UB并不意味着编译失败。UB意味着您无法知道行为将是什么样子。在这个例子中,放置新的操作可以将缓冲区初始化为某些调试表示形式,这意味着x的值可能因不同的实现而异。此外,还有标准明确指出它是UB:https://timsong-cpp.github.io/cppwp/basic#indet-2 - NathanOliver
5
@AlexanderDyagilev - 好吧...直到你的程序因为一个可怜的布尔值而崩溃... https://dev59.com/hFQJ5IYBdhLWcg3wT0HJ - StoryTeller - Unslander Monica
2
@AlexanderDyagilev -- "未定义行为"并不意味着"一定会出现问题"。它只是表示C++语言规范没有告诉你包含该行为的程序会做什么。通常,这样的程序会运行得非常好。直到你向最重要的客户演示时,它才会崩溃。 - Pete Becker
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接