自复制构造函数
MyClass(const MyClass&);
和= =运算符重载
MyClass& operator = (const MyClass&);
具有几乎相同的代码,相同的参数,并且只在返回时有所不同,是否可以为它们使用共同的功能?
答案 0 :(得分:106)
是。有两种常见的选择。一个 - 通常不鼓励 - 是明确地从复制构造函数调用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
。
通常使用成员swap
。 std::swap
可以使用所有基本类型和指针类型保证“无法投掷”。大多数智能指针也可以与无投掷保证交换。
答案 1 :(得分:12)
复制构造函数执行以前是原始内存的对象的首次初始化。赋值运算符OTOH使用新值覆盖现有值。通常,这涉及到解雇旧资源(例如,内存)和分配新资源。
如果两者之间存在相似性,则赋值运算符执行销毁和复制构造。一些开发人员过去常常通过就地破坏实现分配,然后是放置复制构造。但是,这是一个非常坏主意。 (如果这是在赋值派生期间调用的基类的赋值运算符,那该怎么办?)
现在通常被认为是典型的成语,正如查尔斯建议的那样使用swap
:
MyClass& operator=(MyClass other)
{
swap(other);
return *this;
}
这使用了复制构造(注意other
被复制)和销毁(它在函数末尾被破坏) - 它也以正确的顺序使用它们:构造(可能会失败)之前破坏(绝不能失败)。
答案 2 :(得分:-2)
让我烦恼的是:
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,因此它们不会被双重释放)源的资源之前释放目标的所有当前资源。 / p>
而不是{construct,move,destruct},我建议{构造,破坏,移动}。此举是最危险的行动,是在其他所有事情得到解决后最后采取的行动。
是的,破坏失败是两种方案中的问题。数据已损坏(在您认为不是时复制)或丢失(在您认为不存在时释放)。失败比腐败更好。没有数据比糟糕的数据更好。
转移而非交换。无论如何,这是我的建议。