【问题标题】:Safe assignment and copy-and-swap idiom安全分配和复制和交换习语
【发布时间】:2011-05-06 00:25:27
【问题描述】:

我正在学习 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. 上面为Bar 类实现的方法和构造函数是否安全?使用了Foo 的复制和交换功能后,请确保在分配或复制Bar 时不会造成任何伤害?

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

  3. 是不是说当operator=的参数传值时,会为此参数调用拷贝构造函数来生成对象的临时拷贝,然后就是这个拷贝与*this 交换?如果我在operator= 中通过引用传递,我会有一个大问题,对吧?

  4. 在复制和分配Foo时是否存在此习惯用法无法提供完全安全性的情况?

【问题讨论】:

  • 您的复制构造函数有一个相当严重的错误:它在设置size 之前执行foo = new int[size]
  • Bar 中自定义的复制构造函数、赋值运算符和析构函数是完全没有必要的。还是你已经知道了?
  • 西斯拉克:哎呀!感谢您指出。 Lindley:是的,我知道这完全等同于默认构造函数,为了明确起见,澄清我的问题。
  • +1:很好的问题,尤其是对于 C++ 初学者!
  • 我强烈建议使用std::vector<int> 作为会员。那么你就不必一开始就编写析构函数和复制操作了。

标签: c++ class copy-constructor rule-of-three copy-and-swap


【解决方案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); }
}

始终使用相同的成语

注意

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

第二个问题

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

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

第三个问题

是的

第四题

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

【讨论】:

  • 那么,这是我怀疑的一点:信任 Bar 类的默认复制构造函数和赋值运算符是否完全安全?
  • @Rafael 是的,是的。 Bar 的默认复制构造函数和默认赋值运算符都可以正常工作
  • @Rafael:编译器提供的默认构造函数/赋值运算符/析构函数都可以,只要类成员和基类使用“值语义”正确实现,这意味着它们管理所使用的资源。如果 Bar 包含例如指向堆分配内存的原始指针,然后 1)您可以编写自己的 Bar 构造函数/析构函数/operator= 来管理它,因此 Bar 仍然具有值语义(好),或者 2)您可以在 Bar 之后清理 Foo (容易出错,并且需要在 Foo 的构造函数/析构函数/operator= 中执行与 Bar 相关的步骤。
【解决方案2】:

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

关于copy-ctor:这总是安全的(全有或全无)。它要么完成(全部),要么抛出异常(什么都没有)。

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

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

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

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

3 - 是否正确地说,当 operator= 的参数按值传递时,会为此参数调用复制构造函数以生成对象的临时副本,然后将其交换为该副本*这?如果我在 operator= 中通过引用传递,我会遇到很大的问题,对吧?

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

4 - 在复制和分配 Foo 时,是否存在此习惯用法无法提供完全安全性的情况?

据我所知没有。当然,多线程总是可以让事情失败(如果没有正确同步),但这应该是显而易见的。

【讨论】:

  • #1 -- 不正确,如果抛出异常,则可能会泄漏内存。使用函数try-block确保foo在异常情况下被删除。
  • 不。 Foo::Foo 中唯一可以抛出的是新的,如果它抛出了,内存将不会被分配,所以没有什么需要清理的。如果它不是一堆整数,则副本可能会抛出,并且必须防止这种情况发生。但是,在这种情况下,我建议使用一个额外的基类,它只分配并拥有数组,而不在它自己的 ctor 中进行任何复制。这样你就不需要函数 try 块了。
  • 有一个函数调用。缺少有关该函数的其他信息(它是 std::copy 吗?可能不是,因为在调用 std::swap 时明确提供了 std::),我们必须假设它可以抛出。
  • 你把头发剪得很细。是的,如果 copy() 可以抛出,那么 Foo::Foo 就会被破坏。问题仍然不是关于 Foo::Foo 的,而是关于 Bar 的。假设 Foo::Foo 没问题,Bar 的实现也是如此。当然,假设 Foo 以某种方式损坏,Bar 也必须如此。
  • 啊,是的。 Bar 构造函数很好。 +1
【解决方案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; 
}

即检查您可以先回收资源的情况。如果你不能回收你的资源,那就复制和交换(或其他)。

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

【讨论】:

  • 你说的很对。但是,您应该添加一个右值引用重载。否则,只要在赋值右侧使用右值,您的“优化”实际上会损害性能。
  • @pgroke:右值引用仅在 C++0x 中可用,我认为 OP 还没有准备好,我担心它只会让他感到困惑(请记住,他不确定如何参考工作)。恐怕这个答案已经有点牵强了。
  • 我知道右值引用仅在 C++0x 中可用。您可能是对的,讨论右值引用可能会使 OP 感到困惑。尽管如此,我认为如果讨论这种优化,尽可能完整是没有坏处的。
  • 这个“优化”的问题——如果元素复制构造函数可以抛出,对象就会变得一团糟(int 不是真正的问题)。复制和交换要么成功,要么保持旧值不变。
猜你喜欢
  • 2011-10-28
  • 1970-01-01
  • 2023-03-23
  • 2011-11-22
  • 2020-05-27
  • 2014-06-23
  • 2016-02-17
  • 2013-08-10
  • 1970-01-01
相关资源
最近更新 更多