因为是复制构造函数
1
| MyClass(const MyClass&); |
且an=操作员过载
1
| MyClass& operator = (const MyClass&); |
具有几乎相同的代码、相同的参数,并且返回时只有不同的参数,是否可以有一个共同的函数供两者使用?
- "…几乎有相同的代码…"?隐马尔可夫模型。。。你一定是做错了什么。尽量减少使用用户定义函数的需要,让编译器完成所有的脏工作。这通常意味着将资源封装在自己的成员对象中。你可以给我们看一些代码。也许我们有一些好的设计建议。
- 减少operator=和复制构造函数之间的代码重复的可能副本
对。共有两种选择。一种通常不鼓励的方法是从复制构造函数显式调用operator=:
1 2 3 4
| MyClass(const MyClass& other)
{
operator=(other);
} |
然而,提供一个好的operator=是一个挑战,当它涉及到处理旧的状态和自我分配产生的问题。此外,所有成员和基都会首先初始化默认值,即使它们将从other分配给它们。这甚至可能对所有成员和基都无效,甚至在它有效的情况下,它在语义上是多余的,并且可能实际上是昂贵的。
一个日益流行的解决方案是使用复制构造函数和交换方法实现operator=。
1 2 3 4 5 6
| MyClass& operator=(const MyClass& other)
{
MyClass tmp(other);
swap(tmp);
return *this;
} |
甚至:
1 2 3 4 5
| MyClass& operator=(MyClass other)
{
swap(other);
return *this;
} |
swap函数通常很容易编写,因为它只是交换内部的所有权,不必清理现有状态或分配新资源。
复制和交换习惯用法的优点是,它是自动自分配安全的,并且——只要交换操作是不抛出的——也是非常安全的异常。
为确保异常安全,"手写"的分配运算符通常必须在取消分配受让人的旧资源之前分配新资源的副本,以便在分配新资源时发生异常时,仍然可以返回旧状态。所有这些在复制和交换中都是免费的,但通常更复杂,因此容易出错,从头开始。
要小心的一件事是确保swap方法是真正的swap,而不是使用复制构造函数和赋值操作符本身的默认std::swap。
通常使用会员制的swap。std::swap可以工作,并且对所有基本类型和指针类型都有"不抛出"保证。大多数智能指针也可以交换为不抛出保证。
- 实际上,它们不是常见的操作。当copy ctor首次初始化对象的成员时,赋值运算符将覆盖现有值。考虑到这一点,从copy ctor中alling operator=实际上非常糟糕,因为它首先将所有值初始化为某个默认值,只是在随后用另一个对象的值覆盖它们。
- 我说的是共同选择,不是行动。我完全同意从一个复制构造函数调用operator=是不好的,但是您只需要查看一个关于真实世界代码的合理的了解就可以知道它有多常见。
- 投反对票的人,想解释一下吗?
- 嗯,你不是只是回应我的解释吗?
- 也许是"我不推荐",加上"并且没有任何C++专家"。有人可能会出现,并没有意识到你不仅表达了个人的少数族裔偏好,还表达了那些真正考虑过的人的一致意见。而且,好吧,也许我错了,一些C++专家确实推荐它,但是我个人仍然会为某人提出一个挑战,为这个建议提一个参考。
- @是的,我回复了你的评论。我知道这两种选择都很常见,但你不同意。受欢迎程度在某种程度上是一个意见问题,所以我很高兴在这一点上不同意,但我有兴趣知道为什么我被否决了,这意味着"错误或无用",这不是我的意图。
- 史提夫:"我不喜欢听起来太陈规,人们用不同的方式快乐地使用C++(或者说不那么高兴)。在重新阅读我的答案后,我又加了一句"我不推荐",因为我觉得仅仅下一段的内容可能并没有足够强烈地表达我的不满。
- 公平地说,我已经对你投了赞成票。我认为,如果某件事被广泛认为是最佳实践,那么最好这样说(如果有人说它不是真正的最佳实践,请再看一次)。同样,如果有人问"是否有可能在C++中使用互斥体",我就不会说"一个相当普遍的选择是完全忽略RAII,编写非死锁安全代码,在生产中死锁,但它越来越受欢迎地编写体面的、工作的代码";
- + 1。我认为总是需要分析。我认为在某些情况下(对于轻量级类),由copy ctor和assignment操作符使用assign成员函数是合理的。在其他情况下(资源密集型/使用案例、句柄/主体),复制/交换当然是最好的选择。
- @Litb:同意,例如,如果你在一个类中有两个基本类型,那么分配你的成员在任何情况下都不会有任何自我分配或异常安全的问题,所以对于最强大的解决方案来说,没有什么值得担心的。一旦您的构造器new或进行任何类型的资源分配,您就必须更加仔细地考虑您的选项。
- @李特:如果这个类是轻量级的,复制交换有什么害处吗?
- 是的,从那以后你就不需要特别的交换,我也不知道它买了什么,你不需要特别的安全。您只需添加一个无用的函数(交换成员函数),并创建一个无用的副本(因为交换/复制在这里的速度基本相同,所以这在实际意义上是无用的)。但我可能是错的,当然,我只是没有看到做拷贝交换的意义。
- 当然,这取决于"轻量级"到底是什么——我指的是一个不进行动态内存管理或资源保存的类。在这种情况下,很少需要用户定义的CCTOR或分配操作,我认为-但有时可能会出现这种情况。
- 不会投你反对票,但我从来没有见过一个复制构造函数是用operator=实现的,而且它看起来效率很低。
- @我可以向你保证,这是令人沮丧的普遍现象。我从来没有在代码中看到过,否则我会想:"这是一个非常干净的C++,在这里!"但是。
- 事实上,lemme引用了herb-sutter的话:"如果有的话,这个成语就是向后的:复制构造应该按照复制分配的方式实现,而不是相反的方式。"->gotw.ca/gotw/023.htm。当然,那篇文章很旧,但它表明这并不是一个疯狂的想法:)
- 如果我缺乏知识,请原谅我,但在交换解决方案中,如何确保目标的初始破坏?
- @利特布:我对此感到惊讶,所以我查阅了异常C++中的第41项(这个GOW变成了),这个特别的建议已经消失了,他建议复制和交换。他相当狡猾地同时放弃了"问题4:分配效率低下"。
- @MPElletier:交换操作将目标的旧状态移动到临时副本中(当副本的状态移动到目标中时)。由于初始副本是赋值运算符的本地副本,因此它在函数体的末尾超出了范围,因此确保删除目标的旧状态。
- @查尔斯,啊,很好。我原以为和他没什么不同,现在我知道他确实做了"修正"那件事:毫无疑问,我不认为从cctor和copy赋值操作符调用一个公共函数特别"坏",就像这里的一些人说的那样。如果一个简单的解决方案足够的话,为什么要用复杂的方法呢?
- +1 Sutter还介绍了稍后的gotw:gotw.ca/gotw/059.htm中的复制交换。
- @rlbond:实际上,这就是如何定义T a(b);:首先调用默认构造函数,然后调用赋值运算符。因为这发现效率太低,所以在C++中引入了一个"复制构造函数"的概念。
- 我担心签名myClass&operator=(myClass other)。Visual C++ 2013编译器似乎仍然自动生成MyCype和AMP;运算符=(const MyCype和Apple;其他)函数,实现成员分配。myClass&operator=(myClass other)签名是否应抑制生成的vs?
- 第一个建议不是不建议是非法的吗?如果operator=删除内存,这将删除一个随机(未初始化)指针。我相信这不仅仅是"不推荐",而且保证会崩溃。
- @DOV:"删除内存"?什么记忆?听起来你心里有一个具体的例子。如果operator=写得正确,就不应该触发崩溃。
- @charles:operator=假定对象已经存在。因此,如果有指针,应该删除对象。复制构造函数没有。您的第二个解决方案(其中operator=是根据复制构造函数编写的)是有意义的。但是第一个问题,如果对象包含一个动态分配的指针,您将如何编写它?
复制构造函数对以前是原始内存的对象执行首次初始化。赋值运算符Otoh用新值覆盖现有值。通常情况下,这涉及到废弃旧资源(例如内存)和分配新资源。
如果两者之间存在相似性,则表示赋值运算符执行销毁和复制构造。一些开发人员过去常常通过就地销毁和放置副本构造来实际实现分配。然而,这是一个非常坏的主意。(如果这是在派生类赋值期间调用的基类的赋值运算符,该怎么办?)
现在人们通常认为的标准成语是使用swap,正如查尔斯所建议的:
1 2 3 4 5
| MyClass& operator=(MyClass other)
{
swap(other);
return *this;
} |
它使用复制构造(请注意,other是复制的)和销毁(在函数结束时被销毁)并按正确的顺序使用它们:销毁前的构造(可能会失败)(不能失败)。
- 是否应宣布swap为virtual?
- @约翰内斯:虚拟函数用于多态类层次结构。赋值运算符用于值类型。这两个几乎不能混合。
我有点担心:
1 2 3 4 5 6
| MyClass& operator=(const MyClass& other)
{
MyClass tmp(other);
swap(tmp);
return *this;
} |
首先,当我的大脑在思考"复制"时,阅读"交换"一词会刺激我的常识。另外,我对这个花式把戏的目的表示怀疑。是的,在构建新的(复制的)资源时,任何异常都应该发生在交换之前,这似乎是一种安全的方法,可以确保在使所有新数据生效之前都已填充。
那很好。那么,在交换之后会发生什么异常呢?(当临时对象超出范围时,旧资源被破坏)从分配的用户的角度来看,操作失败了,但没有失败。它有一个巨大的副作用:复制确实发生了。只是一些资源清理失败了。目标对象的状态已更改,即使操作似乎从外部失败。
因此,我建议不要用"交换"来进行更自然的"转移":
1 2 3 4 5 6
| MyClass& operator=(const MyClass& other)
{
MyClass tmp(other);
transfer(tmp);
return *this;
} |
还有临时对象的构造,但下一步立即行动是在将源的资源移到目标之前释放目标的所有当前资源(并使其无效,这样它们就不会被双重释放)。
我建议不要构造、移动、销毁,而是构造、销毁、移动。这是最危险的行动,是在其他一切都解决之后最后采取的行动。
是的,破坏失败是两种方案中的一个问题。数据要么已损坏(在您认为不存在时复制),要么已丢失(在您认为不存在时释放)。失败者胜于败者。没有比坏数据更好的数据。
转移而不是交换。这是我的建议。
- 析构函数不能失败,因此不期望在销毁时出现异常。而且,如果移动是最危险的操作,我不明白在破坏后移动的好处是什么?即,在标准方案中,移动失败不会破坏旧状态,而您的新方案会。那为什么呢?另外,作为一名图书馆作者,你通常知道常用的做法(copy+swap),而关键是my mind。你的思想实际上隐藏在公共界面的后面。这就是可重用代码的意义所在。