【问题标题】:Is `new (this) MyClass();` undefined behaviour after directly calling the destructor?`new (this) MyClass();` 是直接调用析构函数后未定义的行为吗?
【发布时间】:2011-09-07 14:57:05
【问题描述】:

this question of mine 中,@DeadMG 表示通过this 指针重新初始化一个类是未定义的行为。在某处的标准中是否有提及?

例子:

#include <iostream>

class X{
  int _i;
public:  
  X() : _i(0) { std::cout << "X()\n"; }
  X(int i) : _i(i) { std::cout << "X(int)\n"; }

  ~X(){ std::cout << "~X()\n"; }

  void foo(){
    this->~X();
    new (this) X(5);
  }

  void print_i(){
    std::cout << _i << "\n";
  }
};

int main(){
  X x;
  x.foo();
  // mock random stack noise
  int noise[20];
  x.print_i();
}

Example output at Ideone(我知道 UB 也可以是“看似正确的行为”)。
请注意,我没有在类之外调用析构函数,以不访问生命周期已结束的对象。另请注意,@DeadMG 表示直接调用析构函数是可以的,只要它为每个构造函数调用一次。

【问题讨论】:

  • 这种结构(调用析构函数,然后调用带有放置 new 的构造函数)是实现赋值运算符的一种流行方式,直到发现异常不安全。我不记得有人说它是UB,没有例外。可能有虚函数和多重继承的情况是UB。
  • 如果是UB,那只是因为使用了this。如果是这种情况,您仍然可以通过在调用析构函数之前获取this 的副本来解决它。
  • +1 程序员。事实上,C++0x 标准 (FDIS) 包含手动销毁 + 放置构造的示例,因此它可能没有那么糟糕。当某些成员具有非平凡的构造函数/析构函数时,它在 §9.5/4 中作为更改联合的 active 成员的方法:u.m.~M(); new (&amp;u.n) N; 现在,示例不会从inside 一个方法,但我不知道这是否有任何区别。
  • @curiousguy 指向存储的指针将保持有效。我在猜测是否有关于使用 this 作为关键字的规则,而不是它的特定值。也就是说,我在考虑之前是否可以访问该值(由于其他要求,这显然是正确的),是否允许使用this
  • @LucDanton this 只是一个关键字,用于获取非静态成员函数的隐式参数的值。即使在从未使用过this-&gt;~T();delete this;new (this) T; 的程序中,this 也可以引用尚未完全构造的对象,在某些情况下,例如甚至没有开始子对象构造。

标签: c++ this undefined-behavior placement-new


【解决方案1】:

如果不与堆栈展开冲突就可以了。

你销毁对象,然后通过指针重建它。如果您需要构造和销毁一个没有默认构造函数的对象数组,您会这样做。

问题是这是异常不安全的。如果调用构造函数抛出异常,栈解开,第二次调用析构函数怎么办?

{
   X x;
   x.foo(); // here ~X succeeds, then construction fails
} //then the destructor is invoked for the second time.

这方面特别是未定义的行为。

【讨论】:

  • 啊,关于异常安全的要点。你能想出什么办法让这个例外安全吗?
  • 什么会使成员函数为此特别? (例如,没有人假装delete this 是 UB。)
  • @Xeo 将构造函数调用包装在 try ... catch 块中,并以不调用 UB 的方式处理异常。 std::abort 想到这里。或者,因为这是一个成员,所以调用一个私有的 nothrow 构造函数,将对象置于不可用但可销毁的状态,然后(重新)抛出。用户将无法看到处于该状态的对象,并且堆栈展开将起作用。但请不要将最后一个解决方案视为实际可行的事情(大部分时间)。
  • @Xeo:我认为在一般情况下不能保证异常安全。问题是在你构造之前,你必须已经破坏了,如果构造失败,状态就不再是原来的状态了。在具有 nothrow 移动构造函数的类中,您可以完全避免该问题:构造一个局部变量,一旦构造就将其移动到 *this。在这种情况下,它将是异常安全的(假设析构函数也是 nothrow)。
  • 啊,顺便说一句,如果你实现了一个 move 构造函数,你最好让它不抛出。否则会导致灾难。 IE。您可能会遇到一些 STL 容器的问题:在缓冲区增长期间,vector 会将对象从一个位置 move 到另一个位置,如果其中一个移动操作抛出,vector 将处于与某些位置不一致的状态原始向量中的对象无效并且一些正确...我没有查看unordered_map的详细信息,但我的猜测是会出现类似的问题。
【解决方案2】:

除了@sharptooth 的回答。我只是想知道还有2个案例。至少它们值得一提。

(1) 如果使用malloc() 分配的堆上的指针调用foo()调用构造函数。能安全吗?

(2) 如果派生类调用foo() 怎么办。这会是一个好的行为吗?例如

struct Y : X {};  // especially when there is virtual ~X()
int main ()
{
  Y y;
  y.foo();
}

【讨论】:

  • (1) 假设您的意思是指向使用malloc 分配的内存的指针,那么除非使用placement new 来创建对象,否则在其上调用函数成员是UB。那么用户有责任在free内存之前显式调用析构函数。
  • (2) 因为X 的析构函数不是虚拟的,this-&gt;~X() 正确地破坏了YX 子对象,然后重新创建。如果析构函数是虚拟的,foo 可以使用 this-&gt;X::X() 来做同样的事情。
  • (2) 在函数(main)的最后,会调用y.~Y()。调用y.foo()后,y处的内存包含一个X;调用 y.~Y() 是未定义的行为。 (@Luc Danton 如果派生类不再存在,您就不能“重建”派生类的基类。)
  • @JamesKanze 感谢您的澄清; AProgrammer 展示了这确实会出错。
  • 上述答案中的两种情况都会导致UB。一般来说,应该避免使用这种成语,因为它最终会导致 UB。
猜你喜欢
  • 2020-09-25
  • 1970-01-01
  • 2013-09-23
  • 2017-09-28
  • 2011-06-19
  • 1970-01-01
  • 1970-01-01
  • 2011-03-18
  • 1970-01-01
相关资源
最近更新 更多