C ++中的复制构造函数和=运算符重载:通用函数可能吗?

Posted

技术标签:

【中文标题】C ++中的复制构造函数和=运算符重载:通用函数可能吗?【英文标题】:Copy constructor and = operator overload in C++: is a common function possible? 【发布时间】:2010-12-16 14:50:11 【问题描述】:

由于复制构造函数

MyClass(const MyClass&);

和 = 运算符重载

MyClass& operator = (const MyClass&);

有几乎相同的代码,相同的参数,只是返回不同,是否有可能有一个共同的功能供他们使用?

【问题讨论】:

"...有几乎相同的代码..."?嗯……你一定是做错了什么。尽量减少为此使用用户定义函数的需要,让编译器完成所有脏活。这通常意味着将资源封装在它们自己的成员对象中。你可以给我们看一些代码。也许我们有一些好的设计建议。 Reducing code duplication between operator= and the copy constructor的可能重复 【参考方案1】:

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

MyClass(const MyClass& other)

    operator=(other);

但是,在处理旧状态和自分配产生的问题时,提供一个好的operator= 是一个挑战。此外,所有成员和基础都首先默认初始化,即使它们要从other 分配。这甚至可能并非对所有成员和基都有效,即使它有效,它在语义上也是多余的,并且实际上可能很昂贵。

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

MyClass& operator=(const MyClass& other)

    MyClass tmp(other);
    swap(tmp);
    return *this;

甚至:

MyClass& operator=(MyClass other)

    swap(other);
    return *this;

swap 函数通常很容易编写,因为它只是交换内部的所有权,而不必清理现有状态或分配新资源。

复制和交换习惯用法的优点是它是自动自赋值安全的,并且 - 假设交换操作是无抛出的 - 也是非常安全的异常。

为了保证异常安全,“手写”分配操作员通常必须在取消分配受让人的旧资源之前分配新资源的副本,以便在分配新资源时发生异常,旧状态仍然可以被退回。所有这些都通过复制和交换免费提供,但通常更复杂,因此容易出错,从头开始。

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

通常使用成员swapstd::swap 有效,并且保证所有基本类型和指针类型都“不抛出”。大多数智能指针也可以通过不抛出保证进行交换。

【讨论】:

其实它们并不是常用的操作。当复制 ctor 首次初始化对象的成员时,赋值运算符会覆盖现有值。考虑到这一点,从复制 ctor 中调用 operator= 实际上非常糟糕,因为它首先将所有值初始化为某个默认值,然后立即用其他对象的值覆盖它们。 也许在“我不推荐”之后添加“而且任何 C++ 专家也不推荐”。有人可能会出现并没有意识到您不仅表达了个人的少数偏好,而且还表达了那些真正考虑过它的人的一致意见。而且,好吧,也许我错了,一些 C++ 专家确实推荐它,但我个人仍然会放下挑战,让某人为该推荐提供参考。 很公平,我已经为你投票了 :-)。我认为,如果某件事被广泛认为是最佳实践,那么最好这样说(如果有人说它毕竟不是最好的,请再看一遍)。同样,如果有人问“是否可以在 C++ 中使用互斥锁”,我不会说“一个相当常见的选择是完全忽略 RAII,并编写在生产中死锁的非异常安全代码,但越来越流行编写体面的工作代码”;-) +1。我认为总是需要分析。我认为在某些情况下(对于轻量级类),复制 ctor 和赋值运算符都使用 assign 成员函数是合理的。在其他情况下(资源密集型/用例,句柄/正文),复制/交换当然是一种方式。 @litb:我对此感到惊讶,所以我查看了异常 C++ 中的第 41 项(这个 gotw 变成了),这个特别的建议已经消失,他建议使用复制和交换代替它。相当偷偷摸摸地,他同时放弃了“问题 4:分配效率低下”。【参考方案2】:

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

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

现在通常被认为是规范的成语是使用swap,正如查尔斯建议的那样:

MyClass& operator=(MyClass other)

    swap(other);
    return *this;

这使用了复制构造(注意 other 被复制)和破坏(它在函数末尾被破坏)——它也以正确的顺序使用它们:构造(可能会失败)在破坏之前(不能失败)。

【讨论】:

应该将swap 声明为virtual @Johannes:虚函数用于多态类层次结构。赋值运算符用于值类型。两者几乎没有混合。【参考方案3】:

有件事让我烦恼:

MyClass& operator=(const MyClass& other)

    MyClass tmp(other);
    swap(tmp);
    return *this;

首先,当我在思考“复制”时阅读“交换”这个词会激怒我的常识。另外,我质疑这个花哨的把戏的目标。是的,构建新(复制)资源的任何异常都应该在交换之前发生,这似乎是一种确保所有新数据在上线之前都已填充的安全方法。

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

所以,我建议不要“swap”,而是做一个更自然的“transfer”:

MyClass& operator=(const MyClass& other)

    MyClass tmp(other);
    transfer(tmp);
    return *this;

临时对象仍在构建中,但下一个立即行动是释放目标的所有当前资源,然后再将源的资源移动(并清空,以免被双重释放)。

我建议使用 construct, destruct, move 而不是 construct, move, destruct 。此举是最危险的动作,是在其他一切都解决后最后采取的动作。

是的,销毁失败在任一方案中都是一个问题。数据要么已损坏(当您认为它不存在时被复制),要么已丢失(当您认为它不存在时被释放)。丢失总比损坏好。没有数据比坏数据更好。

转移而不是交换。这是我的建议。

【讨论】:

析构函数不能失败,因此不会出现析构异常。而且,如果移动是最危险的操作,我不明白移动破坏后面的移动有什么好处?即,在标准方案中,移动失败不会破坏旧状态,而您的新方案会破坏。所以为什么?另外,First, reading the word "swap" when my mind is thinking "copy" irritates -> 作为库编写者,您通常知道常用做法(复制+交换),关键是my mind。您的思想实际上隐藏在公共界面后面。这就是可重用代码的全部意义所在。

以上是关于C ++中的复制构造函数和=运算符重载:通用函数可能吗?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 数组对复制构造函数和赋值运算符

常见的构造函数类型

可克隆层次结构中的C ++副本构造函数和赋值

初始化列表:复制构造函数和赋值运算符 = 冗余?

重写和重载的区别

C++类和对象中