是否允许显式调用析构函数,然后将 new 放置在具有固定生命周期的变量上?

Is it allowed to call destructor explicitly followed by placement new on a variable with fixed lifetime?

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

#include <vector>

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

但是,如果我们对 "resurrect" 对象调用 placement new 怎么办?

#include <vector>

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

更正式地说:

  1. 如果我在某些对象上显式调用析构函数,但 C++ 中会发生什么(我对 C++03 和 C++11 都感兴趣,如果有区别的话)第一个地方(例如,它是 local/global 变量或分配给 new),然后,在这个对象被破坏之前,调用 placement new 在它上面到 "restore" it?
  2. 如果没问题,是否可以保证对该对象的所有非 const 引用也都可以,只要我在对象 "dead" 时不使用它们?
  3. 如果是这样,是否可以使用非常量引用之一放置 new 来复活对象?
  4. 常量引用呢?

示例用例(尽管这个问题更多是关于好奇心):我想 "re-assign" 一个没有 operator=.

的对象

我看过 this 问题,它说 "overriding" 具有非静态 const 成员的对象是非法的。因此,让我们将此问题的范围限制为没有任何 const 成员的对象。

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

下面是展示此行为的示例程序 (Ideone link):

#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();
    }
}

这会打印:

Constructed at 0x7fff41ebf03f

Destroyed at 0x7fff41ebf03f

Destroyed at 0x7fff41ebf03f

Error: Constructor failed

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

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

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

  • (8) If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

    • (8.1) the storage for the new object exactly overlays the storage location which the original object occupied, and
    • (8.2) the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
    • (8.3) the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
    • (8.4) the original object was a most derived object (1.8) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).
  • (9) If a program ends the lifetime of an object of type T with static (3.7.1), thread (3.7.2), or automatic (3.7.3) storage duration and if T has a non-trivial destructor, the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined. This is true even if the block is exited with an exception.

有理由手动 运行 析构函数并放置新的。像 operator= 这样简单的东西不是其中之一 ,除非您正在编写自己的 variant/any/vector 或类似类型。

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