首页 文章

如何实际实施五条规则?

提问于
浏览
41

UPDATE 在底部

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

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

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

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

现在输入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;
}

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

AnObject make()
{
  return AnObject();
}

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

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

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

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

UPDATE 所以似乎有两个阵营:

  • 有人说要跳过移动赋值运算符并继续执行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使用什么都没关系,两个阵营的结果几乎相同:相同的分配数量,时间上的微小变化,但没有可重现的显着差异 .

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

6 回答

  • 1

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

    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 的复制赋值操作符现在只具有基本的异常安全性,而不是强大的异常安全性 . 但考虑一下:

    您的客户始终可以选择快速分配操作员并为其提供强大的异常安全性 . 但他们不能采用慢速赋值运算符并使其更快 . 模板<class T>
    T&
    strong_assign(T&x,T y)
    {
    swap(x,y);
    返回x;
    }

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

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

    ...

    数据a = MakeData();
    数据c = 5 * a(1 MakeMoreData())/ 3;
    问题2:使用复制省略/ RVO /移动语义的组合,编译器应该能够以最少的复制完成此操作,不是吗?

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

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

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

  • 4

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

    您的复制操作看起来很合理,但您的移动操作却没有 . 首先,虽然rvalue引用参数将 bind 到rvalue,但在函数内它是 lvalue ,所以你的移动构造函数应该是:

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

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

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

    如您所见,您不能在移动赋值运算符内的整个对象上使用 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;
    };
    
  • 1

    让我来帮助你:

    #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 正确管理其资源并优化我可以编写的任何代码中的副本/移动 .

  • 13

    既然我还没有看到其他人明确指出这一点......

    由于复制省略,如果(和 only 如果)它传递了一个右值,那么您的副本赋值运算符按值获取其参数是一个重要的优化机会 . 但是在具有赋值运算符的类中,显式 only 采用rvalues(即,具有移动赋值运算符的一个),这是一个荒谬的场景 . 因此,模拟已经在其他答案中指出的内存泄漏,我会说如果你只是改变复制赋值运算符以通过const引用获取其参数,那么你的类已经很理想了 .

  • 17

    q3 of the Original Poster

    我认为你(以及其他一些响应者)误解了编译器错误的含义,并因此得出了错误的结论 . 编译器认为(移动)赋值调用是不明确的,它是正确的!您有多个同等资格的方法 .

    在原始版本的 AnObject 类中,复制构造函数通过 const (左值)引用接受旧对象,而赋值运算符通过(非限定)值获取其参数 . value参数由适当的传递构造函数从运算符右侧的任何内容初始化 . 由于您只有一个传输构造函数,因此无论原始右侧表达式是左值还是右值,都始终使用该复制构造函数 . 这使赋值运算符充当复制赋值特殊成员函数 .

    一旦搬家,情况就会发生变化构造函数已添加 . 每当调用赋值运算符时,传输构造函数有两种选择 . 复制构造函数仍将用于左值表达式,但只要给出右值表达式,就会使用移动构造函数!这使赋值运算符同时充当移动赋值特殊成员函数 .

    当您添加传统的移动赋值运算符时,您为该类提供了相同特殊成员函数的两个版本,这是一个错误 . 你已经拥有了你想要的东西,所以只需要摆脱传统的移动赋值运算符,而不需要进行其他更改 .

    在您的更新中列出的两个阵营中,我想我在技术上处于第一阵营,但原因完全不同 . (不要跳过(传统的)移动赋值运算符,因为它对于你的类来说是“破碎的”,但是因为它是多余的 . )

    顺便说一句,我是读新的C 11和StackOverflow的新手 . 我在浏览另一个S.O.时想出了这个答案 . 在看到这个之前的问题 . ( Update :实际上,我仍然打开page . 链接转到FredOverflow的特定响应,显示了该技术 . )

    About the 2011-May-12 Response by Howard Hinnant

    (我是一个直接评论回答的新手 . )

    如果稍后的测试已经剔除它,则不需要显式检查自我赋值 . 在这种情况下, n != rh.n 已经完成了大部分工作 . 但是, std::copy 调用在(当前)内部 if 之外,因此我们将获得 n 组件级别的自我分配 . 由您来决定这些作业是否过于反向,即使自我指派应该很少 .

  • 1

    在委托构造函数的帮助下,您只需要实现一次每个概念;

    • 默认初始化

    • 资源删除

    • 交换

    • 副本

    其余的只是使用那些 .

    另外不要忘记进行移动分配(和交换) noexcept ,如果你有很多帮助,例如,你把你的 class 放在 vector

    #include <utility>
    
    // header
    
    class T
    {
    public:
        T();
        T(const T&);
        T(T&&);
        T& operator=(const T&);
        T& operator=(T&&) noexcept;
        ~T();
    
        void swap(T&) noexcept;
    
    private:
        void copy_from(const T&);
    };
    
    // implementation
    
    T::T()
    {
        // here (and only here) you implement default init
    }
    
    T::~T()
    {
        // here (and only here) you implement resource delete
    }
    
    void T::swap(T&) noexcept
    {
        using std::swap; // enable ADL
        // here (and only here) you implement swap, typically memberwise swap
    }
    
    void T::copy_from(const T& t)
    {
        if( this == &t ) return; // don't forget to protect against self assign
        // here (and only here) you implement copy
    }
    
    // the rest is generic:
    
    T::T(const T& t)
        : T()
    {
        copy_from(t);
    }
    
    T::T(T&& t)
        : T()
    {
        swap(t);
    }
    
    auto T::operator=(const T& t) -> T&
    {
        copy_from(t);
        return *this;
    }
    
    auto T::operator=(T&& t) noexcept -> T&
    {
        swap(t);
        return *this;
    }
    

相关问题