使用placement new会清零内存吗?

3

I have the following code :

struct foo {};
void bar(foo *d) {
  new(d) foo(*d);
}

表达式new(d) foo(*d)是否不改变由d指向的对象?更具体地说,如果类foo及其递归包含的所有对象仅具有平凡的复制构造函数,则new(d) foo(*d)是否会保持*d不变?一个不成立的情况可能是new在调用复制构造函数之前将内存清零。C++语言中是否存在这样的条款? 编辑:有非平凡的原因可以让人们这样做。考虑将对象从地址空间复制到另一个地址空间,例如从CPU内存到GPU内存。其中一种解决方案是对对象进行逐字节拷贝。这在很多情况下都有效。如果类具有虚方法,则逐字节拷贝会复制vtable指针,该指针将指向某些CPU内存。可以使用上述表达式new(d) foo(*d)强制编译器重置vtable指针。

https://dev59.com/zHE95IYBdhLWcg3wrP6w - m0skit0
如果你这样做,我会预计得到一些相当疯狂的结果。 - Edward Strange
它为什么会这样呢?你期望非放置new会将内存清零吗?在构造函数中没有明确初始化的POD成员具有垃圾值。 - jamesdlin
2
关于您的编辑:我敢说,每当您感到想要推理一些虚构的“vtable指针”时,您就将要做错事情了... - Kerrek SB
6个回答

2
我在研究性能问题时遇到了这个问题。某些代码使用放置new在包含大缓冲区的对象上时,速度不如预期。原因是在调用构造函数之前,放置的new会将内存清零。
根据标准的阅读,我同意其他答案:编译器没有必要特别做什么。
然而,gcc 4.9、gcc 5.3、clang 3.4、clang 3.8和Apple clang似乎都在放置new的情况下清零内存。从汇编输出中可以看出,在调用构造函数之前有一个显式调用memset。堆栈构造的对象不会进行零初始化,因此似乎不是构造函数在工作。
从Dignus Systems/C++ for z/OS的汇编输出中可以看出,它似乎也调用了一个库函数,可能做了类似(而且很慢)的事情。
因此,放置new允许清零内存,而且许多实现确实会清零内存。
例如测试用例:
#include <new>
#include <cstdint>
#include <stdio.h>

struct Test {
    char b[4];

    void show(char const* prefix) {
        for (unsigned i = 0; i < sizeof(b); ++i)
            printf("%s index %d: %d\n", prefix, i, b[i]);
    }
};

int main()
{
    char* p = new char[sizeof(Test)];

    for (unsigned i = 0; i < sizeof(Test); ++i)
        p[i] = 'Q';

    Test* t1 = new(p) Test();
    Test t2;

    t1->show("t1");
    t2.show("t2");
}

例如输出结果(FreeBSD上的clang 3.4):

t1 index 0: 0
t1 index 1: 0
t1 index 2: 0
t1 index 3: 0
t2 index 0: 51
t2 index 1: -1
t2 index 2: 3
t2 index 3: 1

2
关于placement new是否将内存清零,实际上并不会,它只是调用相应的构造函数来执行该构造函数定义的操作,这可能会根据定义方式而将内存清零或不清零。在这种情况下,您正在使用复制构造函数。
至于您提供的代码,它是未定义行为。无论d指向有效对象还是无效对象,都是如此。如果它引用有效对象,则在已构造的对象上调用构造函数是未定义行为,如果该对象具有非平凡析构函数。如果它之前没有被初始化(即它不引用foo对象),则从它进行复制是未定义行为。

1
我不认为“在已初始化的对象上调用构造函数”是绝对错误的。如果析构函数没有任何影响,那么我认为这是可以的。请参见3.8(1)。 - Kerrek SB
除了你说的placement new不会将内存清零之外,我同意你的所有观点。我看不出标准是要求这样做还是那样做的,而且许多实现确实会将内存清零。 - janm

1

我认为这是未定义的行为:一旦存储对象的内存被用于其他用途,对象的生命周期就会结束。当您使用this指针等于d进入复制构造函数时,原始对象在语言上已经不存在了,因此您在复制构造函数中有一个悬空引用。

当然,更容易出现未定义行为的情况是~foo()具有影响的情况。


如果d指向有效对象,由于您所指出的原因,它是未定义行为;如果d指向原始内存,则仍然是未定义行为,因为它被用作复制构造函数的参数。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas:没错。我假设 *d 是一个有效的对象;否则游戏就结束了,你将被送进监狱而无法收集 $200。 - Kerrek SB
@KerrekSB 这是错误的假设,因为in-place new仅适用于未初始化的内存。 - Mahmoud Al-Qudsi
@MahmoudAl-Qudsi:这不是真的。如果析构函数没有任何影响,你完全有权利“覆盖”现有对象。请参阅标准中关于“对象生命周期”的部分。 - Kerrek SB

1

是和不是。

我认为我们正在涉及编译器定义的行为。但是,对于Visual C++ 2019,如果没有默认构造函数,放置 new 将清除数据。请参见下面的片段。

int buffer[4];

class some_class
{
public:
    int a, b, c;
};

class some_class2
{
public:
    int a, b, c;

    some_class2()
    {
        ;
    }
};

buffer[0] = 1;
buffer[1] = 2;
buffer[2] = 3;
some_class* c = new (buffer) some_class();
std::cout << "placement new with a class clears it" << std::endl;
std::cout << c->a << " " << c->b << " " << c->c << std::endl;

buffer[0] = 1;
buffer[1] = 2;
buffer[2] = 3;
some_class2* d = new (buffer) some_class2();
std::cout << "placement new with a class that has a constructor does not clear it" << std::endl;
std::cout << d->a << " " << d->b << " " << d->c << std::endl;

0

Placement new 的唯一工作就是在为尚未初始化的对象分配的内存上运行构造函数。它不会做更多或更少的事情,与手动调用构造函数(虽然这是不可能的)得到的结果相同。


第一句话是有误导性的。并不需要“已经存在的对象”,通常这将是未定义的行为,而且在任何情况下,对象将在构造函数开始时停止存在。相反,放置 new 在给定的内存位置上构造一个对象。 - Kerrek SB
如果将其强制转换为对象,则它是一个对象,因此,如果您分配了该内存并将其分配给该对象类型的指针,则它是一个对象 - 只是尚未初始化。我已更新帖子以提高准确性。 - Mahmoud Al-Qudsi
@MahmoudAl-Qudsi: 其实不然,如果一个对象已经被构建而没有被销毁,那么它是一个对象,如果在构建之前或者在销毁之后,它只是一块内存。 - David Rodríguez - dribeas
标准非常清楚地定义了“对象生命周期”的概念。如果您更喜欢在自己的内心独白中以不同的方式思考C ++,那很好,但为了回答问题,我认为我们应该坚持标准规定的内容。 - Kerrek SB

0
请注意,您正在调用一个对象的复制构造函数,并将其自身作为复制源。我预计这会导致程序出现异常。谁知道呢。我没有看到标准中有任何东西能让我期望发生什么。

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