如何实际执行五条规则? [英] How to actually implement the rule of five?

查看:75
本文介绍了如何实际执行五条规则?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

UPDATE 在底部



q1:如何实现第五章用于管理相当重的资源的类,
,但你想要它通过价值传递,因为大大简化和美化它的用法?



在实践中,我开始使用3D成像技术,其中图像通常是128 * 128 * 128的双精度。
虽然写这样的东西能使数学更容易:

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

q2:使用copy elision / RVO / move语义的组合编译器应该能够这个与最少的复制,没有?



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

 类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)肯定修复它,但是我们不会失去一个相当重要的优化机会?



除此之外,很明显,构造函数和赋值是完全重复的。
所以现在我们忘记了模糊性,并尝试使用副本和交换解决这个问题,但是现在对于右值。
此处我们甚至不需要一个自定义交换,而是有std :: swap做所有的工作,这听起来很有前途。
所以我写了以下代码,希望std :: swap将使用move构造函数复制构造一个临时,然后用* this交换它:

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

但是这不会解决,反而导致堆栈溢出无限递归,因为std :: swap再次调用我们的operator =(AnObject&& rh)。 q4:有人可以提供示例中的示例吗?



我们可以通过提供第二个交换函数来解决这个问题: / p>

  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& amp; second)
{
first.n = second.n;
first.a = second.a;
second.n = 0;
second.a = nullptr;
}

现在有几乎两倍的金额代码,通过以下相当便宜的移动;但另一方面,正常分配不能再受益于复制缺失。
在这一点上,我真的很困惑,虽然看不到什么是对和错,所以我希望得到一些输入这里..



UPDATE 因此,它似乎有两个阵营:




  • 一个说跳过移动赋值运算符, C ++ 03教我们,即写一个赋值运算符,通过值传递参数。

  • 另一个说实现移动赋值运算符(毕竟,它是C ++ 11现在),并让副本赋值运算符以引用的方式引用它的参数。



(确定,第三个阵营告诉我使用向量,但是这是一个超出这个假设类的范围。在现实生活中,我会使用一个向量,还会有其他成员,但由于移动构造函数/赋值不是自动生成的(但?)问题仍然抱歉)



不幸的是,我不能在现实世界的情况下测试这两个实现,因为这个项目刚刚开始,数据实际流动的方式还不知道。所以我只是实现了它们,添加计数器分配等,并运行了几个迭代约。这个代码,其中T是其中一个实现:

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

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

template<类T>
void Test()
{
T a;
T b;
for(size_t i = 0; 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。

解决方案

您错过了一个重要的优化在您的副本赋值运算符。后来情况变得混乱了。

  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 s,这更好。



有些人可能会抱怨现在的 AnObject 的副本分配运算符只有基本的异常安全而不是强大的异常安全。不过,请考虑以下方面:


您的客户端可以使用快速
赋值运算符并赋予它强大的
异常安全性。但是他们不能接受
a慢赋值运算符,并使它
更快。

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

...



< blockquote>

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

q2:使用copy elision / RVO / move语义的组合
编译器应该能够至少这个
与最小的复制,没有?


重载您的运算符以利用右值资源:

 数据运算符+(数据& x,const数据& y)
{
//在x中循环资源!
x + = y;
return std :: move(x);
}



最后,每个数据你关心。

UPDATE at the bottom

q1: How would you implement the rule of five for a class that manages rather heavy resources, but of which you want it to be passed around by value because that greatly simplifies and beautifies it's usage? Or are not all five items of the rule even needed?

In practice, I'm starting something with 3D imaging where an image is usually 128*128*128 doubles. Being able though to write things like this would make the math alot easier:

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

q2: Using a combination of copy elision / RVO / move semantics the compiler should be able to this this with a minimum of copying, no?

I tried to figure out how to do this so I started with the basics; suppose an object implementing the traditional way of implementing copy and assignment:

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

Now enter rvalues and move semantics. As far as I can tell this would be a working implementation:

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

However the compiler (VC++ 2010 SP1) is not too happy with this, and compilers are usually correct:

AnObject make()
{
  return AnObject();
}

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

q3: How to solve this? Going back to AnObject& operator = ( const AnObject& rh ) certainly fixes it but don't we lose a rather important optimization opportunity?

Apart from that, it's clear that the code for the move constructor and assignment is full of duplication. So for now we forget about the ambiguity and try to solve this using copy and swap but now for rvalues. As explained here we wouldn't even need a custom swap but instead have std::swap do all the work, which sounds very promising. So I wrote the following, hoping std::swap would copy construct a temporary using the move constructor, then swap it with *this:

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

But that doesn't work out and instead leads to a stack overflow due to infinite recursion since std::swap calls our operator = ( AnObject&& rh ) again. q4: Can someone provide an example of what is meant in the example then?

We can solve this by providing a second swap function:

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

Now there's almost twice the amount code, however the move part of it pays of by alowing pretty cheap moving; but on the other hand the normal assignment can't benefit from copy elision anymore. At this point I'm really confused though, and not seeing anymore what's right and wrong, so I'm hoping to get some input here..

UPDATE So it seems there are two camps:

  • one saying to skip the move assignment operator and continue doing what C++03 taught us, ie write a single assignment operator that passes the argument by value.
  • the other one saying to implement the move assignment operator (after all, it's C++11 now) and have the copy assignment operator take its argument by reference.

(ok and there's the 3rd camp telling me to use a vector, but that's sort of out of scope for this hypothetical class. Ok in real life I would use a vector, and there would be also other members, but since the move constructor/assignment are not automatically generated (yet?) the question would still hold)

Unfortunately I cannot test both implementations in a real world scenario since this project has just started and the way the data will actually flow is not known yet. So I simply implemented both of them, added counters for allocation etc and ran a couple of iterations of approx. this code, where T is one of the implementations:

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

Either this code is not good enough to test what I'm after, or the compiler is just too smart: doesn't matter what I use for arraySize and numIter, the results for both camps are pretty much identical: same number of allocations, very slight variations in timing but no reproducable significant difference.

So unless someone can point to a better way to test this (given that the actual usage scnearios are not known yet), I'll have to conclude that it doesn't matter and hence is left to the taste of the developper. In which case I'd pick #2.

解决方案

You've missed a significant optimization in your copy assignment operator. And subsequently the situation has gotten confused.

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

Unless you really never think you'll be assigning AnObjects of the same size, this is much better. Never throw away resources if you can recycle them.

Some might complain that the AnObject's copy assignment operator now has only basic exception safety instead of strong exception safety. However consider this:

Your clients can always take a fast assignment operator and give it strong exception safety. But they can't take a slow assignment operator and make it faster.

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

Your move constructor is fine, but your move assignment operator has a memory leak. It should be:

  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: Using a combination of copy elision / RVO / move semantics the compiler should be able to this this with a minimum of copying, no?

You may need to overload your operators to take advantage of resources in rvalues:

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

Ultimately resources ought to be created exactly once for each Data you care about. There should be no needless new/delete just for the purpose of moving things around.

这篇关于如何实际执行五条规则?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆