14

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

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

默认赋值运算符将与普通旧数据(int、double、char、short)以及用户定义的每个赋值运算符的类一起表现良好。指针需要单独处理。

一个缺点是,由于没有执行额外的重新初始化,这种方法使赋值运算符失效。也不可能禁用赋值运算符,从而打开用户使用不完整的默认赋值运算符创建损坏类的选项A obj1,obj2; obj2=obj1; /* Could result is an incorrectly initialized obj2 */

a(orig.a),b(orig.b)...放宽 to除了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"? */
4

6 回答 6

15

正如 brone 指出的那样,您最好在复制构造方面实施分配。我更喜欢他的另一种成语:

T& T::operator=(T t) {
    swap(*this, t);
    return *this;
}

它有点短,可以利用一些深奥的语言特性来提高性能。像任何优秀的 C++ 代码一样,它也有一些需要注意的细微之处。

首先,t参数是故意传值的,这样复制构造函数就会被调用(大部分时间),我们可以随心所欲地修改,而不影响原来的值。使用const T&将无法编译,并且T&会通过修改分配的值来触发一些令人惊讶的行为。

此技术还需要swap以不使用类型的赋值运算符(如使用std::swap)的方式专门用于类型,否则将导致无限递归。小心任何杂散using std::swap或,因为如果您不专注于using namespace std,它们会进入范围并引起问题。重载解决方案和ADL将确保使用正确版本的交换(如果您已定义它)。std::swapswapT

有几种方法可以定义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 中右值引用的一些好处的常用方法,因此,如果类可以利用它并且您确实需要性能,那么一般来说这是一个好主意。

于 2009-10-07T19:59:11.403 回答
4

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

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

于 2009-10-07T19:50:22.797 回答
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则应确保此行为适用于交换的其他用途。

于 2009-10-08T06:37:16.087 回答
1

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

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

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

那么至少只有类本身可以使用损坏的默认赋值运算符,并且您希望在这个受限的上下文中更容易小心。

于 2009-10-07T21:47:32.657 回答
0

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

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

如果你出于某种原因需要提供一个默认的浅拷贝构造函数,那么 C++0X 有

 X(const X&) = default;

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

于 2009-10-07T23:43:44.097 回答
0

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

我认为,尽管 DRY 很好,但从维护的角度来看,创建细微的未指定需求要糟糕得多。即使重复自己,尽管如此糟糕,也没有那么邪恶。

于 2009-10-07T20:54:22.477 回答