在我学习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()
这是合法的电话吗?这是否会破坏对象结构(没有堆;从堆栈弹出)?
答案 0 :(得分:27)
TL; DR版本:请勿遵循该链接的作者提供的任何建议
该链接表明,只要不使用虚拟析构函数调用,就可以在基类中使用此技术,因为这样做会破坏派生类的部分,这不是基类的责任{{ 1}}。
这种推理完全失败了。该技术永远不会在基类中使用。原因是C ++标准只允许使用完全相同类型的另一个对象就地替换对象(参见标准3.8节):
如果在对象的生命周期结束之后并且在重用或释放对象占用的存储之前,则在原始对象占用的存储位置创建新对象,指向原始对象的指针,引用原始对象的引用,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用于操作新对象,如果:
- 新对象的存储空间正好覆盖原始对象占用的存储位置,
- 新对象与原始对象的类型相同(忽略顶级cv-quali firs),
- 原始对象的类型不是const-quali fi ed,如果是类类型,则不包含任何类型为const-qualified或引用类型的非静态数据成员,并且
- 原始对象是类型
operator=
的派生程度最高的对象(1.8),新对象是类型T
的派生程度最高的对象(也就是说,它们不是基类子对象) )。强>
在原始代码中,T
和后续使用对象都是未定义的行为;他们访问一个已被破坏的对象,而不是新创建的对象。
这在实践中也是一个问题:placement-new调用将设置一个对应于基类的v-table ptr,而不是对象的正确派生类型。
即使对于叶类(非基类),这种技术也是值得怀疑的。
答案 1 :(得分:21)
TL; DR不要这样做。
回答具体问题:
在这个特定的例子中,没有区别。正如您链接到的文章中所解释的那样,如果这是一个具有虚拟析构函数的多态基类,则会有所不同。
合格的电话:
this->UtilityClass::~UtilityClass()
将专门调用此类的析构函数,而不是最派生类的析构函数。因此它只会破坏分配给的子对象,而不是整个对象。
不合格的电话:
this->~UtilityClass()
将使用虚拟调度来调用派生最多的析构函数,从而破坏整个对象。
文章作者声称第一个是你想要的,所以你只需要分配给基础子对象,保留派生部分。但是,实际上你用一个基类型的新对象覆盖了对象的一部分;你已经改变了动态类型,并且泄露了旧对象的派生部分中的任何内容。在任何情况下这都是件坏事。您还引入了一个异常问题:如果新对象的构造失败,则旧对象将处于无效状态,甚至无法安全销毁。
更新:您还有未定义的行为,因为如另一个答案中所述,它被禁止使用placement-new在(部分)不同的上面创建对象打字对象。
对于非多态类型,编写复制赋值运算符的好方法是使用copy-and-swap idiom。通过重用复制构造函数来避免重复,并提供强大的异常保证 - 如果赋值失败,则原始对象不会被修改。
对于多态类型,复制对象更复杂,并且通常不能通过简单的赋值运算符来完成。一种常见的方法是虚拟clone
函数,每种类型都会覆盖以动态分配具有正确类型的自身副本。
答案 2 :(得分:9)
您可以决定如何调用析构函数:
this->MyClass::~MyClass(); // Non-virtual call
this->~MyClass(); // Virtual call