C++ 复制和交换总是最好的解决方案吗?

C++ 复制和交换总是最好的解决方案吗?,c++,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[siz

我认为中推荐的习惯用法是为赋值操作符实现强异常安全性的推荐/最佳/唯一方法。在我看来,这种方法也有缺点

考虑以下使用复制和交换的简化类向量:

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 ...
}
通过复制构造函数实现分配可能是有效的,可以保证异常安全,但也可能导致不必要的分配,甚至可能导致不必要的内存不足错误


考虑将700MB
IntVec
分配给具有的机器上的1GB
IntVec
的情况。实现重用空间存在两个问题

  • 如果您正在分配
    非常大的向量=非常小的向量将不会释放额外内存。这可能是你想要的,也可能不是

  • 对于整数,一切都可以,但是对于复制操作可能引发异常的对象呢?您将得到一个混乱的数组,其中部分拷贝已经完成,并且被截断。如果复制操作失败(交换习惯用法就是这样做的),最好保持目标不动


顺便说一句,一般来说,在很少的情况下,你可以找到任何看起来“总是最好的解决方案”的东西。如果您正在寻找解决方案,那么编程将不是正确的选择。

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

您已经知道复制和交换解决了什么问题。这就是如何“撤消”失败的分配


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

要解决您的特定问题,请修改“复制交换”以清除“复制交换”

这可以通过以下方式实现:

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可能处于空状态。相比之下,标准副本交换将产生有效副本,或lhs保持不变

现在,我们可以做最后一个小把戏。假设我们的状态被捕获在子状态的
向量中,并且这些子状态具有异常安全的
交换
移动
。然后我们可以:

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;
}
现在,如果我们从一个源移动,我们会重用内部内存并避免分配,如果你害怕内存分配,我们不会比源大太多

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

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

我是否忽略了复制和交换版本解决的“更好”版本的一些问题

  • 您正在为罕见的情况进行优化。为什么要执行额外的检查并给代码增加圈复杂度?(当然,除非应用程序中的向量太大)

  • < C++ > 11 C++以来,临时变量的r值可由C++获得。因此,通过
    const&
    传递参数会丢弃优化

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

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


    通过常量而不是值接受它的参数(如复制和交换习惯用法)

    您的方法有两个警告

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

  • 正如您所指出的,解决这些问题可能不是很节省内存,但软件设计始终是权衡的。

    您可以看到STL如何实现向量的operator=of

    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;
    }
    
    模板
    向量&
    向量::运算符=(常量向量和x)
    {
    如果(&x!=此){
    const size_type_uuxlen=uuuux.size();
    如果(\uxlen>capacity()){
    迭代器\uuuTMP=\uM\uAllocate\u和\uCopy(\uuxlen,\uuux.begin(),\uuuuux.end());
    销毁(开始、结束);
    _M_解除分配(_M_开始,_M_结束\u存储-_M_开始);
    _M_开始=u tmp;
    _存储器的M_end_=_M_start+_xlen;
    }
    否则如果(大小()>=\uxLen){
    迭代器i=copy(uuux.begin(),uuux.end(),begin());
    销毁(i,M,finish);
    }
    否则{
    复制(uuu x.begin(),uuu x.begin()+大小(),u M_start);
    未初始化的拷贝(uuu x.begin()+size(),uuu x.end(),u M_finish);
    }
    _M_finish=_M_start+_xlen;
    }
    归还*这个;
    }
    
    就个人而言,我基本上不使用复制和交换,因为正如您所指出的,它实际上从来都不是最有效的技术。。。我们使用C++来提高效率,不要编写拷贝700兆字节向量的程序。至少不是700兆字节的向量,这些向量包含构造函数和析构函数,可以抛出异常。@Dave:Copy and swap比optimal多了一步,编译器可能会对其进行优化。唯一应该成为问题的时间是,如果您的移动是重量级的(如
    std::array
    ),也建议复制和交换,因为它只有3行代码,而且相对来说是复制粘贴。更少出错的机会。优化的代码需要更多的代码;掉期(a、b)
    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;
      }
    }
    
    T& T::operator=(const T&);
    
    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;
    }