C++ 是不是使用放置新的未定义行为两次构造对象?

Posted

技术标签:

【中文标题】C++ 是不是使用放置新的未定义行为两次构造对象?【英文标题】:C++ Is constructing object twice using placement new undefined behaviour?C++ 是否使用放置新的未定义行为两次构造对象? 【发布时间】:2015-02-17 14:06:24 【问题描述】:

我遇到了一些让我感到恐惧的代码。 本质上它遵循这种模式:

class Foo

  public:
    //default constructor
    Foo(): x(0), ptr(nullptr)  
    
      //do nothing
    

    //more interesting constructor
    Foo( FooInitialiser& init): x(0), ptr(nullptr) 
    
      x = init.getX();
      ptr = new int;
    
    ~Foo()
    
      delete ptr;
    

  private:
    int x;
    int* ptr;
;


void someFunction( FooInitialiser initialiser )

   int numFoos = MAGIC_NUMBER;
   Foo* fooArray = new Foo[numFoos];   //allocate an array of default constructed Foo's

   for(int i = 0; i < numFoos; ++i)
   
       new( fooArray+ i) Foo( initialiser );    //use placement new to initialise
   

    //... do stuff

   delete[] fooArray;

此代码已在代码库中多年,似乎从未引起过问题。这显然是一个坏主意,因为有人可以更改默认构造函数来分配而不是期望第二次构造。简单地用等效的初始化方法替换第二个构造函数似乎是明智的做法。例如。

void Foo::initialise(FooInitialiser& init)

    x = init.getX();
    ptr = new int;

尽管仍然可能存在资源泄漏,但至少防御性程序员可能会考虑以正常方法检查先前的分配。

我的问题是:

像这样构造两次实际上是未定义的行为/被标准禁止还是只是一个坏主意?如果未定义的行为,您能否引用或指出我在标准中查看的正确位置?

【问题讨论】:

你在这段代码上试过 valgrind 了吗? 我看到的主要问题是Foo 不遵循三法则——默认的copy-ctor 和copy-assignment-operator 不会对Foo::ptr 做正确的事情. @cdhowie 也许我们不应该假设其他人的代码最糟糕。我猜 OP 只是删掉了不需要提问的代码。 @cdhowie anatolyg 是对的。原代码删除了拷贝构造函数和赋值运算符。我认为这与我的问题无关。 @DavidWoo 公平点。只是想我会指出,以防这些成员确实是默认实现的。 【参考方案1】:

通常,以这种方式处理新展示位置并不是一个好主意。从第一个 new 调用初始化程序,或调用初始化程序而不是放置 new 都被认为比您提供的代码更好。

但是,在这种情况下,在现有对象上调用placement new 的行为是明确定义的。

程序可以通过重用存储来结束任何对象的生命周期 对象占据的位置或显式调用析构函数 具有非平凡析构函数的类类型的对象。对于一个对象 具有非平凡析构函数的类类型,程序不是 需要在存储之前显式调用析构函数 占用的对象被重用或释放;但是,如果没有 显式调用析构函数,或者如果删除表达式 (5.3.5) 是 不用于释放存储,析构函数不得 隐式调用和任何依赖副作用的程序 析构函数产生的行为未定义。

所以当这种情况发生时:

Foo* fooArray = new Foo[numFoos];   //allocate an array of default constructed Foo's

for(int i = 0; i < numFoos; ++i)

    new( fooArray+ i) Foo( initialiser );    //use placement new to initialise

placement new 操作将结束那里的Foo 的生命周期,并在其位置创建一个新的。在许多情况下,这可能很糟糕,但考虑到您的析构函数的工作方式,这会很好。

在现有对象上调用placement new 可能是未定义的行为,但这取决于特定对象。

这不会产生未定义的行为,因为您不依赖于析构函数产生的“副作用”。

对象的析构函数中唯一的“副作用”是delete 包含的int 指针,但在这种情况下,当调用放置new 时,该对象永远不会处于可删除状态。

如果包含的 int 指针可能等于 nullptr 以外的值,并且可能需要删除,那么在现有对象上调用放置 new 将调用未定义的行为。

【讨论】:

虽然准确,但我认为这种重用是一个糟糕的主意。 在我们超过 30 年的代码库中有很多类似的结构,它们都是从 c 到 c++ 的未完成过渡的结果 @MooingDuck 同意。谢谢,我明确表示这可能是个坏主意,但在这种情况下,它的定义很明确。 即使对象处于delete 将释放内存的状态,我认为只要您不在乎是否有泄漏,行为就会被明确定义:您会仍然不依赖于副作用。 没错,内存泄漏并不是未定义的行为(这通常是个坏主意)

以上是关于C++ 是不是使用放置新的未定义行为两次构造对象?的主要内容,如果未能解决你的问题,请参考以下文章

StructLayout 和 FieldOffset 的未定义行为

fieldoffset 的未定义行为[重复]

是否需要在同一环境中使用同一编译器对同一程序进行编译之间保持一致的未指定和未定义行为?

是否需要在同一环境中使用同一编译器对同一程序进行编译之间保持一致的未指定和未定义行为?

在 C++ 中比较两个 std::string 时的未定义行为 [关闭]

C++ SSE:存储到数组后的未定义行为