Boost logo

Boost :

From: Hamish Mackenzie (boost_at_[hidden])
Date: 2002-02-05 13:36:39


On Tue, 2002-02-05 at 18:11, Howard Hinnant wrote:
> 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.

But even 1 will leave the source damaged will it not? So an exception
could still lead to double destruction of the same object.
 
> 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.

I think option 3 is best something like

template< class T >
void move_construct_destruct( T *destination, T *source )
{
  using namespace std; // so this will work in boost namespace

  construct( destination );
  move( *destination, *source ); //see my previous post for this
  destruct( source );
}

Then we can add move_construct_destruct overloads such as

void move_construct_destruct( int *destination, int *source )
{
  move( *destination, *source );
}

Hamish Mackenzie


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk