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

28.04.2010

Zagadka: narciarze

Dwóch takich samych narciarzy zjeżdża po takim samym podłożu po krzywych 1 i 2 (rzut z boku). Różnica wysokości i przemieszczenie w poziomie są takie same. Narciarze zaczynają z zerową prędkością, nie odpychają się ani nie robią żadnych sprytnych ruchów przyspieszająco-hamujących. A jak ktoś nie lubi narciarzy, to może mieć dwie kulki staczające się po twardym podłożu. Opory pomijamy. Który pierwszy dojedzie do swojego punktu B i dlaczego?
Zagadka wyszła trochę bardziej skomplikowana, niż mi się na początku wydawało.
Odpowiedź krótka: drugi narciarz dojedzie wcześniej, bo wcześniej nabierze prędkości.
Odpowiedź szczegółowa:
Obierzmy na trasie narciarza punkt E w połowie przemieszczenia poziomego i wprowadźmy parametr p oznaczający stosunek różnicy wysokości AE do różnicy wysokości AB.
Fakty:
  • prędkość w punkcie E z z.z.E: vE=sqrt(2gph)
  • prędkość w punkcie B z z.z.E: vB=sqrt(2gh)
  • z drugiej strony znamy przyspieszenie na odcinku AE, więc możemy wprowadzić czas jazdy tAE na odcinku AE: vE=gph/a*tAE
  • analogicznie na odcinku EB: vB=vE+g(1-p)h/b*tEB
  • t=tAE+tEB
  • a=sqrt(hhpp+xx), b=sqrt(hh(1-p)(1-p)+xx)
Z powyższych wzorów można wyprowadzić wzór na czas w zależności od p: t(p), policzyć pochodną dt/dp i pokazać, że dla p=1/2 (przypadek gdy narciarz zjeżdża po prostej) pochodna jest ujemna, więc t(p) jest malejąca, więc t zmaleje gdy p wzrośnie, więc opłaca się obniżyć punkt E.
A skoro tak, to dalej opłaca się odcinki AE i EB dzielić w połowie przemieszczenia poziomego punktem F i obniżać punkt F i tak z indukcji matematycznej wklęsła trasa się opłaca bardziej niż prosta, mimo nadrobienia drogi.
A jak bardzo wklęsła się opłaca najbardziej? - tego na razie w prosty sposób nie umiem pokazać, bo dt/dp wyszło za skomplikowane, żeby znaleźć miejsce zerowe ;-) Jeśli ktoś ma pomysł, to śmiało ;-)
W miarę łatwo można pokazać, że t(1/2)>t(1) dla h<5x. W zwykłych trasach h<2x, a najczęściej h<x/2, więc intuicja mówi mi, że niekiedy może się opłacać punkt E obniżyć nawet poniżej punktu B!
Michał: brachistochroma to krzywa, po której czas staczania się masy punktowej od punktu A do punktu B pod wpływem stałej siły (siły ciężkości) jest najkrótszy.

3.12.2009

Re: 6.1.08 wystawa płaska okiem wystawiającego

tak mi się przypomniał stary post na tym blogu, to ci zaśmiecę nowym rysunkiem z tej samej kategorii.

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 ;-)

23.05.2009

Polski komentator sportowy

Tenis.
Polski komentator: Ale wspaniaała wymiaana, widzieeli paaństwo, już serw Rumunki dał jej nieznaczną przewagę, a później te zmiaany rytmu gry i wreszcie to wykończeenie, z forhendowego volley'a, które nie było przecież łatwe, gdyż Bondarenko zagrała jej piłkę pod nogi, ale Rumunka poradziła sobie doskonaale.
Tymczasem niemiecki komentator: Klasse gespielt.
Siatkówka. Mecz Polska-Włochy.
Polski komentator: ... [2 akapity, kilka wykrzykników, kto ma ochotę polać trochę wody?]
Tymczasem niemiecki komentator: Die Italiener haben nur ein Problem - und das heisst Dawid Murek. [Dzięki Piter za przypomnienie ;-)]

20.05.2008

Przegraliśmy pietruszkę

Tradycyjnie nasi środkowi pozawodzili i na meczu ze środkowych był tylko Wygos. Jednak skoro stawka spotkania nie była duża, więc zastosowaliśmy najzabawniejszy możliwy wariant i w ten sposób pierwszy raz w tym sezonie zagrałem na środku.
Bawiłem się świetnie, pamięć krótkotrwałą uszkodziłem sobie na popijawie po meczu, a od samego meczu parę dni minęło, więc większość szczegółów mi umknęła, dlatego nie będę się rozpisywał o przebiegu meczu. Wygraliśmy pierwszego seta, schrzaniliśmy początek drugiego (z 1-7 ciężko się goni), przegraliśmy trzeciego, wygraliśmy czwartego po świetnej końcówce, kiedy przegrywaliśmy 19-21 i marzyłem tylko o tym, żeby nikt nie zagrał w moją stronę. Jakim cudem nie zrobiłem w końcówce żadnego błędu nie wiem, piłka odbijała się we właściwą stronę widocznie. Przy 23-23 Czajnik poszedł na zagrywkę i przeleciało mi przez głowę, że ta zagrywka zdecyduje o wszystkim. Czajnik trafił asa, a w następnej piłce zatrzymałem atak na prawym skrzydle i powtórkę na lewym (choć powtórka była już w taśmę).
Wyglądało na to, że nasza taktyka na tie-break działa. Piter zmienił Wygosa w drugiej linii, weszła mu zagrywka i strony zmienialiśmy prowadząc 8-3. Wystarczyło dowieźć przewagę do końca i wszystko wskazywało na to, że się uda. było już 13-9 kiedy na zagrywkę wszedł niejaki QBA i urządził rzeź niewiniątek. Lolek 2 razy wpuścił piłkę w pole a kolejną przebił na drugą stronę, Wygos zamiast skończyć przechodzącą lub zostawić ją Czajnikowi, podbił ją możliwie daleko od Winiego, a jedyne dobre przyjęcie skończyło się widowiskowym atakiem Wygosa w siatkę. Czajnik dostał bloka, a Szyba zdjął mi piłkę z rąk i nie dałem rady jej podbić, więc przy meczowej ja zdjąłem piłkę z rąk Szybie i wygranego tie-breaka przegraliśmy...
Pocieszająca w naszej grze była bardzo duża ilość podbitych piłek w obronie, zmartwić za to musiało kiepskie przyjęcie - z pewnością zbyt kiepskie jak na potrzeby Winiego.
Oceny (dziś w duchu po reformie oświaty ocena opisowa):
  • Wini - poprawnie jak na rekonwalescenta. Szybkość mu się nie poprawiła, nie zawsze był dokładny, ale i tak zagrał lepiej niż jego nieudolny zastępca w dwóch ostatnich meczach. W tie-breaku nie przyznał się do bloku przy bardzo ważnej piłce - skutecznie.
  • Czajnik - świetnie. 3/4 piłek kierowane było do Czajnika, a to, ile asów Czajnik zdobędzie w serii przesądzało o zwycięstwie lub przegranej. On sam ma do siebie pewne pretensje za nieudany skrót, czapę i coś tam jeszcze, ale to nie zmienia faktu, ze gdybyśmy wygrali tie-breaka, to on byłby bezsprzecznie graczem meczu.
  • Szyba - chyba dobrze. Trudno mi powiedzieć, bo nie do mnie trafiały piłki po przyjęciu a ataki i tak szły do Czajnika, więc trudno mi podać odpowiednie przykłady. W pamięci utkwił mi jedynie śliczny atak z mojej wystawy i 2 obrony piłek, które powinienem wziąć a mnie tam nie było.
  • Lolek - nierówno. Przez cały mecz narzekał, że nie dostaje piłek i, jak to zwykle on, brylował w zabawnych choć nie trafionych usprawiedliwieniach. Wytłumaczeniem czemu nie odebrał dwóch asów było, że nie chciał, by Wini biegał... Przez cztery sety miał za mało okazji, by się wykazać, a w piątym został wybrany na jelenia i potulnie się z tej roli wywiązał.
  • Piter - całkiem dobrze - wzmocnił 2 razy przyjęcie (nie tak bardzo jak byśmy chcieli ale zawsze), zawalczył na zagrywce, parę razy zagrał ładnie w obronie.
  • Wygos - tak sobie. Coś tam robił na siatce, coś skończył, przyjmował nieźle. Ale 2 (de facto) zepsutych w końcówce piłek mu nie mogę darować.
  • Gorzki - trudno powiedzieć. Z jednej strony 4 bloki punktowe, 3 podbite i 4 wybloki, 1 as, parę fajnych obron spod samej siatki i świetne przyjęcie (chyba 2 trudne dla Winiego, jedna przebita do przeciwników od razu a reszta dobrych). Z drugiej strony 2 piłki w ataku oddane gratis, 1 przebita i 1 stracona z sytuacyjnych, i 3 przegrane piłki na siatce. Do tego obrony były wynikiem tego, że czatowałem na nie pod siatką, a dalekie piłki wybierał za mnie Piter z Szybą.

11.05.2008

Ponoć najgorszy nasz mecz w tym roku

Niby wygraliśmy, ale jakoś mało radości sprawiła ta wygrana.
Pierwszy set zaczęliśmy tragicznie, z sześcioma punktami w plecy przy stanie 12-18. Mimo wszystko udało nam się wyrównać na 23-23, ale niestety straciliśmy potem 2 punkty z rzędu i całego seta 23-25.
Cały drugi set siedzieliśmy przeciwnikom na piętach aż do końcówki (19-20) kiedy ze zdenerwowania zapomniałem, że w pierwszej linii mam Czajnika i Pitera. Z konieczności więc 4 piłki z rzędu dostał Wygos (i wszystkie skończył). Dopiero przy setowej uświadomiłem sobie, że to Czajnik jest naszym głównym atakującym, więc rzuciłem piłkę na prawe skrzydło.... do Pitera. ;P
Na początku trzeciego seta uznałem, że jak Piter skończył, to może i zacząć i zaczął dobrze. Wyglądało na to, że zmiażdżymy przeciwników aż... cóż, obiecałem nie narzekać na przyjęcie, ale dwie piłki, do których biegłem z lewej bocznej linii po to, żeby je podbić zza prawej linii, były symptomatyczne dla tego krótkiego okresu czasu, kiedy prowadzenie 7-1 zamieniliśmy na 7-9. Tym razem jednak Czajnik nie chciał czekać na to kogo wybiorę na bohatera końcówki i tradycyjnie dla siebie w okolicach piętnastego punktu dołożył serię zagrywek aż przeciwnicy ratowali się czasem... dając Czajnikowi tak przydatną chwilę odpoczynku. Dzięki temu trzeci set padł naszym łupem.
Czwarty set miał się obyć bez historii i spokojnie prowadziliśmy zdobywając punkt po punkcie... Niestety w końcówce dziewczyny (i jeden inwalida) uparli się by zapewnić sędziemu emocjonującą końcówkę. Przeciwnicy doszli nas na 23-23 i dopiero wtedy przestaliśmy się wygłupiać. W końcu udało nam się zakończyć ten mecz wygrywając czwartego seta 27-25.
Oceny:
  • Gorzki może i nastąpiła znacząca poprawa w porównaniu z poprzednim występem, ale oznacza to jedynie tyle, że popełniałem mniej błedów, bo nie dostawałem tyle piłek co do tej pory i tyle. Chociaż jeden pozytyw - wreszcie trafiałem jako tako do Wygosa. 3+
  • Czajnik co dostał to (poza dwoma przypadkami) skończył, zaliczył ładne serie zagrywek. Co najważniejsze - zwrzeszczał wszystkich naprawdę skutecznie, bez czego pewnie dostalibyśmy widowiskowo w tyłek. 4+
  • Piter zagrał fajnie w ataku, ale kiepsko w przyjęciu, a zagrywka mu raczej nie siedziała. 3+
  • Szyba w odbiorze chyba podobnie do Pitera, ale radził sobie z moimi szybkimi piłkami gdy miał dostać normalną, z wysokimi za antenkę gdy miał dostać szybką i zaliczył 2 fajne bloki. 4-
  • Paweł widać że jeszcze trochę się obawia o swoją nogę po kontuzji, a i do pełni formy nie wrócił. Z Pawłem niestety problem jest taki, że trzeba umieć z nim grać, więc nie miał okazji by się popisać. 3+
  • Wygos skuteczność miał naprawdę niezłą, a co najważniejsze - radził sobie nawet kiedy dostał piłkę ponad metr za plecy i na wysokość swojej głowy. W bloku musi się jeszcze nauczyć skakać przodem do siatki a nie prawym bokiem. 4+
  • Rafał Ponoć zagrał paskudnie w odbiorze (co w wypadku libero chyba jest ważne), ale z drugiej strony parę razy fajnie coś wyciągnął... powiedzmy 3