UPDATE at
the
bottom
q1:
How would you implement the href="https://stackoverflow.com/questions/4782757/rule-of-three-becomes-rule-of-five-with-c0x">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 href="https://stackoverflow.com/questions/2078515/why-arent-there-compiler-generated-swap-methods-in-c0x/2078987#2078987">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 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.
No comments:
Post a Comment