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:
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; } #endifAnd 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:
- Writing a move-aware type is fairly easy with the rule of five.
- 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:
Prześlij komentarz