复制和交换始终是最佳解决方案吗?

Bar*_*uch 18 c++

我已经看到了在各个地方推荐的复制和交换习惯用法,作为为赋值运算符实现强异常安全性的推荐/最佳/唯一方法.在我看来,这种方法也有缺点.

考虑以下简化的类似矢量的类,它使用复制和交换:

class IntVec {
  size_t size;
  int* vec;
public:
  IntVec()
    : size(0),
      vec(0)
  {}
  IntVec(IntVec const& other)
    : size(other.size),
      vec(size? new int[size] : 0)
  {
    std::copy(other.vec, other.vec + size, vec);
  }

  void swap(IntVec& other) {
    using std::swap;
    swap(size, other.size);
    swap(vec, other.vec);
  }

  IntVec& operator=(IntVec that) {
    swap(that);
    return *this;
  }

  //~IntVec() and other functions ...
}
Run Code Online (Sandbox Code Playgroud)

通过复制构造函数实现赋值可能是有效的并且可以保证异常安全,但是它也可能导致不必要的分配,甚至可能导致无内存错误.

考虑分配700MB的情况下IntVec,以一个1GB IntVec以<2GB堆限制的机器上.最佳分配将意识到它已经分配了足够的内存,并且只将数据复制到已经分配的缓冲区中.复制和交换实现将导致在释放1GB缓冲区之前分配另一个700MB缓冲区,导致所有3个缓冲区同时尝试在内存中共存,这将不必要地抛出内存不足错误.

这种实现可以解决问题:

IntVec& operator=(IntVec const& that) {
  if(that.size <= size) {
    size = that.size;
    std::copy(that.vec, that.vec + that.size, vec);
  } else
    swap(IntVec(that));
  return *this;
}
Run Code Online (Sandbox Code Playgroud)

所以底线是:
我是对的,这是复制和交换习惯用法的问题,或者正常的编译器优化以某种方式消除额外的分配,或者我忽略了一些问题与我的"更好"版本的副本 -和交换一个解决,或者我在做我的数学/算法错误,问题不存在?

650*_*502 15

重用空间的实现存在两个问题

  • 如果你要分配very_huge_vect = very_small_vect;额外的内存将不会被释放.这可能是你想要的,也可能不是.

  • 在整数的情况下,一切都很好,但复制操作可能引发异常的对象呢?你最终会得到一个混乱的数组,其中部分副本已经完成并且已被截断.如果复制操作失败(交换习惯做什么),更好的办法是保持目标不受影响.

顺便说一句,作为一般规则,在极少数情况下,您可以找到任何看起来像"永远是最佳解决方案"的东西.如果你正在寻找一颗银弹,那么编程就不是正确的选择.


Yak*_*ont 12

要解决您的特定问题,请将copy-swap修改为clear-copy-swap.

这可以通过以下方式进行:

Foo& operator=( Foo const& o ) {
  using std::swap;
  if (this == &o) return *this; // efficient self assign does nothing
  swap( *this, Foo{} ); // generic clear
  Foo tmp = o; // copy to a temporary
  swap( *this, tmp ); // swap temporary state into us
  return *this;
}
Foo& operator=( Foo && o ) {
  using std::swap;
  if (this == &o) return *this; // efficient self assign does nothing
  swap( *this, Foo{} ); // generic clear
  Foo tmp = std::move(o); // move to a temporary
  swap( *this, tmp ); // swap temporary state into us
  return *this;
}
Run Code Online (Sandbox Code Playgroud)

虽然这确实会导致大量分配发生,但是在发生大量释放后会立即发生.

copy-swap的关键部分是它实现了正确的实现,并且获得异常安全的副本权限非常棘​​手.

你会注意到,如果抛出异常,上面的结果是lhs可能处于空状态.相比之下,标准的copy-swap将产生有效的副本,或lhs不变.

现在,我们可以做一个最后的小技巧.假设我们的状态是在一个vector子状态中捕获的,那些子状态具有异常安全swapmove.然后我们可以:

Foo& move_substates( Foo && o ) {
  if (this == &o) return *this; // efficient self assign does nothing
  substates.resize( o.substates.size() );
  for ( unsigned i = 0; i < substates.size(); ++i) {
    substates[i] = std::move( o.substates[i] );
  }
  return *this;
}
Run Code Online (Sandbox Code Playgroud)

它模仿你的副本内容,但是move代替它copy.然后我们可以在以下方面利用它operator=:

Foo& operator=( Foo && o ) {
  using std::swap;
  if (this == &o) return *this; // efficient self assign does nothing
  if (substates.capacity() >= o.substates.size() && substates.capacity() <= o.substates.size()*2) {
    return move_substates(std::move(o));
  } else {
    swap( *this, Foo{} ); // generic clear
    Foo tmp = std::move(o); // move to a temporary
    swap( *this, tmp ); // swap temporary state into us
    return *this;
  }
}
Run Code Online (Sandbox Code Playgroud)

现在我们重用内部内存并避免分配,如果我们从源代码移动,如果你害怕内存分配,我们不会比源代码大得多.


Zan*_*ynx 11

是的,内存不足是一个潜在的问题.

您已经知道复制和交换解决了什么问题.这是你"撤消"失败的任务的方式.

使用更有效的方法,如果在流程的某个阶段分配失败,则无法返回.而且,如果一个对象编写得不好,失败的赋值甚至可能使对象损坏,并且程序在对象销毁期间会崩溃.


Nik*_*iou 5

我是对的,这是复制和交换习语的问题

这取决于; 如果你有这么大的载体,那么是的,你是对的.

我是否忽略了复制交换解决的"更好"版本的问题

  1. 你正在优化罕见的情况.为什么要执行额外的检查并为代码添加圈复杂度?(除非当然,在您的应用中有太大的载体是常态)

  2. 从C++ 11开始,临时数的r值可以通过c ++来收获.所以通过const&抛弃优化来传递你的论点.

底线:这个词总是容易被反驳.如果有一个通用解决方案,总是优于任何替代方案,我想编译器可以采用它并隐式生成基于此的默认赋值运算符

在前面的注释中,隐式声明的复制赋值运算符具有表单

T& T::operator=(const T&);
Run Code Online (Sandbox Code Playgroud)

通过const&而不是通过值接受其参数(如在复制和交换习语中)