Pokazywanie postów oznaczonych etykietą Cpp03. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Cpp03. Pokaż wszystkie posty

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

22.07.2009

Ocena znajomości C++

Mały cytacik z comp.lang.c++.moderated:
On a scale of 1 to 10, how would you rate your C++ skill level?
  • 10 - wrote a C++ compiler fully supporting C++0x last night after dinner (on my iPhone)
  • 9 - wrote a book with leading-edge techniques in it
  • 8 - published a used library with leading-edge techniques (or wrote a generic tutorial with standard techniques in it)
  • 7 - have invented leading-edge techniques & blogged about them
  • 6 - have architected entire successful C++ applications
  • 5 - have unit-tested, maintained, and debugged C++ apps
  • 4 - know how to factually avoid undefined behavior
  • 3 - obey a sane subset so narrow most of my behavior is defined!
  • 2 - know how to debug C++ until it works
  • 1 - am allowed to look at C++ written by other teammates
  • 0 - I post "delete NULL" questions to the newsgroups!
I jedna z odpowiedzi (umieszczona tuż po opisie do punktu 10):
Damn it! "Dinner..." I knew I forgot something...
Anyway, ja się dzielnie wspinam z 5 na 6 jak na razie ;-)

7.05.2008

Specyfikacje throw(...) i bezpieczeństwo wyjątków

Czy pisać specyfikacje throw()? To pytanie zadawało sobie wielu ekspertów. Oprę się na wnioskach Herba Suttera (stąd):
  • Moral #1: Never write an exception specification.
  • Moral #2: Except possibly an empty one, but if I were you I’d avoid even that.
Czyli nie piszemy specyfikacji throw()... No to w takim razie olewać co funkcje rzucają? Lepiej nie. Sensowne wydaje się pisanie tego w komentarzu.
Warto zawrzeć informacje:
  • jeśli to funkcja wirtualna, to warto napisać, jakiej klasy funkcję zastępuje /** @note @e overrides Klasa */
  • jeśli funkcja nie jest wirtualna i przesłania inną, również warto to napisać /** @note @e hide Klasa */
  • co funkcja może rzucać /** @throws co kiedy */
  • jak się zachowa w razie wyjątku, czyli bezpieczeństwo wyjątków /** @note @e safety --- */ (o tych --- za chwilę)
Ponownie powołam się na Herba Suttera, który fajnie opisał tę sprawę.
Opis bezpieczeństwa w razie wyjątku składa się z 3 części:
  • Atomicity - niepodzielność. 3 możliwości: 1) [-] brak, 2) [a] niepodzielność - czyli całość albo wcale, 3) [A] na pewno całość - czyli gwarancja powodzenia operacji.
  • Consistency - spójność. 3 możliwości: 1) [-] brak, 2) [c] w razie niepowodzenia system pozostaje spójny, 3) [C] niezależnie od powodzenia lub nie, system pozostaje spójny. W kontekście programowania, ma sens rozpatrywanie tylko możliwości 3) [C] - każda funkcja musi po sobie pozostawiać program w stanie spójnym, niezależnie od tego czy wystąpił błąd, czy nie.
  • Isolation - brak skutków ubocznych. 3 możliwości: 1) [-] skutki uboczne występują niezależnie od powodzenia operacji, 2) [i] skutki uboczne występują tylko w przypadku powodzenia operacji, 3) [I] całkowity brak skutków ubocznych
Sutter pisze jeszcze o Durability - jak to z modelu baz danych - ale my się tym nie będziemy zajmować. Rzadko w programowaniu będziemy pilnować, czy wynik działania operacji przetrwa w razie jakichś kataklizmów typu upadek systemu...
Oznaczenia: Każdą z wymienionych powyżej 3 części opisu bezpieczeństwa będziemy oznaczać zależnie od poziomu bezpieczeństwa przy pomocy 3 znaków, z których każdy będzie odpowiadał jednej z części.
Przykłady:
  • /** @note @e safety AC- */ oznacza, że operacja zawsze się powiedzie (ekwiwalent throw()); może emitować skutki uboczne.
  • /** @note @e safety -CI */ oznacza, że w razie niepowodzenia, obiekty pozostaną w stanie spójnym[C], ale nie wiadomo jakim; niezależnie od powodzenia, nie wystąpią żadne skutki uboczne.
  • /** @note @e safety aCi */ oznacza silną gwarancję Abrahama, czyli w razie niepowodzenia system będzie w takim stanie, jakby wywołanie funkcji nie nastąpiło; dotyczy to także skutków ubocznych.