如何实际执行五项规则?

在底部更新

问题1:如何为一个管理相当重的资源的类实施五项规则,但是您希望它按价值传递,因为这极大地简化和美化了它的用法。 或者不是规则的全部五项甚至需要?

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

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2:使用复制elision / 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;
};

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

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

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

AnObject make()
{
  return AnObject();
}

int main()
{
  AnObject a;
  a = make(); //error C2593: 'operator =' is ambiguous
}

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

除此之外,很明显移动构造函数和赋值的代码充满了重复。 因此,现在我们忘记了含糊不清,并尝试使用复制和交换来解决这个问题,但是现在对于右值。 正如这里所解释的,我们甚至不需要定制交换,而是使用std :: swap完成所有工作,这听起来非常有前途。 所以我写了以下内容,希望std :: swap会使用移动构造函数复制构建一个临时文件,然后将其与* this进行交换:

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

但是,这并不奏效,而是由于std :: swap再次调用我们的operator =(AnObject && rh),导致堆栈溢出,因为无限递归。 Q4:有人可以举例说明那个例子中的含义吗?

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

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

现在差不多是代码量的两倍,但是它的移动部分通过降低相当便宜的移动来支付; 但另一方面,正常的作业不再受益于复制手段。 在这一点上,我很困惑,而且再也看不到什么是非对的,所以我希望在这里得到一些输入。

更新所以看起来有两个阵营:

  • 一个说跳过移动赋值操作符并继续做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 >() );
      }
    }
    

    这个代码不够好,不足以测试我之后的事情,或者编译器太聪明了:无论我使用的是arraySize和numIter,这两个阵营的结果几乎完全相同:分配的数量相同,时间上非常微小的变化,但没有可再现的显着差异。

    所以除非有人能指出一个更好的方法来测试这个(鉴于实际的使用范围还不知道),我将不得不得出结论,这并不重要,因此留给开发者的味道。 在这种情况下,我会选择#2。


    您错过了复印分配操作员的重大优化。 随后情况变得混乱。

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

    除非你真的永远不会认为你会分配相同大小的AnObject这会更好。 永远不要扔掉资源,如果你可以回收它们。

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

    您的客户始终可以快速分配操作员,并赋予其强大的异常安全性。 但他们不能采用一个缓慢的赋值运算符并使其更快。

    template <class T>
    T&
    strong_assign(T& x, T y)
    {
        swap(x, y);
        return x;
    }
    

    移动构造函数没问题,但移动赋值运算符有内存泄漏。 它应该是:

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

    ...

    Data a = MakeData();
    Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
    

    q2:使用复制elision / RVO /移动语义的组合,编译器应该能够以最少的复制来实现这一点,不是吗?

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

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

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


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

    您的复制操作看起来很明智,但您的移动操作不会。 首先,虽然右值引用参数会绑定到右值,但在函数内它是左值 ,所以您的移动构造函数应该是:

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

    当然,对于像你这样的基本类型,其实并没有什么区别,但也应该养成这种习惯。

    如果您提供移动构造函数,那么当您定义复制分配时,您不需要移动赋值运算符,因为您接受按的参数,右值将移入参数而不是复制。

    正如你发现的那样,你不能在移动赋值操作符中的整个对象上使用std::swap() ,因为它会递归回移位赋值操作符。 您链接到的帖子中的评论意见是,如果您提供移动操作,则不需要实施自定义swap ,因为std::swap将使用移动操作。 不幸的是,如果你没有定义一个单独的移动赋值操作符,这不起作用,并且仍然会递归。 你当然可以使用std::swap交换成员:

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

    因此你的最后一堂课是:

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

    我来帮你:

    #include <vector>
    
    class AnObject
    {
    public:
      AnObject( size_t n = 0 ) : data(n) {}
    
    private:
      std::vector<int> data;
    };
    

    从C ++ 0x FDIS, [class.copy]注9:

    如果类X的定义没有明确声明移动构造函数,则当且仅当

  • X没有用户声明的拷贝构造函数,

  • X没有用户声明的复制赋值操作符,

  • X没有用户声明的移动赋值运算符,

  • X没有用户声明的析构函数,并且

  • 移动构造函数不会被隐式定义为删除。

  • [注意:当移动构造函数未被隐式声明或显式提供时,否则将调用移动构造函数的表达式可能会调用复制构造函数。 - 注意]

    就我个人而言,我对std::vector更加自信,正确地管理其资源并优化我可以编写的任何代码中的副本/移动。

    链接地址: http://www.djcxy.com/p/73159.html

    上一篇: How to actually implement the rule of five?

    下一篇: C++11 rvalues and move semantics confusion (return statement)