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

dun*_*can 29 c++ destructor copy-constructor

在我学习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;
  }

  ...
};
Run Code Online (Sandbox Code Playgroud)

这似乎是避免代码重复同时确保"编程完整性"的好方法,但需要权衡浪费工作的风险 - 然后分配嵌套内存,而不是重新使用(正如其作者指出的那样).

但我不熟悉其核心语法:

this->UtilityClass::~UtilityClass()
Run Code Online (Sandbox Code Playgroud)

我假设这是一种调用对象的析构函数(破坏对象结构的内容)同时保持结构本身的方法.对于C++新手来说,语法看起来像是对象方法和类方法的奇怪混合.

有人可以向我解释这个语法,还是指向一个可以解释它的资源?

该呼叫与以下内容有何不同?

this->~UtilityClass()
Run Code Online (Sandbox Code Playgroud)

这是合法的电话吗?这是否会破坏对象结构(没有堆;从堆栈中弹出)?

Ben*_*igt 27

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


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

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

如果在对象的生命周期结束之后并且在重用或释放对象占用的存储之前,则在原始对象占用的存储位置创建新对象,指向原始对象的指针,引用引用原始对象,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用来操纵新对象,如果:

  • 新对象的存储完全覆盖原始对象占用的存储位置,以及
  • 新对象与原始对象的类型相同(忽略顶级cv-quali firs),以及
  • 原始对象的类型不是const-quali fi ed,如果是类类型,则不包含任何类型为const-qualified或引用类型的非静态数据成员,以及
  • 原始对象是类型最派生的对象(1.8),T新对象是类型最派生的对象T(也就是说,它们不是基类子对象).

在原始代码中,return *this;对象的使用和后续使用都是未定义的行为; 他们访问一个已被破坏的对象,而不是新创建的对象.

这在实践中也是一个问题:placement-new调用将设置对应于基类的v-table ptr,而不是对象的正确派生类型.

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


Mik*_*our 21

TL; DR不要这样做.

回答具体问题:

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

合格的电话:

this->UtilityClass::~UtilityClass()
Run Code Online (Sandbox Code Playgroud)

将专门调用此类的析构函数,而不是最派生类的析构函数.因此它只会破坏分配给的子对象,而不是整个对象.

一个不合格的电话:

this->~UtilityClass()
Run Code Online (Sandbox Code Playgroud)

将使用虚拟调度来调用派生最多的析构函数,从而销毁整个对象.

文章作者声称第一个是你想要的,所以你只分配给基础子对象,保留派生部分.但是,实际上你用一个基类型的新对象覆盖了对象的一部分; 你已经改变了动态类型,并且泄漏了旧对象的派生部分中的任何内容.在任何情况下这都是一件坏事.您还引入了一个异常问题:如果新对象的构造失败,则旧对象将处于无效状态,甚至无法安全销毁.

更新:您还有未定义的行为,因为如另一个答案所述,禁止使用placement-new在不同类型的对象(部分)之上创建对象.

对于非多态类型,编写复制赋值运算符的一种好方法是使用复制和交换习惯用法.这两者都可以通过重用复制构造函数来避免重复,并提供强大的异常保证 - 如果赋值失败,则原始对象不会被修改.

对于多态类型,复制对象更复杂,通常不能使用简单的赋值运算符.一种常见的方法是虚clone函数,每种类型都会覆盖以使用正确的类型动态分配自身的副本.


mas*_*oud 9

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

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

this->~MyClass();          // Virtual call
Run Code Online (Sandbox Code Playgroud)