复制-交换总是最好的解决方案吗?



我看到在不同地方推荐的复制-交换习惯用法是为赋值操作符实现强异常安全的推荐/最佳/唯一方法。在我看来,这种方法也有缺点。

考虑下面这个简化的类向量,它利用了复制和交换:

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堆限制的机器上将700MB的IntVec分配给1GB的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;
}

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

实现重用空间有两个问题

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

  • 对于整数来说一切都很好,但是对于复制操作可能抛出异常的对象呢?你最终会得到一个混乱的数组,其中部分复制已经完成,并且已经被截断。更好的做法是,如果复制操作失败(swap习惯用法就是这样做的),保持目标不变。

顺便说一下,作为一般规则,在极少数情况下,你可以找到任何看起来"总是最好的解决方案"。如果您正在寻找灵丹妙药,那么编程将不是正确的选择。

要解决您的特定问题,请将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;
}

虽然这确实会导致大的分配发生,但它会在大的释放发生后立即发生。

copy-swap的关键部分是它使正确的实现,而获得异常安全的拷贝权是很棘手的。

您将注意到,如果抛出异常,则会导致lhs处于空状态。相比之下,标准的拷贝交换将产生一个有效的拷贝,或者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;
  }
}

,现在我们重用了内部内存,避免了从源移动时的内存分配,如果你害怕内存分配,我们不会比源大太多。

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

你已经知道复制和交换解决了什么问题。这是你如何"撤销"一个失败的分配。

使用更有效的方法,如果在过程的某个阶段赋值失败,就没有办法返回。而且,如果对象写得很糟糕,赋值失败甚至可能导致对象损坏,程序在对象销毁期间崩溃。

我说这是复制-交换习惯用法的问题,对吗

视情况而定;如果你有这么大的向量,那么是的你是对的。

我是否忽略了我的"更好"版本所解决的一些问题

  1. 您正在针对罕见情况进行优化。为什么要执行额外的检查并增加代码的圈复杂度?(当然,除非有这么大的向量是你的应用程序的标准)

  2. 从c++ 11开始,c++可以获取临时变量的r值。所以通过const&传递你的参数会丢掉优化。

底线:总是这个词很容易被反驳。如果有一个通用的解决方案,总是优于任何替代方案,我想编译器可以采用它,并基于这个

隐式地生成一个默认的赋值操作符。在前面的注释中,隐式声明的复制赋值操作符的形式为
T& T::operator=(const T&);

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

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

  1. 考虑将1KB IntVec分配给1GB IntVec的情况。你最终会得到大量已分配但未使用(浪费)的内存。
  2. 如果在复制过程中抛出异常怎么办?如果你覆盖现有的位置,你最终会得到损坏的,部分复制的数据。

正如你所指出的,解决这些问题可能不是很有效的内存,但软件设计总是关于权衡。

您可以看到STL是如何实现operator= 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;
}

相关内容

最新更新