安全转让和复制和交换成语 [已关闭]

Safe assignment and copy-and-swap idiom [closed]

提问人:Rafael S. Calsaverini 提问时间:5/6/2011 最后编辑:Rafael S. Calsaverini 更新时间:5/6/2011 访问量:2236

问:


想改进这个问题吗?通过编辑这篇文章来更新问题,使其仅关注一个问题。

去年关闭。

我正在学习 c++,最近我学到了(在堆栈溢出中)关于复制和交换的习惯用语,我对此有一些疑问。因此,假设我有以下类使用复制和交换成语,例如:

class Foo {
private:
  int * foo;
  int size;

public:
  Foo(size_t size) : size(size) { foo = new int[size](); }
  ~Foo(){delete foo;}

  Foo(Foo const& other){
    size = other.size;
    foo = new int[size];
    copy(other.foo, other.foo + size, foo);
  }

  void swap(Foo& other) { 
    std::swap(foo,  other.foo);  
    std::swap(size, other.size); 
  }

  Foo& operator=(Foo g) { 
    g.swap(*this); 
    return *this; 
  }

  int& operator[] (const int idx) {return foo[idx];}
};

我的问题是,假设我有另一个类,它有一个 Foo 对象作为数据,但没有可能需要自定义复制或赋值的指针或其他资源:

class Bar {
private:
  Foo bar;
public:
  Bar(Foo foo) : bar(foo) {};
  ~Bar(){};
  Bar(Bar const& other) : bar(other.bar) {}; 
  Bar& operator=(Bar other) {bar = other.bar;}
};

现在我有一系列的问题:

  1. 上面为类实现的方法和构造函数是否安全?使用复制和交换后,确保在分配或复制时不会造成任何伤害?BarFooBar

  2. 在复制构造函数和 swap 中通过引用传递参数是强制性的吗?

  3. 当参数按值传递时,会调用 copy 构造函数来生成对象的临时副本,然后与此副本交换,这是对的吗?如果我通过引用,我会有一个大问题,对吧?operator=*thisoperator=

  4. 在有些情况下,这个成语在复制和分配时无法提供完全的安全性吗?Foo

C++ 构造函数 Rule-of-Three 复制和交换

评论

0赞 rlc 5/6/2011
您的复制构造函数有一个相当严重的错误:它在设置之前执行foo = new int[size]size
0赞 Benjamin Lindley 5/6/2011
Bar 中的自定义复制构造函数、赋值运算符和析构函数是完全不必要的。还是你已经知道了?
0赞 Rafael S. Calsaverini 5/6/2011
Cieslak:哎呀!谢谢你的指出。Lindley:是的,我知道这完全等同于默认构造函数,为了明确起见,澄清我的问题。
0赞 Paul Groke 5/6/2011
+1:好问题,特别是对于C++初学者!
1赞 fredoverflow 5/6/2011
我强烈建议作为会员使用。这样就不必首先编写析构函数和复制操作了。std::vector<int>

答:

8赞 rlc 5/6/2011 #1

应尽可能在初始值设定项列表中初始化类的成员。这也将解决我在评论中告诉您的错误。考虑到这一点,您的代码将变为:

class Foo {
private:
  int size;
  int * foo;

public:
  Foo(size_t size) : size(size), foo(new int[size]) {}
  ~Foo(){delete[] foo;} // note operator delete[], not delete

  Foo(Foo const& other) : size(other.size), foo(new int[other.size]) {
    copy(other.foo, other.foo + size, foo);
  }

  Foo& swap(Foo& other) { 
    std::swap(foo,  other.foo);  
    std::swap(size, other.size); 
    return *this;
  }

  Foo& operator=(Foo g) { 
    return swap(g); 
  }

  int& operator[] (const int idx) {return foo[idx];}
};

class Bar {
private:
  Foo bar;
public:
  Bar(Foo foo) : bar(foo) {};
  ~Bar(){};
  Bar(Bar const& other) : bar(other.bar) { }
  Bar& swap(Bar &other) { bar.swap(other.bar); return *this; }
  Bar& operator=(Bar other) { return swap(other); }
}

自始至终使用相同的成语

注意

正如在对该问题的评论中提到的,的自定义复制构造函数等是不必要的,但我们假设还有其他东西:-)BarBar

第二个问题

需要通过引用传递,因为两个实例都已更改。swap

需要通过引用传递复制构造函数,因为如果按值传递,则需要调用复制构造函数

第三个问题

是的

第四个问题

不,但这并不总是最有效的做事方式

评论

0赞 Rafael S. Calsaverini 5/6/2011
所以,这是我怀疑的一点:信任类的默认复制构造函数和赋值运算符是完全安全的吗?Bar
0赞 rlc 5/6/2011
@Rafael是的,是的。的默认复制构造函数及其默认赋值运算符都可以正常工作Bar
0赞 Tony Delroy 5/6/2011
@Rafael:编译器提供的默认构造函数/赋值运算符/析构函数都可以,只要类成员和基是用“值语义”正确实现的,这意味着它们管理它们使用的资源。如果 Bar 包含e.g. raw指向堆分配内存的指针,那么 1) 你可以编写自己的 Bar 构造函数/析构函数/来管理它,这样 Bar 仍然具有值语义(好),或者 2) 你可以在 Bar 之后清理 Foo(容易出错,并且需要在 Foo 的构造函数/析构函数/中执行与 Bar 相关的步骤)。operator=operator=
5赞 Paul Groke 5/6/2011 #2

1 - 上面为 Bar 类实现的方法和构造函数是否安全?对 Foo 使用了复制和交换,确保在分配或复制 Bar 时不会造成任何伤害吗?

关于复制者:这总是安全的(全有或全无)。它要么完成(全部),要么引发异常(无)。

如果类仅由一个成员组成(即没有基类),则赋值运算符将与成员类的运算符一样安全。如果有多个成员,则赋值运算符将不再是“全有或全无”。第二个成员的赋值运算符可能会引发异常,在这种情况下,对象将被赋值为“中途”。这意味着您需要在新类中再次实现复制和交换,以获得“全有或全无”的分配。

但是,从某种意义上说,它仍然是“安全的”,因为您不会泄漏任何资源。当然,每个成员的状态都是一致的——只是新类的状态不会一致,因为一个成员被分配了,而另一个成员没有被分配。

2 - 在复制构造函数和 swap 中通过引用传递参数是强制性的吗?

是的,通过引用传递是强制性的。复制构造函数是复制对象的构造函数,因此它不能按值获取其参数,因为这意味着必须复制参数。这将导致无限递归。(参数将调用 copy-ctor,这意味着为参数调用 copy-ctor,这意味着......对于交换,原因还有另一个原因:如果你要按值传递参数,你永远不能使用 swap 来真正交换两个对象的内容——交换的“目标”将是最初传入的对象的副本,它会立即被销毁。

3 - 当 operator= 的参数按值传递时,会调用 copy 构造函数来生成对象的临时副本,然后用 *this 交换这个副本,这是对的吗?如果我在 operator= 中通过引用传递,我会有一个大问题,对吧?

是的,没错。但是,通过引用 const 来获取参数,构造本地副本,然后与本地副本交换也很常见。然而,by reference-to-const 方法有一些缺点(它禁用了一些优化)。但是,如果您没有实现复制和交换,则可能应该通过对 const 的引用。

4 - 在有些情况下,这个成语在复制和分配 Foo 时无法提供完全的安全性?

我都不知道。当然,人们总是可以通过多线程(如果没有正确同步)使事情失败,但这应该是显而易见的。

评论

0赞 Ben Voigt 7/4/2012
#1 -- 不为真,如果抛出异常,则内存可能泄漏。使用 function-try-block 确保在发生异常时将其删除。foo
0赞 Paul Groke 7/4/2012
不。Foo::Foo 中唯一可以抛出的就是新的,如果可以,内存将不会被分配,所以没有什么可以清理的。如果它不是一堆整数,副本可能会抛出,并且必须防止这种情况发生。然而,在这种情况下,我建议使用一个额外的基类,它只分配和拥有数组,而不在其自己的ctor中进行任何复制。这样你就不需要函数尝试块了。
0赞 Ben Voigt 7/4/2012
有一个函数调用。没有关于该函数的其他信息(是吗?可能不是,因为在调用时明确提供了),必须假设它可以抛出。std::copystd::std::swap
0赞 Paul Groke 7/4/2012
你在这里把头发劈得很稀。是的,如果 copy() 可以抛出,那么 Foo::Foo 就会被破坏。不过,问题不在于 Foo::Foo,而在于 Bar。假设 Foo::Foo 没问题,那么 Bar 的实现也是如此。当然,假设 Foo 在某种程度上被破坏了,那么 Bar 也必须被破坏。
0赞 Ben Voigt 7/4/2012
啊,是的。构造函数很好。+1Bar
2赞 Howard Hinnant 5/6/2011 #3

这是一个典型的例子,说明遵循一个成语会导致不必要的性能损失(过早悲观)。这不是你的错。复制和交换的成语被过度炒作了。这是一个很好的成语。但不应盲目追随。

注意:您可以在计算机上执行的最昂贵的操作之一就是分配和释放内存。在可行的情况下,避免这样做是值得的。

注意:在您的示例中,复制和交换惯用法始终执行一次释放,并且通常(当赋值的 rhs 为左值时)也执行一次分配。

观察:当 时,不需要进行解除分配或分配。你所要做的就是一个.这要快得多size() == rhs.size()copy

Foo& operator=(const Foo& g) {
    if (size != g.size)
        Foo(g).swap(*this); 
    else
       copy(other.foo, other.foo + size, foo);
    return *this; 
}

即检查可以首先回收资源的情况。然后,如果您无法回收资源,则进行复制和交换(或其他任何操作)。

注意:我的评论与其他人给出的良好答案并不矛盾,也没有任何正确性问题。我的评论只是关于显着的性能损失。

评论

0赞 Paul Groke 5/6/2011
你说的很对。但是,您应该添加一个 rvalue-reference 重载。否则,每当在赋值的右侧使用右值时,您的“优化”实际上都会损害性能。
0赞 Matthieu M. 5/6/2011
@pgroke:右值引用仅在 C++0x 中可用,我认为 OP 还没有准备好,恐怕只会让他感到困惑(请记住,他不确定引用是如何工作的)。恐怕这个答案已经有点太牵强了。
1赞 Paul Groke 5/6/2011
我知道右值引用仅在 C++0x 中可用。您可能是对的,讨论右值引用可能会混淆 OP。尽管如此,我认为如果讨论像这样的优化,尽可能完整也没有什么坏处。
0赞 Ben Voigt 7/4/2012
这种“优化”的问题——如果元素复制构造函数可以抛出,对象就会一团糟(对 来说不是真正的问题)。复制和交换要么成功,要么保持旧值不变。int