自我毁灭:this->MyClass::~MyClass() vs. this->~MyClass()

Self destruction: this->MyClass::~MyClass() vs. this->~MyClass()

本文关键字:MyClass gt this- vs 毁灭 自我      更新时间:2023-10-16

在我学习c++的过程中,我偶然发现了一篇文章"编写复制构造函数和赋值操作符",该文章提出了一种避免复制构造函数和赋值操作符之间代码重复的机制。

要总结/复制该链接的内容,建议的机制是:

struct UtilityClass
{
  ...
  UtilityClass(UtilityClass const &rhs)
    : data_(new int(*rhs_.data_))
  {
    // nothing left to do here
  }
  UtilityClass &operator=(UtilityClass const &rhs)
  {
    //
    // Leaves all the work to the copy constructor.
    //
    if(this != &rhs)
    {
      // deconstruct myself    
      this->UtilityClass::~UtilityClass();
      // reconstruct myself by copying from the right hand side.
      new(this) UtilityClass(rhs);
    }
    return *this;
  }
  ...
};

这似乎是一种避免代码重复同时确保"编程完整性"的好方法,但需要权衡浪费精力释放然后分配嵌套内存的风险,而这些嵌套内存可以被重用(正如其作者所指出的)。

但是我不熟悉它的核心语法:

this->UtilityClass::~UtilityClass()

我认为这是一种调用对象析构函数(销毁对象结构的内容)同时保持结构本身的方法。对于c++新手来说,这种语法看起来像是对象方法和类方法的奇怪混合。

有人可以解释这个语法给我,或指出我的资源,解释它?

这个调用与下面的调用有什么不同?

this->~UtilityClass()

这是一个合法的呼叫吗?这是否会额外破坏对象结构(从堆中释放;弹出堆栈)?

TL;DR版本:不要遵循该链接的作者给出的任何建议


链接表明,只要不使用虚析构函数调用,这种技术就可以在基类中使用,因为这样做会破坏派生类的部分,这不是基类operator=的责任。

这种推理完全失败了。该技术永远不能在基类中使用。原因是c++标准只允许用完全相同类型的另一个对象替换一个对象(参见标准的第3.8节):

,如果一个对象的生命周期已经结束,后重用对象占用的存储或释放,创建一个新的对象在原始对象占用的存储位置,一个指针,指向原始对象,称为原始对象的引用,或者原始对象的名称会自动引用新对象,一旦新对象的生命周期已经开始,可以用来操纵新对象,如果:

  • 新对象的存储完全覆盖原始对象占用的存储位置,
  • 新对象与原始对象的类型相同(忽略顶级cv限定符),并且
  • 原始对象的类型不是const限定的,如果是类类型,则不包含任何类型为const限定的或引用类型的非静态数据成员,并且
  • 原对象是T类型的最派生对象(1.8),新对象是T类型的最派生对象(即它们不是基类子对象)。

在原始代码中,return *this;和随后的对象使用都是未定义的行为;它们访问的是已销毁的对象,而不是新创建的对象。

这在实践中也是一个问题:place -new调用将建立一个与基类对应的v-table ptr,而不是对象的正确派生类型。

即使对于叶类(非基类),该技术也是非常值得怀疑的。

不要这样做。

回答具体问题:

在这个特殊的例子中,没有区别。正如您所链接到的文章中所解释的那样,如果这是一个多态基类,具有虚拟析构函数,则会有所不同。

合格呼叫:

this->UtilityClass::~UtilityClass()

将专门调用该类的析构函数,而不是最派生类的析构函数。所以它只销毁被赋值的子对象,而不是整个对象。

非限定呼叫:

this->~UtilityClass()

将使用虚拟分派调用最派生的析构函数,销毁整个对象。

文章作者声称第一个是您想要的,因此您只分配给基本子对象,而保留派生部分不变。然而,你实际做的是用基类型的新对象覆盖对象的一部分;您已经更改了动态类型,并且泄露了旧对象派生部分中的内容。这在任何情况下都是不好的。您还引入了一个异常问题:如果新对象的构造失败,那么旧对象将处于无效状态,甚至不能安全地销毁。

UPDATE:你也有未定义的行为,因为,正如另一个答案所描述的,禁止使用place -new在不同类型对象的顶部(部分)创建对象。

对于非多态类型,编写复制赋值操作符的好方法是使用复制-交换习惯。这既通过重用复制构造函数避免了重复,又提供了一个强大的异常保证——如果赋值失败,则原始对象未被修改。

对于多态类型,复制对象更复杂,通常不能用简单的赋值操作符完成。一种常见的方法是使用虚clone函数,每个类型都覆盖该函数,以便动态分配具有正确类型的自身副本。

你可以决定如何调用析构函数:

this->MyClass::~MyClass(); // Non-virtual call
this->~MyClass();          // Virtual call