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

时间:2009-10-07 19:37:37

标签: c++ constructor c++11 copy-constructor assignment-operator

考虑需要制作副本的类。副本中的绝大多数数据元素必须严格反映原始数据元素,但选择少数元素,其状态不被保留,需要重新初始化

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

默认赋值运算符对于Plain Old Data(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中正确添加。 / p>

有更好的方法吗?

在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"? */

6 个答案:

答案 0 :(得分:13)

正如布朗指出的那样,你最好在复制结构方面实施任务。我更喜欢他的替代习语:

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&将通过修改assign-from值触发一些令人惊讶的行为。

这种技术还要求swap以不使用类型赋值运算符的方式专门用于类型(如std::swap所做的那样),否则它将导致无限递归。请注意任何流浪using std::swapusing namespace std,因为如果您没有为std::swap专门设置swap,他们会将T拉入范围并导致问题。重载决策和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中实现rvalue引用的一些好处的常用方法,因此如果类可以利用它并且你是一个好主意真的需要表现。

答案 1 :(得分:4)

使用您的版本的复制构造函数,首先默认构造成员然后分配成员 对于整数类型,这没关系,但是如果你有像std::string这样的非平凡成员,这是不必要的开销。

因此,是的,一般来说,你的替代拷贝构造函数更好,但是如果你只有成员类型作为成员那么它并不重要。

答案 2 :(得分:2)

基本上,你所说的是你的班级中有些成员对班级的身份没有贡献。目前的情况是,通过使用赋值运算符复制类成员然后重置那些不应复制的成员来表达这一点。这将为您留下一个与复制构造函数不一致的赋值运算符。

更好的方法是使用复制和交换习惯用法,并表示不应在复制构造函数中复制哪些成员。您仍然有一个地方表示“不要复制此成员”行为,但现在您的赋值运算符和复制构造函数是一致的。

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免费函数提供对它的访问权限,则应确保此行为适用于swap的其他用途。

答案 3 :(得分:1)

我个人认为破坏的赋值运算符是杀手。我总是说人们应该阅读文档,而不是做任何告诉他们不要做的事情,但即便如此,在不考虑它的情况下编写作业也很容易,或者使用需要类型可分配的模板。不可复制的成语有一个原因:如果operator=不起作用,那么让它可以访问就太危险了。

如果我没记错的话,C ++ 0x会让你这样做:

private:
    A &operator=(const A &) = default;

然后至少它只是类本身可以使用破坏的默认赋值运算符,并且你希望在这个受限制的上下文中更容易小心。

答案 4 :(得分:0)

我称之为糟糕的形式,不是因为你对所有对象进行了双重分配,而是因为根据我的经验,依赖在特定集合的默认复制构造函数/赋值运算符上通常是不好的形式功能。由于这些不在任何地方,所以很难说你想要的行为取决于他们的行为。例如,如果一年中有人想要为您的班级添加字符串向量,该怎么办?你不再拥有普通的旧数据类型,维护者很难知道他们正在破坏它们。

我认为,就像DRY一样,从维护的角度来看,创建微妙的未指定要求会更糟糕。即使重复自己,就像那样糟糕,也不那么邪恶。

答案 5 :(得分:0)

我认为更好的方法是实现复制构造函数,如果行为是微不足道的(在您的情况下它似乎被破坏了:至少赋值和复制应该具有相似的语义但是您的代码建议这不会是这样 - 但我认为这是一个人为的例子)。为您生成的代码不会错。

如果您需要来实现这些方法,那么很可能该类可以使用快速交换方法,因此可以重用复制构造函数来实现赋值运算符。

如果由于某种原因需要提供默认的浅拷贝构造函数,那么C ++ 0X已经

 X(const X&) = default;

但我不认为奇怪的语义有成语。在这种情况下,使用赋值而不是初始化是很便宜的(因为保留未初始化的int不会花费任何成本),所以你可能会这样做。