如何实际实施五条规则?

sti*_*ijn 42 c++ rvalue-reference move-semantics c++11

在底部更新

问题1:如何为管理相当繁重的资源的类实现五条规则,但是您希望它按值传递,因为这极大地简化并美化了它的用法?或者甚至不需要所有五项规则?

在实践中,我开始使用3D成像,其中图像通常是128*128*128双倍.能够写这样的东西会让数学变得更容易:

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
Run Code Online (Sandbox Code Playgroud)

问题2:使用复制省略/ RVO /移动语义的组合,编译器应该能够以最少的复制完成此操作,不是吗?

我试图找出如何做到这一点,所以我从基础开始; 假设一个对象实现了实现复制和赋值的传统方式:

class AnObject
{
public:
  AnObject( size_t n = 0 ) :
    n( n ),
    a( new int[ n ] )
  {}
  AnObject( const AnObject& rh ) :
    n( rh.n ),
    a( new int[ rh.n ] )
  {
    std::copy( rh.a, rh.a + n, a );
  }
  AnObject& operator = ( AnObject rh )
  {
    swap( *this, rh );
    return *this;
  }
  friend void swap( AnObject& first, AnObject& second )
  {
    std::swap( first.n, second.n );
    std::swap( first.a, second.a );
  }
  ~AnObject()
  {
    delete [] a;
  }
private:
  size_t n;
  int* a;
};
Run Code Online (Sandbox Code Playgroud)

现在输入rvalues并移动语义.据我所知,这将是一个有效的实施:

AnObject( AnObject&& rh ) :
  n( rh.n ),
  a( rh.a )
{
  rh.n = 0;
  rh.a = nullptr;
}

AnObject& operator = ( AnObject&& rh )
{
  n = rh.n;
  a = rh.a;
  rh.n = 0;
  rh.a = nullptr;
  return *this;
}
Run Code Online (Sandbox Code Playgroud)

但是编译器(VC++ 2010 SP1)对此并不满意,编译器通常是正确的:

AnObject make()
{
  return AnObject();
}

int main()
{
  AnObject a;
  a = make(); //error C2593: 'operator =' is ambiguous
}
Run Code Online (Sandbox Code Playgroud)

问题3:如何解决这个问题?回到AnObject&operator =(const AnObject&rh)肯定会修复它,但是我们不会失去一个相当重要的优化机会吗?

除此之外,很明显移动构造函数和赋值的代码充满了重复.所以现在我们忘记了歧义,并尝试使用复制和交换来解决这个问题,但现在我们用于rvalues.正如这里所解释的,我们甚至不需要自定义交换,而是让std :: swap完成所有工作,听起来非常有前景.所以我写了以下内容,希望std :: swap将使用move构造函数复制构造一个临时构建器,然后用*this交换它:

AnObject& operator = ( AnObject&& rh )
{
  std::swap( *this, rh );
  return *this;
}
Run Code Online (Sandbox Code Playgroud)

但由于std :: swap再次调用我们的operator =(AnObject && rh),因此无效递归会导致堆栈溢出.问题4:有人可以举例说明示例中的含义吗?

我们可以通过提供第二个交换功能来解决这个问题

AnObject( AnObject&& rh )
{
  swap( *this, std::move( rh ) );
}

AnObject& operator = ( AnObject&& rh )
{
  swap( *this, std::move( rh ) );
  return *this;
}

friend void swap( AnObject& first, AnObject&& second )
{
  first.n = second.n;
  first.a = second.a;
  second.n = 0;
  second.a = nullptr;
}
Run Code Online (Sandbox Code Playgroud)

现在几乎是金额代码的两倍,但是它的移动部分是通过提供相当便宜的移动来支付的; 但另一方面,正常的任务不再受益于复制省略.在这一点上,我真的很困惑,并没有看到什么是对的,所以我希望在这里得到一些输入..

更新所以似乎有两个阵营:

  • 一个人说要跳过移动赋值运算符并继续执行C++ 03教给我们的东西,即编写一个按值传递参数的单个赋值运算符.
  • 另一个说实现移动赋值运算符(毕竟,现在是C++ 11)并让复制赋值运算符通过引用获取其参数.

(好吧,第三阵营告诉我使用一个向量,但这有点超出了这个假设类的范围.在现实生活中我会使用一个向量,并且还会有其他成员,但是因为移动构造函数/分配不是自动生成的(但是?)问题仍然存在)

遗憾的是,由于该项目刚刚开始并且数据实际流动的方式尚不清楚,因此无法在实际场景中测试这两种实现.所以我简单地实现了它们,添加了分配计数器等,并运行了大约几次迭代.这段代码,其中T是其中一个实现:

template< class T >
T make() { return T( narraySize ); }

template< class T >
void assign( T& r ) { r = make< T >(); }

template< class T >
void Test()
{
  T a;
  T b;
  for( size_t i = 0 ; i < numIter ; ++i )
  {
    assign( a );
    assign( b );
    T d( a );
    T e( b );
    T f( make< T >() );
    T g( make< T >() + make< T >() );
  }
}
Run Code Online (Sandbox Code Playgroud)

要么这段代码还不足以测试我正在追求的东西,要么编译器太聪明了:无论我对arraySize和numIter使用什么都没关系,两个阵营的结果几乎相同:相同的分配数量,时间上的微小变化,但没有可重现的显着差异.

因此,除非有人可以指出一种更好的方法来测试这个(假设实际使用的scnearios尚未知晓),我将不得不得出结论,它无关紧要,因此留给开发者的味道.在这种情况下,我会选择#2.

How*_*ant 17

您错过了复制赋值运算符的重要优化.随后情况变得混乱.

  AnObject& operator = ( const AnObject& rh )
  {
    if (this != &rh)
    {
      if (n != rh.n)
      {
         delete [] a;
         n = 0;
         a = new int [ rh.n ];
         n = rh.n;
      }
      std::copy(rh.a, rh.a+n, a);
    }
    return *this;
  }
Run Code Online (Sandbox Code Playgroud)

除非你真的从不认为你会分配AnObject相同大小的s,否则这要好得多.如果可以回收资源,切勿丢弃资源.

有些人可能会抱怨说,AnObject复制赋值操作符现在只有基本的异常安全性,而不是强大的异常安全性.但考虑一下:

您的客户始终可以使用快速分配操作员并为其提供强大的异常安全性.但他们不能采用慢速赋值运算符并使其更快.

template <class T>
T&
strong_assign(T& x, T y)
{
    swap(x, y);
    return x;
}
Run Code Online (Sandbox Code Playgroud)

您的移动构造函数很好,但您的移动赋值运算符有内存泄漏.它应该是:

  AnObject& operator = ( AnObject&& rh )
  {
    delete [] a;
    n = rh.n;
    a = rh.a;
    rh.n = 0;
    rh.a = nullptr;
    return *this;
  }
Run Code Online (Sandbox Code Playgroud)

...

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
Run Code Online (Sandbox Code Playgroud)

问题2:使用复制省略/ RVO /移动语义的组合,编译器应该能够以最少的复制完成此操作,不是吗?

您可能需要重载运算符以利用rvalues中的资源:

Data operator+(Data&& x, const Data& y)
{
   // recycle resources in x!
   x += y;
   return std::move(x);
}
Run Code Online (Sandbox Code Playgroud)

最终,资源应该为Data您关注的每一个创建一次.new/delete只是为了移动东西,不应该有任何不必要的东西.

  • @DeadMG:感谢你找到你的downvote.我非常不同意你的立场,我很乐意接受一百万次投票而不改变我的立场.复制/交换习语只不过是一个穷人的C++ 03移动赋值操作符.在工具箱中有一个真正的移动赋值运算符后,盲目地沿着该路径继续是不合逻辑的.每个感兴趣的人都应该计算分配并衡量绩效.测试左值和左值的rhs.测试您的常见用例.Otoh,如果你不关心性能那么移动成员是浪费时间. (3认同)

Ant*_*ams 13

如果您的对象资源很多,您可能希望完全避免复制,只需提供移动构造函数和移动赋值运算符.但是,如果您真的想要复制,则很容易提供所有操作.

您的复制操作看起来很合理,但您的移动操作却没有.首先,虽然rvalue引用参数将绑定到rvalue,但在函数内它是一个左值,因此你的移动构造函数应该是:

AnObject( AnObject&& rh ) :
  n( std::move(rh.n) ),
  a( std::move(rh.a) )
{
  rh.n = 0;
  rh.a = nullptr;
}
Run Code Online (Sandbox Code Playgroud)

当然,对于你已经到过这里的基本类型,它实际上并没有什么不同,但是养成习惯也是如此.

如果你提供了一个移动构造函数,那么当你定义复制赋值时就不需要一个移动赋值操作符了 - 因为你按接受参数,一个rvalue将被移入参数而不是被复制.

如您所见,您不能std::swap()在移动赋值运算符内的整个对象上使用,因为它将递归回移动赋值运算符.您链接到的帖子中的注释点是,swap如果您提供移动操作,则不需要实现自定义,因为std::swap您将使用移动操作.不幸的是,如果你没有定义一个单独的移动赋值运算符,这不起作用,并且仍会递归.您当然可以std::swap用来交换成员:

AnObject& operator=(AnObject other)
{
    std::swap(n,other.n);
    std::swap(a,other.a);
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

因此,你的最后一堂课是:

class AnObject
{
public:
  AnObject( size_t n = 0 ) :
    n( n ),
    a( new int[ n ] )
  {}
  AnObject( const AnObject& rh ) :
    n( rh.n ),
    a( new int[ rh.n ] )
  {
    std::copy( rh.a, rh.a + n, a );
  }
  AnObject( AnObject&& rh ) :
    n( std::move(rh.n) ),
    a( std::move(rh.a) )
  {
    rh.n = 0;
    rh.a = nullptr;
  }
  AnObject& operator = ( AnObject rh )
  {
    std::swap(n,rh.n);
    std::swap(a,rh.a);
    return *this;
  }
  ~AnObject()
  {
    delete [] a;
  }
private:
  size_t n;
  int* a;
};
Run Code Online (Sandbox Code Playgroud)