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

时间:2014-05-28 17:45:05

标签: c++

我已在copy-and-swap various中看到推荐的places惯用法,作为为赋值运算符实现强异常安全性的推荐/最佳/唯一方法。在我看来,这种方法也有缺点。

考虑以下简化的类似矢量的类,它使用了copy-and-swap:

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 ...
}

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

考虑在具有< 2GB堆限制的计算机上为1GB IntVec分配700MB IntVec的情况。最佳分配将意识到它已经分配了足够的内存并且仅将数据复制到其已经分配的缓冲区中。复制和交换实现将导致在释放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;
}

所以底线是:
我是对的,这是复制和交换习惯用法的问题,或者正常的编译器优化以某种方式消除额外的分配,或者我忽略了我的&#34;更好的&#34;复制和交换解决的版本,或者我的数学/算法错误,问题是否真的存在?

6 个答案:

答案 0 :(得分:15)

重用空间

的实现有两个问题
  • 如果您正在分配very_huge_vect = very_small_vect;,则不会释放额外的内存。这可能是你想要的,也可能不是。

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

顺便说一句,作为一般规则,在极少数情况下,您可以找到任何看起来像&#34;始终是最佳解决方案&#34;。如果您正在寻找灵丹妙药,编程就不会是正确的选择。

答案 1 :(得分: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;
}

虽然这确实会导致发生大量分配,但是在发生大量重新分配后会立即发生。

复制交换的关键部分是它实现了正确实现,并且获得异常安全的版权是很棘手的。

您将注意到,如果抛出异常,上面的内容会导致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;
}

模仿您的副本内容,但由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;
  }
}

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

答案 2 :(得分:11)

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

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

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

答案 3 :(得分:5)

  

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

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

  是的,我忽略了我的&#34;更好的问题&#34;复制和交换解决的版本

  1. 针对极少数情况进行优化。为什么要执行额外的检查并为代码添加圈复杂度? (除非当然,在你的申请中有太大的载体是常态)

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

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

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

    T& T::operator=(const T&);
    

    通过const&而不是通过值接受其参数(如复制和交换习惯用法)

答案 4 :(得分:1)

你的方法有两点需要注意。

  1. 考虑将1KB IntVec分配给1GB IntVec的情况。你最终将获得大量已分配但未使用(浪费)的内存。
  2. 如果在复制过程中抛出异常怎么办?如果您要覆盖现有位置,最终会导致数据被损坏,部分复制。
  3. 正如您所指出的,解决这些问题可能不是非常有效,但软件设计总是需要权衡。

答案 5 :(得分:0)

您可以看到STL如何实现vector = of vector。

template <class _Tp, class _Alloc>
vector<_Tp,_Alloc>& 
vector<_Tp,_Alloc>::operator=(const vector<_Tp, _Alloc>& __x)
{
  if (&__x != this) {
    const size_type __xlen = __x.size();
    if (__xlen > capacity()) {
      iterator __tmp = _M_allocate_and_copy(__xlen, __x.begin(), __x.end());
      destroy(_M_start, _M_finish);
      _M_deallocate(_M_start, _M_end_of_storage - _M_start);
      _M_start = __tmp;
      _M_end_of_storage = _M_start + __xlen;
    }
    else if (size() >= __xlen) {
      iterator __i = copy(__x.begin(), __x.end(), begin());
      destroy(__i, _M_finish);
    }
    else {
      copy(__x.begin(), __x.begin() + size(), _M_start);
      uninitialized_copy(__x.begin() + size(), __x.end(), _M_finish);
    }
    _M_finish = _M_start + __xlen;
  }
  return *this;
}