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

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

提问人:yeputons 提问时间:3/5/2017 最后编辑:Communityyeputons 更新时间:3/26/2017 访问量:1214

问:

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

#include <vector>

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

但是,如果我们称放置新来“复活”对象呢?

#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 都感兴趣,如果存在差异),如果我在某个对象上显式调用析构函数,而该对象首先不是用 new 构造的(例如,它是局部/全局变量或被分配了),然后,在这个对象被析构之前, 将放置新称为“恢复”它?new
  2. 如果可以,只要我在对象“死”时不使用它们,是否可以保证对该对象的所有非常量引用也可以?
  3. 如果是这样,是否可以使用一个非常量引用来放置新对象以复活对象?
  4. 常量引用呢?

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

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

C++ C++11 生存 期放置 - 新显 式析构函数调用

评论

1赞 Kerrek SB 3/5/2017
也许相关:stackoverflow.com/q/8829548
1赞 Barry 3/5/2017
这似乎是一个多问题。特别是 2 是一整罐蠕虫,可能涉及 std::launder
0赞 T.C. 3/5/2017
[basic.life].读一读,再读一遍。
0赞 3/10/2017
编辑这个问题可能会更好,这样它读起来就不像征求意见了(我把“可以吗”看作是与代码风格相关的东西),而更像是问有效性(即基本上是 s/is it ok/is it valid/)。我的意思是,在实际阅读了你的问题之后,很明显你问的是后者,但明确总是有帮助的。

答:

8赞 George Hilliard 3/5/2017 #1

这不是一个好主意,因为如果新对象的构造函数引发异常,您最终仍可能运行析构函数两次。也就是说,析构函数将始终在作用域的末尾运行,即使您在例外情况下离开作用域也是如此。

下面是展示此行为的示例程序(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 年被摧毁

错误:构造函数失败

评论

0赞 chris 3/5/2017
有趣的一点。在大多数情况下,您可以通过先构造,然后在使用放置新时移动来解决这个问题。
0赞 George Hilliard 3/5/2017
@chris我不这么认为,因为没有什么能阻止移动构造函数抛出与我的示例构造函数相同的内容。无论哪种情况,原始对象仍然会被双重销毁。
1赞 chris 3/5/2017
强烈建议使用移动构造函数。 利用这些来优化其运营。大多数类都应该有一个非抛出移动构造函数,所以这就是我在大多数情况下说的原因。noexceptstd::vector
0赞 Benjamin Lindley 3/5/2017
@chris:但是你打算从什么地方搬走呢?
0赞 Benjamin Lindley 3/5/2017
哦,没关系,我明白你在说什么。在调用原始析构函数之前构造一个对象,然后从该对象移动。
14赞 Yakk - Adam Nevraumont 3/5/2017 #2

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

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

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

  • (8) 如果在对象的生存期结束后,在该对象占用的存储被重复使用或释放之前,在原始对象占用的存储位置创建了一个新对象,则指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用新对象,并且, 新对象的生存期开始后,可用于操作新对象,如果满足以下条件:

    • (8.1) 新对象的存储与原始对象所占用的存储位置完全重叠,并且
    • (8.2) 新对象与原始对象属于同一类型(忽略顶级 cv 限定符),并且
    • (8.3) 原始对象的类型不是常量限定的,如果是类类型,则不包含任何非静态 其类型为 const 限定或引用类型的数据成员,以及
    • (8.4) 原始对象是类型 (1.8) 的最派生对象 T和新对象是派生的 类型为 T 的对象(即,它们不是基类子对象)。
  • (9) 如果程序以静态 (3.7.1)、线程 (3.7.2) 或自动 (3.7.3) 存储持续时间结束 T 类型的对象的生存期,并且如果 T 具有非平凡的析构函数,则程序必须确保 当隐式析构函数调用发生时,原始类型占用相同的存储位置;否则,程序的行为是未定义的。即使退出块并出现异常,也是如此。

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

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