从复制构造函数调用默认赋值运算符是不好的形式吗?
Posted
技术标签:
【中文标题】从复制构造函数调用默认赋值运算符是不好的形式吗?【英文标题】:Is it bad form to call the default assignment operator from the copy constructor? 【发布时间】:2010-12-04 18:13:25 【问题描述】:考虑一个需要复制的类。副本中的绝大多数数据元素必须严格反映原始数据,但是只有少数元素的状态不需要保留,需要重新初始化。
从复制构造函数调用默认赋值运算符是不是不好的形式?
默认的赋值运算符可以很好地处理普通旧数据(int、double、char、short)以及用户定义的每个赋值运算符的类。指针需要单独处理。
一个缺点是,由于没有执行额外的重新初始化,这种方法会使赋值运算符失效。也不能禁用赋值运算符,从而打开用户使用不完整的默认赋值运算符 A obj1,obj2; obj2=obj1; /* Could result is an incorrectly initialized obj2 */
创建损坏类的选项。
最好放宽对a(orig.a),b(orig.b)...
和a(0),b(0) ...
的要求。需要将所有初始化代码编写两次,这会产生两个错误位置,如果要将新变量(例如 double x,y,z
)添加到类中,则需要在至少 2 个位置而不是 1 个位置正确添加初始化代码。
有没有更好的方法?
C++0x 中有没有更好的方法?
class A
public:
A(): a(0),b(0),c(0),d(0)
A(const A & orig)
*this = orig; /* <----- is this "bad"? */
c = int();
public:
int a,b,c,d;
;
A X;
X.a = 123;
X.b = 456;
X.c = 789;
X.d = 987;
A Y(X);
printf("X: %d %d %d %d\n",X.a,X.b,X.c,X.d);
printf("Y: %d %d %d %d\n",Y.a,Y.b,Y.c,Y.d);
输出:
X: 123 456 789 987
Y: 123 456 0 987
替代复制构造函数:
A(const A & orig):a(orig.a),b(orig.b),c(0),d(orig.d) /* <-- is this "better"? */
【问题讨论】:
见:***.com/questions/1457842/… 【参考方案1】:使用您的复制构造函数版本,成员首先是默认构造的,然后是分配的。
对于整数类型,这无关紧要,但如果您有像 std::string
s 这样的重要成员,这是不必要的开销。
因此,是的,通常您的替代复制构造函数更好,但如果您只有整数类型作为成员,那并不重要。
【讨论】:
【参考方案2】:正如 brone 指出的那样,您最好在复制构造方面实施分配。我更喜欢他的替代成语:
T& T::operator=(T t)
swap(*this, t);
return *this;
它有点短,can take advantage of some esoteric language features 可以提高性能。就像任何优秀的 C++ 代码一样,它也有一些需要注意的细微之处。
首先,t
参数是故意传值的,这样会调用复制构造函数(most of the time),在不影响原值的情况下,可以随心所欲地修改。使用const T&
会编译失败,T&
会通过修改assigned-from 值触发一些令人惊讶的行为。
此技术还要求swap
以不使用类型的赋值运算符的方式专门用于类型(如std::swap
所做的那样),否则将导致无限递归。小心任何杂散的using std::swap
或using namespace std
,因为如果您没有将swap
专门用于T
,它们会将std::swap
拉入范围并导致问题。重载解析和ADL 将确保使用正确版本的交换(如果您已定义它)。
有几种方法可以为类型定义swap
。第一种方法使用swap
成员函数来完成实际工作,并有一个委托给它的swap
特化,如下所示:
class T
public:
// ....
void swap(T&) ...
;
void swap(T& a, T& b) a.swap(b);
这在标准库中很常见;例如,std::vector
以这种方式实现了交换。如果您有 swap
成员函数,您可以直接从赋值运算符中调用它,避免函数查找出现任何问题。
另一种方法是将swap
声明为友元函数并让它完成所有工作:
class T
// ....
friend void swap(T& a, T& b);
;
void swap(T& a, T& b) ...
我更喜欢第二个,因为swap()
通常不是类接口的组成部分;它似乎更适合作为免费功能。然而,这是一个品味问题。
为一个类型优化 swap
是实现 C++0x 中右值引用的一些好处的常用方法,所以一般来说,如果类可以利用它并且你真的需要它是一个好主意性能。
【讨论】:
这有潜在的危险!如果没有明确的特化,std::swap 需要一个工作副本赋值运算符并且可以在其实现中使用它。这将使这个 operator= 无限递归。 @Charles - 已编辑,谢谢。我应该知道这一点,因为我曾经犯过同样的错误。 这就是 ADL 的用途:在定义 T 的命名空间中为 T 提供一个交换函数,在同一个头文件中,这样如果你有nm::T
,你总是有nm::swap(T&,T&)
也是如此。然后预计呼叫者执行using std::swap; swap(myT,otherT);
,而不是std::swap(myT,otherT)
。如果他们确实调用了std::swap
,至少operator=
调用了nm::swap
,所以它不是递归的,只是效率不高。我不是很喜欢这个公约,但它确实存在。
我同意将交换的实际机制实现为公共成员函数是一个好主意。如果不出意外,这意味着对名称查找和/或模板专业化偏执的人可以只做myT.swap(otherT)
并确切地知道立场。
@onebyone:我同意。我认为答案可能应该明确说明operator=
中的swap
需要是用户定义的,不使用赋值运算符,如果只是为了消除任何可能的混淆来源。【参考方案3】:
我会称它为糟糕的形式,不是因为你对所有对象进行双重分配,而是因为根据我的经验,依赖特定集合的默认复制构造函数/赋值运算符通常是糟糕的形式的功能。由于这些不在任何地方的源中,因此很难说您想要的行为取决于它们的行为。例如,如果有人想在一年内向您的班级添加一个字符串向量怎么办?您不再拥有普通的旧数据类型,维护人员很难知道它们正在破坏事物。
我认为,尽管 DRY 很好,但从维护的角度来看,创建微妙的未指定需求要糟糕得多。即使是重复自己,尽管如此糟糕,也没有那么邪恶。
【讨论】:
【参考方案4】:我个人认为损坏的赋值运算符是杀手。我总是说人们应该阅读文档,不要做任何它告诉他们不要做的事情,但即便如此,写作业而不考虑它也太容易了,或者使用需要类型可分配的模板。不可复制的习惯用法是有原因的:如果operator=
不起作用,那么让它可以访问就太危险了。
如果我没记错的话,C++0x 会让你这样做:
private:
A &operator=(const A &) = default;
那么至少只有类本身可以使用损坏的默认赋值运算符,并且您希望在这种受限的上下文中更容易小心。
【讨论】:
【参考方案5】:我认为更好的方法是不如果行为是微不足道的(在你的情况下它似乎被破坏了:至少赋值和复制应该具有相似的语义,但你的代码建议这不会是这样 - 但我想这是一个人为的例子)。为您生成的代码不会出错。
如果您需要实现这些方法,该类很可能可以使用快速交换方法,从而能够重用复制构造函数来实现赋值运算符。
如果你因为某种原因需要提供一个默认的浅拷贝构造函数,那么 C++0X 有
X(const X&) = default;
但我认为奇怪的语义没有成语。在这种情况下,使用赋值而不是初始化很便宜(因为保持 int 未初始化不会花费任何成本),所以您不妨这样做。
【讨论】:
【参考方案6】:本质上,您的意思是您的班级中有一些成员对班级的身份没有贡献。按照目前的情况,您可以通过使用赋值运算符来复制类成员,然后重置那些不应该被复制的成员来表达这一点。这会留下一个与复制构造函数不一致的赋值运算符。
更好的是使用复制和交换习语,并在复制构造函数中表示不应该复制哪些成员。您仍然有一个地方可以表达“不要复制此成员”行为,但是现在您的赋值运算符和复制构造函数是一致的。
class A
public:
A() : a(), b(), c(), d()
A(const A& other)
: a(other.a)
, b(other.b)
, c() // c isn't copied!
, d(other.d)
A& operator=(const A& other)
A tmp(other); // doesn't copy other.c
swap(tmp);
return *this;
void Swap(A& other)
using std::swap;
swap(a, other.a);
swap(b, other.b);
swap(c, other.c); // see note
swap(d, other.d);
private:
// ...
;
注意:在swap
成员函数中,我已经交换了c
成员。为了在赋值运算符中使用,它保留了与复制构造函数相匹配的行为:它重新初始化了c
成员。如果您将swap
函数公开,或通过swap
免费函数提供对它的访问权限,则应确保此行为适用于交换的其他用途。
【讨论】:
operator= 应该按值取 A - 我修改了我的答案以指出这一点。另外,您忘记返回 *this。 @Jeff Hardy:我不得不说这是一个风格问题。按值取参数有一些优点,但文案很容易被忽视。使用 const 引用会使副本更加明显。在某些情况下,按值取值可以进行一些额外的优化,但我从未见过它有明显的不同。以上是关于从复制构造函数调用默认赋值运算符是不好的形式吗?的主要内容,如果未能解决你的问题,请参考以下文章