23.07.2012

Move aware classes in C++03 and C++11

Writing copyable classes in C++ can be tricky, especially if you try to write an optimized solution, but two idioms help to get it right and near-optimal at the first attempt:

  1. The rule of three [1].
  2. The pass-by-value-and-swap idiom [2].
Let's temporarily combine these two rules and create the rule of four (copy constructor, destructor, copy assignment, swap). As an example, let's take a look at ptr<T>:

template < class T >
T* clone_me( T const* p )
{ return p ? new T(*p) : 0; }

template < class T >
class ptr
{
public:

    explicit ptr( T* p = 0 ) : p_(p) {}

    // copy ctor (no. 1/4)
    ptr( ptr const& b ) : p_( clone_me(b.get()) ) {}
    
    // dtor (no. 2/4)
    ~ptr() { boost::checked_delete(p_); }

    // copy assignment (no. 3/4)
    ptr& operator=( ptr b ) { swap(*this,b); return *this; }
    
    // nothrow swap (no. 4/4)
    friend void swap( ptr& a, ptr& b ) { boost::swap(a.p_,b.p_); }

    T* get() const { return p_; }

    T* release() { T* r = p_; p_ = 0; return r; }

    // reset, operator* and ->

private:

    T* p_;
};

Note how operator= is implemented in terms of pass-by-value and swap. This implementation may not be the optimal version of assignment in some cases. In rare cases it might even be incorrect - for types like std::vector, which aren't expected to free reserved memory upon assignment. But assuming swap is a cheap operation, the above assignment should be close to optimal in many if not most cases. Furthermore, it is simple and provides the strong exception safety guarantee [2]. And in cases, where RVO kicks in, it may be even better than providing separate overloads of operator= for lvalues and rvalues [3]. Therefore, I believe this is the version of copy assignment one should begin with, and later optimize if needed.

Ok, so how to make ptr<T> move aware? The simple and close-to-optimal version can easily be written for both C++03 and C++11 using Boost.Move [4]. It turns out, all we actually need is a move-constructor, because our assignment, implemented in terms of pass-by-value and swap, shall work fine as move-assignment (although it may only be close to optimal).

This brings us to the rule of five [5], which is adding the move-constructor to the rule of four. All we need to add to our class is a move-constructor:

template < class T >
class ptr
{
public:

    explicit ptr( T* p = 0 ) : p_(p) {}

    // copy ctor (no. 1/5)
    ptr( ptr const& b ) : p_( clone_me(b.get()) ) {}

    // move ctor (no. 2/5)
    ptr( BOOST_RV_REF(ptr) b ) : p_( b.release() ) {}
    
    // dtor (no. 3/5)
    ~ptr() { boost::checked_delete(p_); }

    // copy- and move-assignment (no. 4/5)
    ptr& operator=( ptr b ) { swap(*this,b); return *this; }
    
    // nothrow swap (no. 5/5)
    friend void swap( ptr& a, ptr& b ) { boost::swap(a.p_,b.p_); }

    T* get() const { return p_; }

    T* release() { T* r = p_; p_ = 0; return r; }

    // reset, operator* and ->

private:
    BOOST_COPYABLE_AND_MOVABLE_ALT(ptr)

    T* p_;
};

Note, that for move-emulation to work in C++03, we also added a macro call BOOST_COPYABLE_AND_MOVABLE_ALT(ptr) [7] in the private section of our class.

Boost.Move docs by default suggest writing a separate copy-assignment operator, and a move-assignment operator [6], and for that to work in C++03, there needs to be a third overload of operator=, which is provided by BOOST_COPYABLE_AND_MOVABLE(ptr). However, since we implement operator= in terms of pass-by-value, we don't need that third overload (it would even create an ambiguity), so we use the macro with the _ALT suffix: BOOST_COPYABLE_AND_MOVABLE_ALT(ptr). (I wish that macro was simply called BOOST_MOVABLE, but that's a different story.)

This was easy. Let's add some conversions. In C++ pointers to derived classes are convertible to pointers to base classes. So our ptr<T> should work similarly. Note that we enable the conversion only for types, whose pointers are convertible, using SFINAE.

template < class T >
class ptr
{
public:

    explicit ptr( T* p = 0 ) : p_(p) {}

    // copy ctor (no. 1/5)
    ptr( ptr const& b ) : p_( clone_me(b.get()) ) {}

    // move ctor (no. 2/5)
    ptr( BOOST_RV_REF(ptr) b ) : p_( b.release() ) {}
    
    // generalized copy-/move-constructor implemented by pass-by-value and steal
    template < class U >
    ptr( ptr <U> b,
            typename boost::enable_if< boost::is_convertible<u*,t*> >::type* = 0 )
    : p_( b.release() ) {}

    // dtor (no. 3/5)
    ~ptr() { boost::checked_delete(p_); }

    // copy- and move- assignment (no. 4/5)
    ptr& operator=( ptr b ) { swap(*this,b); return *this; }
    
    // nothrow swap (no. 5/5)
    friend void swap( ptr& a, ptr& b ) { boost::swap(a.p_,b.p_); }

    T* get() const { return p_; }

    T* release() { T* r = p_; p_ = 0; return r; }

    // reset, operator* and ->

private:
    BOOST_COPYABLE_AND_MOVABLE_ALT(ptr)

    T* p_;
};
What about conversion-assignment? Don't worry, I didn't forget about it. The operator= provided will work for that too. If you are concerned about an extra temporary ptr<T> (which doesn't clone the pointee, just moves the pointer an extra time), you can provide an additional version assignment:

template < class U >
ptr& operator=( ptr<U> b/*, enable_if...*/ ) { reset( b.release() ); }
But why stop here? Go ahead, and provide separate overloads for rvalue references, and lvalue references. That is, replace our one operator= with 4 (or 6 for C++03), one of which is given by BOOST_COPYABLE_AND_MOVABLE(ptr):

ptr& operator=( BOOST_COPY_ASSIGN_REF(ptr) b ) { /*copy-and-swap or do better*/ }

ptr& operator=( BOOST_RV_REF(ptr) b ) { reset( b.release() ); /*swap gotcha [8]*/ }

template < class U >
ptr& operator=( BOOST_COPY_ASSIGN_REF(ptr<U>) b/*, enable_if...*/ )
{ reset( b.release() ); }

// warning: doesn't work as expected:
template < class U >
ptr& operator=( BOOST_RV_REF(ptr<U>) b/*, enable_if...*/ ) { reset( b.release() ); }

#if defined(BOOST_NO_RVALUE_REFERENCES)
ptr& operator=( ptr& b ) { ptr const& a = *this; return a = b; }

template < class U >
ptr& operator=( ptr<U>& b/*, enable_if...*/ ) { ptr const& a = *this; return a = b; }
#endif
And if you are willing to write this much, you'll probably want to write 2 conversion constructors, instead of one implemented by pass-by-value and steal. Stop right here, there's a gotcha.

Surprise: move emulation won't work in C++03 for template parameters of a template-function, because the parameters requiring a user-supplied conversion to rv<>& won't trigger the template instantiations to be considered in overload resolution. I'm not sure if the last sentence explains this correctly, but the fact is it won't work as expected in C++03, so in C++03 you're stuck with templated conversions by value.

I hope I showed you, that:

  1. Writing a move-aware type is fairly easy with the rule of five.
  2. Optimizing for rvalue references with Boost.Move has pitfalls, especially in C++03, so you may want to avoid it.

Acknowledgments: thanks to Ion Gaztañaga and Jeffrey Lee Hellrung, Jr. for insights in this discussion [9].

[1] Wikipedia: Rule of three (C++ programming)
[2] More C++ Idioms/Copy-and-swap
[3] Want Speed? Pass by Value.
[4] Boost.Move
[5] I believe I've read about a rule of five somewhere, but I can't find a reference now.
[6] Copyable and movable classes in portable syntax for both C++03 and C++0x compilers
[7] BOOST_COPYABLE_AND_MOVABLE_ALT() macro
[8] Again, I believe I'd read about this problem somewhere: if we just swap our state out of this object, its destruction may be delayed, which is unexpected, and in some cases (a lock for example) can be very bad.
[9] [move] case study: simple cloning smart pointer

Brak komentarzy: