21

我知道显式调用析构函数可能会由于双重析构函数调用而导致未定义的行为,如下所示:

#include <vector>

int main() {
  std::vector<int> foo(10);
  foo.~vector<int>();
  return 0;  // Oops, destructor will be called again on return, double-free.
}

但是,如果我们称placement new 来“复活”对象呢?

#include <vector>

int main() {
  std::vector<int> foo(10);
  foo.~vector<int>();
  new (&foo) std::vector<int>(5);
  return 0;
}

更正式地说:

  1. 如果我明确地在某个对象上显式调用析构函数,而该对象最初不是用放置 new 构造的(例如它要么是局部/全局变量,要么是用new) 分配的,然后,在这个对象被破坏之前,在它上面调用placement new 来“恢复”它?
  2. 如果没问题,是否可以保证对该对象的所有非常量引用也可以,只要我在对象“死”时不使用它们?
  3. 如果是这样,是否可以使用非常量引用之一来放置 new 以复活对象?
  4. const 引用呢?

示例用例(尽管这个问题更多是关于好奇心):我想“重新分配”一个没有operator=.

我见过这个问题,它说具有非静态const成员的“覆盖”对象是非法的。因此,让我们将这个问题的范围限制在没有任何const成员的对象上。

4

2 回答 2

14

首先,[basic.life]/8明确指出对原始对象的任何指针或引用foo都应引用您在您的情况下构造的新对象foo。此外,名称foo将指代在那里构造的新对象(也[basic.life]/8)。

其次,您必须确保foo在退出其范围之前存在存储所使用的原始类型的对象;所以如果有任何东西抛出,你必须抓住它并终止你的程序([basic.life]/9)。

总的来说,这个想法通常很诱人,但几乎总是一个可怕的想法。

  • (8) 如果在对象的生命周期结束后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:

    • (8.1) 新对象的存储恰好覆盖了原始对象占用的存储位置,并且
    • (8.2) 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且
    • (8.3) 原始对象的类型不是 const 限定的,并且,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且
    • (8.4) 原始对象是类型 T 的最衍生对象 (1.8),而新对象是类型 T 的最衍生对象(也就是说,它们不是基类子对象)。
  • (9) 如果程序以静态 (3.7.1)、线程 (3.7.2) 或自动 (3.7.3) 存储持续时间结束 T 类型对象的生命周期,并且如果 T 具有非平凡的析构函数,则当隐式析构函数调用发生时,程序必须确保原始类型的对象占据相同的存储位置;否则程序的行为是不确定的。即使块因异常而退出也是如此。

有理由手动运行析构函数并进行新的放置。除非您正在编写自己的变体/任何/向量或类似类型,否则简单的事情operator= 就不是其中之一。

如果您真的非常想重新分配一个对象,找到一个std::optional实现,并使用它创建/销毁对象;它很小心,你几乎肯定不会足够小心。

于 2017-03-04T18:01:24.260 回答
8

这不是一个好主意,因为如果新对象的构造函数抛出异常,您仍然可以运行两次析构函数。也就是说,析构函数将始终在作用域的末尾运行,即使您异常离开作用域也是如此。

以下是展示此行为的示例程序(Ideone 链接):

#include <iostream>
#include <stdexcept>
using namespace std;
 
struct Foo
{
    Foo(bool should_throw) {
        if(should_throw)
            throw std::logic_error("Constructor failed");
        cout << "Constructed at " << this << endl;
    }
    ~Foo() {
        cout << "Destroyed at " << this << endl;
    }
};
 
void double_free_anyway()
{
    Foo f(false);
    f.~Foo();

    // This constructor will throw, so the object is not considered constructed.
    new (&f) Foo(true);

    // The compiler re-destroys the old value at the end of the scope.
}
 
int main() {
    try {
        double_free_anyway();
    } catch(std::logic_error& e) {
        cout << "Error: " << e.what();
    }
}

这打印:

在 0x7fff41ebf03f 构建

在 0x7fff41ebf03f 销毁

在 0x7fff41ebf03f 销毁

错误:构造函数失败

于 2017-03-04T17:52:47.577 回答