在C ++中复制构造函数和=运算符重载:是否可能使用通用函数?


87

由于复制构造函数

MyClass(const MyClass&);

和=运算符重载

MyClass& operator = (const MyClass&);

具有几乎相同的代码,相同的参数,并且仅在返回值上有所不同,是否可以为它们两者使用共同的功能?


6
“ ...具有几乎相同的代码...”?嗯...你一定做错了。尝试最小化为此使用用户定义函数的需求,并让编译器完成所有肮脏的工作。这通常意味着将资源封装在它们自己的成员对象中。您可以向我们展示一些代码。也许我们有一些好的设计建议。
sellibitze

Answers:


121

是。有两种常见的选择。一种方法(通常不鼓励使用)是operator=从复制构造函数中显式调用:

MyClass(const MyClass& other)
{
    operator=(other);
}

但是,operator=在处理旧状态和自我分配引起的问题时,提供一种物品是一个挑战。同样,所有成员和基础都将首先默认初始化,即使要将它们分配给from other。这甚至可能对所有成员和基地都无效,即使它是有效的,它在语义上也是多余的,并且实际上可能很昂贵。

越来越流行的解决方案是operator=使用复制构造函数和交换方法来实现。

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

甚至:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

一个swap函数通常是简单的写,因为它只是交换了内部的所有权,并没有清理现有状态或分配新的资源。

复制和交换惯用语的优点在于,它是自动自我分配安全的,并且-只要交换操作为非抛出条件-也是异常例外安全的。

为了严格保护例外,“手写”书面分配操作员通常必须先分配新资源的副本,然后再取消分配受让人的旧资源,这样,如果在分配新资源时发生异常,仍然可以将旧状态返回到。所有这些都是通过复制和交换免费提供的,但通常更复杂,因此从头开始就容易出错。

要注意的一件事是确保swap方法是一个真正的交换,而不是std::swap使用复制构造函数和赋值运算符本身的默认交换。

通常使用成员swap方式。std::swap有效,并且所有基本类型和指针类型均保证“不抛出”。大多数智能指针也可以与无掷保证互换。


3
实际上,它们不是常见的操作。当复制ctor首次初始化对象的成员时,赋值运算符将覆盖现有值。考虑到这一点,operator=复制ctor的所有功能实际上是非常糟糕的,因为它首先将所有值初始化为某个默认值,然后才用另一个对象的值覆盖它们。
09年

14
可能在“我不推荐”中添加“而且C ++专家也没有”。可能有人会出现,并且没有意识到您并不仅仅是在表达个人少数派的偏爱,而是表达了真正考虑过的人的定居共识。而且,好吧,也许我错了,有些C ++专家确实推荐了它,但是我个人还是会给别人提出建议的参考书。
史蒂夫·杰索普

4
公平地说,无论如何,我已经支持了您:-)。我认为,如果某件事被广泛认为是最佳做法,那么最好这样说(如果有人说这毕竟不是最好的话,请再来看一遍)。同样,如果有人问“是否可以在C ++中使用互斥锁”,我不会说“一个相当普遍的选择是完全忽略RAII,并编写会在生产中造成死锁的非异常安全代码,但是编写它变得越来越流行体面的工作代码” ;-)
史蒂夫·杰索普

4
+1。而且我认为始终需要进行分析。我认为assign在某些情况下(对于轻量级类)让复制ctor和赋值运算符同时使用成员函数是合理的。在其他情况下(资源密集型/使用情况,句柄/正文),复制/交换是当然的方法。
Johannes Schaub-litb

2
@litb:我对此感到惊讶,因此我在Exception C ++中查找了第41项(此内容已转化为该内容),并且此特定建议已消失,他建议在其位置进行复制和交换。相反,他同时偷偷丢掉了“问题4:分配工作效率低下”。
CB Bailey

13

复制构造函数对曾经是原始内存的对象执行首次初始化。赋值运算符OTOH用新值覆盖现有值。通常涉及解散旧资源(例如,内存)并分配新资源。

如果两者之间有相似之处,那就是赋值运算符执行销毁和复制构造。一些开发人员过去实际上是通过就地销毁,然后是布局复制构造来实现分配的。但是,这是一个非常糟糕的主意。(如果这是派生类的分配期间调用的基类的赋值运算符,该怎么办?)

如今,通常被视为规范用语的是swapCharles所建议的:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

这使用了复制构造(注意other已复制)和销毁(在函数末尾对其进行了销毁)-并且它也以正确的顺序使用它们:销毁(必须失败)之前的构造(可能会失败)。


应该swap申报virtual

1
@Johannes:在多态类层次结构中使用了虚函数。赋值运算符用于值类型。两者几乎没有融合。
2016年

-3

有些事情困扰着我:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

首先,当我的思想是“复制”时读“交换”一词会激怒我的常识。另外,我对这个幻想的目的提出了质疑。是的,在构造新的(复制的)资源时,任何异常都应该发生在交换之前,这似乎是一种确保所有新数据都被填充后才能上线的安全方法。

没关系。那么,交换之后发生的异常又如何呢?(当临时对象超出范围时旧资源被破坏时)从分配用户的角度来看,该操作失败了,但没有失败。它具有巨大的副作用:复制确实发生了。只是某些资源清理失败。即使从外部看来操作失败,目标对象的状态也已更改。

因此,我建议不要“交换”来进行更自然的“转移”:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

仍然有临时对象的构造,但是下一个立即的操作是释放源的所有当前资源,然后再将源的资源移动(并为NULL,这样它们就不会被双重释放)。

我提出了{构造,破坏,移动}而不是{构造,移动,破坏}。此举是最危险的举动,是在解决所有其他问题后采取的最后一步。

是的,无论哪种方案,销毁失败都是一个问题。数据已损坏(在您不认为是复制时复制)或丢失(在您不认为是被释放时释放)。丢失总比损坏好。没有数据比坏数据更好。

转移而不是交换。无论如何,这是我的建议。


2
析构函数一定不能失败,因此不会发生销毁异常。而且,如果搬​​家是最危险的行动,我不知道搬迁到破坏之后会有什么好处?也就是说,在标准方案中,移动失败不会破坏旧状态,而新方案会破坏旧状态。所以为什么?另外,First, reading the word "swap" when my mind is thinking "copy" irritates->作为图书馆作家,您通常会知道一些常见的做法(copy + swap),而症结所在my mind。您的想法实际上隐藏在公共界面的后面。这就是可重用代码的全部意义。
塞巴斯蒂安·马赫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.