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

Copy constructor and = operator overload in C++: is a common function possible?

提问人:MPelletier 提问时间:11/14/2009 最后编辑:CerbrusMPelletier 更新时间:10/4/2023 访问量:100328

问:

由于复制构造函数

MyClass(const MyClass&);

和 = 运算符重载

MyClass& operator = (const MyClass&);

具有几乎相同的代码,相同的参数,并且仅在返回值上有所不同,是否可以有一个共同的函数供它们使用?

C 变量赋值 复制构造函数 C++-FAQ

评论

7赞 sellibitze 11/15/2009
"...有几乎相同的代码......“?嗯。。。你一定做错了什么。尽量减少为此使用用户定义函数的需要,让编译器完成所有肮脏的工作。这通常意味着将资源封装在其自己的成员对象中。您可以向我们展示一些代码。也许我们有一些很好的设计建议。
3赞 mpromonet 11/15/2015
减少 operator= 和复制构造函数之间的代码重复的可能重复

答:

135赞 CB Bailey 11/14/2009 #1

是的。有两种常见的选择。一种 - 通常不鼓励 - 是显式调用 from 复制构造函数:operator=

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

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

一种越来越流行的解决方案是使用 copy 构造函数和 swap 方法实现。operator=

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

甚至:

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

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

复制和交换习惯的优点是它自动自分配安全,并且 - 如果交换操作是无抛出的 - 也是非常安全的异常。

为了实现强烈的异常安全,“手写”分配操作员通常必须在取消分配被分配者的旧资源之前分配新资源的副本,以便在分配新资源时发生异常,仍然可以返回到旧状态。所有这些都是免费的,但从头开始通常更复杂,因此容易出错。

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

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

评论

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

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

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

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

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

这使用复制构造(注意是复制的)和销毁(在函数结束时销毁)——并且它也以正确的顺序使用它们:在销毁(一定不能失败)之前构造(可能失败)。other

评论

0赞 8/1/2016
应该申报吗?swapvirtual
1赞 sbi 8/9/2016
@Johannes:虚函数用于多态类层次结构。赋值运算符用于值类型。两者几乎不混为一谈。
-3赞 Matthew 1/18/2010 #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;
}

临时对象的构造仍然存在,但下一个直接操作是在将源资源移动到目标之前释放目标的所有当前资源(以及 NULLing,以便它们不会被双重释放)。

我建议 { construct, destruct, move } 代替 { construct, move, destruct }。此举是最危险的行动,是在其他一切都解决后最后采取的行动。

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

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

评论

2赞 Sebastian Mach 11/4/2010
析构函数不得失败,因此销毁时不会出现异常。而且,如果移动是最危险的操作,我不明白将移动移动到破坏后面有什么好处?也就是说,在标准方案中,移动失败不会破坏旧状态,而新方案会损坏旧状态。那么为什么呢?另外,-> 作为库编写者,您通常知道常见的做法(复制+交换),关键是 .你的思想实际上隐藏在公共界面后面。这就是可重用代码的意义所在。First, reading the word "swap" when my mind is thinking "copy" irritatesmy mind