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

Posted

技术标签:

【中文标题】是否允许显式调用析构函数,然后在具有固定生命周期的变量上放置 new?【英文标题】:Is it allowed to call destructor explicitly followed by placement new on a variable with fixed lifetime? 【发布时间】:2017-07-24 17:54:34 【问题描述】:

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

#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;

更正式地说:

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

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

我看到this 的问题说具有非静态const 成员的“覆盖”对象是非法的。所以,让我们把这个问题的范围限制在没有任何const 成员的对象上。

【问题讨论】:

可能相关:***.com/q/8829548 这似乎是一个问题。 2 特别是一整罐蠕虫,可能涉及std::launder [basic.life]。读一遍,然后再读一遍。 编辑这个问题可能会更好,这样它读起来不像是在征求意见(我读到“可以吗”作为与代码风格相关的东西),而更像是在询问有效性(即基本上s/是否可以/是否有效/)。我的意思是,在实际阅读您的问题之后,很明显您是在询问后者,但明确总是有帮助的。 【参考方案1】:

首先,[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) 原始对象是类型最衍生的对象 (1.8) T 和新对象是最衍生的 T 类型的对象(也就是说,它们不是基类子对象)。

(9) 如果程序以静态 (3.7.1)、线程 (3.7.2) 或自动 (3.7.3) 存储持续时间结束 T 类型对象的生命周期,并且如果 T 具有非平凡的析构函数,程序必须确保 当隐式析构函数调用发生时,原始类型占用相同的存储位置;否则程序的行为是未定义的。即使块因异常退出也是如此。

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

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

【讨论】:

【参考方案2】:

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

这是一个展示此行为的示例程序 (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();
    

打印出来:

在 0x7fff41ebf03f 构建

在 0x7fff41ebf03f 销毁

在 0x7fff41ebf03f 销毁

错误:构造函数失败

【讨论】:

有趣的地方。在大多数情况下,您可以通过在使用placement new 时先构建然后移动来解决此问题。 @chris 我不这么认为,因为没有什么可以阻止移动构造函数与我的示例构造函数一样抛出。在任何一种情况下,原始对象仍然会被双重破坏。 强烈建议使用noexcept 移动构造函数。 std::vector 利用这些优势来优化其运营。大多数类都应该有一个不抛出的移动构造函数,这就是我在大多数情况下所说的原因。 @chris:但你打算从哪里搬走? 哦,没关系,我明白你在说什么。在调用原对象的析构函数之前构造一个对象,然后从它开始移动。

以上是关于是否允许显式调用析构函数,然后在具有固定生命周期的变量上放置 new?的主要内容,如果未能解决你的问题,请参考以下文章

在调用析构函数之前对象的生命周期已经结束?

c++类中 各种成员的生命周期?

在一个派生类对象结束其生命周期时析构函数的调用顺序

阻止使用静态生命周期创建对象

C++ 堆栈分配对象,显式析构函数调用

C++:对象和类|| 类的构造函数与析构函数