|
Boost : |
From: Howard Hinnant (hinnant_at_[hidden])
Date: 2002-02-05 13:11:30
On Tuesday, February 5, 2002, at 12:22 PM, Peter Dimov wrote:
>>> Yup! A minimum of 2 types of move are absolutely necessary:
>>>
>>> 1. Move construct.
>>> 2. Move assign.
>>>
>>> One could argue that 2 more moves are useful:
>>>
>>> 3. Move construct, destructing the source.
>>> 4. Move assign, destructing the source.
>>>
>>> However 3 and 4 are very easily built on 1 and 2 by just calling the
>>> destructor on the source after the move.
>>
>> Not necessarily: sometimes an object must hold resources to maintain
>> class invariants. For example:
>>
>> class C {
>> public:
>> C() : fp(fopen("myfile", "rb")
>> {
>> if (!this->fp) throw something;
>> }
>> ~C()
>> {
>> fclose(fp); /* Do not check for null. */
>> }
>> private:
>> FILE *fp;
>> };
>
> That's why I prefer 2. and 3., not 1. and 2. (keeping the object count
> constant, so to speak.) We either start with two objects A and B and
> move B
> into A, or we start with object A and raw memory R and end up with
> object R'
> and raw memory A'.
>
> This makes sense, hopefully. :-)
Yup, makes sense. I can appreciate the symmetry. However I see a
problem with vector::insert using only 2 and 3:
Consider vector::insert(iterator position, size_type n, const
value_type& x);
There is enough capacity to handle the insert. To accomplish this the
vector must move some elements from the end of the vector to
uninitialized memory, say by using 3 (move construct, destruct source).
At this point there is an area in the middle of the vector of
uninitialized memory. If an exception is thrown while copy constructing
x into this unitialized memory, the vector escapes in a corrupted state.
However if the vector uses 1 (move construct) to move the elements out
of the way, and an exception is thrown while copy assigning x into
place, then the vector is still in a valid state. There are no holes of
uninitialized memory. Basic exception safety has been maintained.
Perhaps Rainer's example successfully argues that #3 is necessary.
vector::insert could make use of #3 in the case that capacity is
exceeded. It must move existing elements from the existing buffer to
the new buffer - constructing in the new buffer while destructing in the
old. I had been assuming that a 2-step, manual destruction move would
be sufficient, but perhaps not.
Otoh, I can imagine that it might be inconvenient for vector to use #3
instead of #1 followed, but not immediately followed, by a destructor
call (again for exception safety reasons). You might choose to build
the new buffer up, leaving the old buffer in a constructed valid state
until all of the x have been successfully copy constructed in. And only
then destruct the elements in the original buffer and swap the buffers.
So in this latter implementation, (as far as vector is concerned) a
class that doesn't support 1 and 2 is not movable.
-Howard
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk