【问题标题】:Self destruction: this->MyClass::~MyClass() vs. this->~MyClass()自毁:this->MyClass::~MyClass() vs. this->~MyClass()
【发布时间】:2014-10-11 11:19:17
【问题描述】:

在学习 C++ 的过程中,我偶然发现了文章 Writing Copy Constructors and Assignment Operators,该文章提出了一种机制,可以避免复制构造函数和赋值运算符之间的代码重复。

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

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()

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

【问题讨论】:

  • 如果析构函数删除data OK。否则会造成内存泄漏。
  • 这是一个异常不安全的雷区(这就是为什么 Sutter 非常正确地贬低它的原因)。您可以考虑使用 copy-and-swap idiom 来避免重复并提供强大的异常保证。
  • 虽然我强烈建议您不要使用此模式,但两个调用之间不应有任何区别,您可以将第二个版本 this->~UitilityClass() 视为简写。
  • @STNYU 不正确,考虑新的展示位置

标签: c++ destructor copy-constructor


【解决方案1】:

TL;DR 版本:请勿听从该链接作者提供的任何建议


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

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

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

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

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

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

即使对于叶类(非基类),该技术也存在很大问题。

【讨论】:

  • 我不明白为什么这个placement-new 不符合*this 引用新创建的对象的要求。当标准说“最派生对象”时,这是否意味着UtilityClass 必须 是某个基类的最终继承类,还是意味着它必须是一个非派生类?如果是后者,它看起来好像placement-new 符合最后一个要点:原始对象的类型为UtilityClass,而它被替换为UtilityClass 类型的对象。
  • @0x499602D2:如果您阅读链接,可以清楚地说明原始对象源自UtilityClass
  • 顺便说一句,我想说您的 TL;DR 版本还扩展到该页面中几乎所有其他 C++ 建议。有些写得很糟糕,有些只是奇怪/不常见,有些是完全错误/危险的......
  • @anderas:我的 TL;DR 措辞旨在适用于来自该来源的所有建议。在这里,我们有一个案例,有人推荐不良做法,不能以无知为借口;作者完全清楚,他们推荐的内容被世界专家特别指出为一种应避免的模式,但与其试图理解其基本原理,不如构建一个有利于他们观点的错误论据,甚至没有对其进行测试。这就是为什么我建议忽略该作者的建议如此广泛的原因。
  • @BenVoigt 100% 同意。抱歉不清楚,我的意思是写“...来自该作者的建议”,因为(对我而言)您的文字看起来只是关于链接页面,而不是整个网站。
【解决方案2】:

TL;DR 不要这样做。

回答具体问题:

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

一个合格的调用:

this->UtilityClass::~UtilityClass()

会专门调用这个类的析构函数,而不是最派生类的析构函数。所以它只会破坏被分配的子对象,而不是整个对象。

一个不合格的电话:

this->~UtilityClass()

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

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

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

对于非多态类型,编写复制赋值运算符的好方法是使用copy-and-swap idiom。这既可以通过重用复制构造函数来避免重复,又可以提供强大的异常保证——如果分配失败,则原始对象不会被修改。

对于多态类型,复制对象比较复杂,一般不能用简单的赋值运算符来完成。一种常见的方法是一个虚拟的clone 函数,每个类型都会覆盖它以动态分配具有正确类型的自身副本。

【讨论】:

  • 实际上严禁使用placement-new替换基础子对象;看我的回答。
  • @BenVoigt:谢谢,我不知道这是被禁止还是仅仅是错误的。
  • 使用克隆函数和使用shared_from_this 有什么区别(如果它们甚至相关......)?
  • @0x499602D2:克隆函数创建一个新对象;共享指针共享现有对象的所有权。
  • @0x499602D2: 假设Animal -> Cat -> Tiger -- clone 会返回底层动物的新副本,即使你已经Animal*; shared_from_this 将返回一个 shared_ptr<Animal> 给一个已经存在的动物对象,不管你有什么指针,无论是 Animal*Cat* 还是 Tiger*
【解决方案3】:

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

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

this->~MyClass();          // Virtual call

【讨论】:

    猜你喜欢
    • 2011-10-11
    • 1970-01-01
    • 2013-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多