从复制构造函数调用默认赋值运算符是不好的形式吗?

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::strings 这样的重要成员,这是不必要的开销。

因此,是的,通常您的替代复制构造函数更好,但如果您只有整数类型作为成员,那并不重要。

【讨论】:

【参考方案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&amp; 会编译失败,T&amp; 会通过修改assigned-from 值触发一些令人惊讶的行为。

此技术还要求swap 以不使用类型的赋值运算符的方式专门用于类型(如std::swap 所做的那样),否则将导致无限递归。小心任何杂散的using std::swapusing 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&amp;,T&amp;) 也是如此。然后预计呼叫者执行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 引用会使副本更加明显。在某些情况下,按值取值可以进行一些额外的优化,但我从未见过它有明显的不同。

以上是关于从复制构造函数调用默认赋值运算符是不好的形式吗?的主要内容,如果未能解决你的问题,请参考以下文章

复制构造函数与赋值运算符(=)有何不同

C++:在派生类构造函数中调用基类赋值运算符的错误形式?

如何从基类调用派生赋值运算符?

当我对具有复制构造函数但没有赋值运算符的对象进行赋值时会发生啥?

我应该删除内部创建线程的类的复制构造函数和赋值运算符吗?

自动生成默认/复制/移动 ctor 和复制/移动赋值运算符的条件?