arXiv:1309.1307v1 [cs.PL] 5 Sep 2013

C++11 – idea r-wartości i przenoszenia
Piotr Beling
Uniwersytet Łódzki, Wydział Matematyki i Informatyki
6 września 2013
Streszczenie
Niniejszy artykuł jest jednym z serii artykułów w których zawarto
przegląd nowych elementów języka C++ wprowadzonych przez standard
ISO/IEC 14882:2011, znany pod nazwą C++11.
W artykule przedstawiono nowe możliwości związane z przekazywaniem parametrów i pisaniem konstruktorów. Zawarto w nim dokładne
omówienie idei r-wartości i przenoszenia obiektów.

Spis treści
1 Wstęp

2

2 R-wartości

2

3 Kopiujące i przenoszące konstruktory i operatory przypisania

5

4 Konstruktor przenoszący a zarządzenia zasobami

6

5 Użyj C++11 a Twój program sam przyspieszy

7

1

1

Wstęp

Standard języka C++, znany jako C++11 (a także C++0x), został opublikowany we wrześniu 2011 roku, w dokumencie ISO/IEC 14882:20111.
W stosunku do poprzedniego standardu (C++03), wprowadza on dużą liczbą
nowych elementów, zarówno do samego języka, jak i do jego standardowych
bibliotek.
W większość popularnych kompilatorów dość szybko zaimplementowano prawie wszystkie nowe elementy wprowadzone w C++11.
Niniejszy artykuł, jeden z serii, stanowi krótki i bogato zilustrowany przykładami przegląd nowych konstrukcji które wzbogaciły język.
Szczególną uwagę przywiązano w nim do możliwość związanych z przekazywaniem parametrów i pisaniem konstruktorów. Szczególnie dokładne omówiona
jest idea r-wartości i przenoszenia wartości obiektów.

2

R-wartości

Terminem r-wartość (ang. r-value) określa się wartości mogące wystąpić jedynie
po prawej stronie operatora przypisania, takie jak obiekty tymczasowe. Można
je też postrzegać jako zmienne które nie będą dalej używane, aż do momentu
ich skasowania, np. we fragmencie programu:
z . s e t V a l u e ( Obj ( ) ) ;
obiekt tymczasowy Obj jest r-wartością. Zostanie on skasowany w miejscu wystąpienia średnika na końcu linii i w związku z tym nie będzie używany po powrocie z metody setValue. Proszę zauważyć, że ostatnia informacja może być bardzo
cenna dla możliwości efektywnego zaimplementowania tej metody (tymczasem
w C++03 nie możliwości jej wygodnego przekazania). Będzie tak na przykład
jeśli zadaniem metody setValue jest ustawienie wartości pola value (typu Obj)
obiektu na wartość przekazaną jako argument metody setValue i równocześnie
typ Obj jest właścicielem danych zaalokowanych dynamicznie, powiedzmy:
c l a s s Obj {
i nt ∗ da ta ; // dane
i nt s i z e ;
// rozmiar danych
public :
Obj ( const Obj& s r c ) : da ta (new i nt [ s r c . s i z e ] ) , s i z e ( s r c . s i z e ) {
// k o p i o w a n i e danych :
s t d : : copy ( s r c . data , s r c . da ta+s i z e , da ta ) ;
}
Obj& operator=(const Obj& s r c ) {
Obj cpy = s r c ;
this −>swap ( cpy ) ;
1 Dokument ten dostępny jest odpłatnie, jednakże ze strony http://www.open-std.org
można pobrać jego darmową wersję roboczą [5]. Najnowsza (N3337) datowana jest na 16
stycznia 2012.

2

return ∗ t h i s ;
}
˜Obj ( ) { delete da ta ; }
void swap ( Obj& o ) {
s t d : : swap ( data , o . da ta ) ;
s t d : : swap ( s i z e , o . s i z e ) ;
}
// . . . in n e k o n s t r u k t o r y i metody
};
Proszę zwrócić uwagę, że powyższy kod (szczególnie operator przypisania) jest
odporny zarówno na rzucanie wyjątków przez new, tj. nie zmienia w takim
wypadku stanu obiektu docelowego, jak i na pseudo-przypisania (typu x=x).
Więcej informacji można znaleźć w [1].
W C++03 metoda setValue można napisać na jeden z snastępujących sposobów:
void s e t V a l u e ( const Obj& o ) { // 1
this −>v a l u e = o ;
}
void s e t V a l u e ( Obj& o ) {
// 2
this −>v a l u e = o ;
}
void s e t V a l u e ( Obj o ) {
// 3
this −>v a l u e . swap ( o ) ;
} // tu , d e s t r u k t o r o b i e k t u o s k a s u j e s t a r e dane t h i s
Implementacja (1) nie wie czy argument jest obiektem tymczasowym, więc
na wszelki wypadek nie może go zmienić. Jest ona jednak optymalna dla obiektów nietymczasowych.
Wersja (2) nie wnosi nic pozytywnego w stosunku do (1), a dodatkowo nie
można jej wywołać na rzecz obiektów stałych i tymczasowych (z.setValue(Obj())
nie skompiluje się).
Implementacja (3) w C++03 może wykonać kopiowanie już przy przekazywaniu parametru do funkcji, potem zaś dodatkowo wykona metodę swap. Pozornie jest ona zatem mniej efektywna niż (1). Jednakże, dla argumentu będącego
r-wartością, kompilator może wygenerować kod nie wykonujący wspomnianego
kopiowania2, zaś nadmiarowa operacja swap jest dość „tania” w stosunku do kopiowania.
W C++11 można przekazać argument przez referencję do r-wartości:
void s e t V a l u e ( Obj&& o ) {
// 4
this −>v a l u e . swap ( o ) ;
} // d e s t r u k t o r o b i e k t u o s k a s u j e s t a r e dane t h i s
2 Może to zrobić nawet gdy konstruktor kopiujący Obj powoduje efekty uboczne. Dlatego
konstruktory kopiujące nigdy nie powinny ich powodować!

3

Proszę zwrócić uwagę, że ta metoda zmienia stan obiektu o, ale ponieważ jest
on przekazany przez referencję do r-wartości, to mamy pewność3 , że, po powrocie, nie będzie on używany aż do momentu skasowania.
Dobrym rozwiązaniem jest umiejscowienie logiki przenoszącej obiekty klasy
Obj w przenoszących konstruktorze i operatorze przypisania tej klasy:
Obj ( Obj&& o ) : da ta ( s r c . da ta ) , s i z e ( s r c . s i z e ) {
o . da ta = n u l l p t r ; // d e s t r u k t o r o b i e k t u o n i e s k a s u j e danych
}
Obj& operator=(Obj&& o ) {
this −>swap ( o ) ;
return ∗ t h i s ;
} // d e s t r u k t o r o b i e k t u o s k a s u j e s t a r e dane t h i s
Przenoszącą metodę setValue można teraz zaimplementować czytelniej:
void s e t V a l u e ( Obj&& o ) {
// 5
this −>v a l u e = s t d : : move ( o ) ;
// p i s z ą c s t d : : move obiecu jemy ,
// ż e n i e b ę dziemy pó ź n i e j , t j . w tym miejscu ,
// u ż ywa ć o b i e k t u o
}
Użycie std :: move jest wymagane, inaczej zostałby wywołany kopiujący operator przypisania zamiast przenoszącego. Proszę zauważyć, że wewnątrz metody
setValue obiekt o żyje aż do jej końca i w związku z tym zachowuje się jak typ
Obj&. Potencjalnie, o mógłby być używany po przypisaniu, dlatego trzeba jawnie go rzutować na referencję do r-wartości (to właśnie robi std :: move) i tym
samym pozwolić na zniszczenie jego stanu.
Ostatecznie, optymalnie jest wyposażyć naszą klasę w dwie metody setValue:
(1) i (5). Alternatywnie, można użyć jedynie wersji (3), która w przypadku argumentu będącego obiektem tymczasowym, wykona przenoszenie i operację swap,
czyli także nie spowoduje kosztownego i niepotrzebnego kopiowania. Przekazywanie argumentów przez wartość będzie miało jeszcze większy sens, gdy tych
argumentów będzie więcej, np.
void s e t V a l u e s ( Obj o1 , Obj o2 ) {
this −>v a l u e 1 . swap ( o1 ) ;
this −>v a l u e 2 . swap ( o2 ) ;
}
jest dobrą alternatywą w stosunku do napisania aż 4 przeciążeń o sygnaturach:
void setValues(const Obj& o1, const Obj& o2), void setValues(Obj&& o1, Obj&& o2),
void setValues(const Obj& o1, Obj&& o2), void setValues(Obj&& o1, const Obj& o2).
Ogólnie, dla n argumentów, wystarczy napisać 1 metodę zamiast 2n .
3 konkretniej: wywołujący składa obietnicę że tak jest, ale jeśli jej nie dotrzyma to jego
problem

4

3

Kopiujące i przenoszące konstruktory i operatory przypisania

Dzięki napisaniu kompletu konstruktorów i operatorów przypisania w klasie
Obj, podobny komplet zostanie wygenerowany automatycznie dla takiej klasy:
struct ObjHolder {
Obj o b j ;
};
Domyślne kopiujące i przenoszące konstruktory i operatory przypisania tej klasy
po prostu odpowiednio kopiują lub przenoszą obiekty pole po polu4 (w przypadku dziedziczenia, wpierw wykonywany jest odpowiedni konstruktor lub operator z nad-klasy, a potem są kopiowane lub przenoszone nowo dodane pola).
Gdybyśmy chcieli zaimplementować je jawnie, wyglądało by one następująco:
struct ObjHolder {
Obj o b j ;
// K o n s t r u k t o r y :
ObjHolder ( ) = default ;
ObjHolder ( const ObjHolder& h )
: o b j ( h . o b j ) {}
ObjHolder ( ObjHolder&& h )
: o b j ( s t d : : move( h . o b j ) ) {}

//domy ś l n y
// k o p i u j ą cy
// p r z e n o s z ą cy

// o p e r a t o r y p r z y p i s a n i a :
ObjHolder& operator=(const ObjHolder& h ) { // k o p i u j ą cy
this −>o b j = h . o b j ;
return ∗ t h i s ;
}
ObjHolder& operator=(ObjHolder&& h ) {
// p r z e n o s z ą cy
this −>o b j = s t d : : move ( h . o b j ) ;
return ∗ t h i s ;
}
};
Proszę też zwrócić uwagę, na wprowadzoną w C++11 składnię umożliwiającą
przywrócenie konstruktora (w tym przypadku domyślnego). W analogiczny sposób, jednak za pomocą słowa kluczowego delete, można skasować wygenerowany
konstruktor lub metodę.
4 To wymaga oczywiście, by wszystkie pola miały odpowiedni konstruktor lub operator
przypisania. Gdyby np. Obj nie miał konstruktora kopiującego, to nie zostałby on także wygenerowany dla ObjHolder (wciąż mogli byśmy jednak taki konstruktor w ObjHolder napisać).

5

4

Konstruktor przenoszący a zarządzenia zasobami

Czasami zasadne jest by klasa posiadała możliwość przenoszenia (przenoszący
konstruktor i operator przypisania) równocześnie nie umożliwiając kopiowania
(np. z jawnie skasowanymi przenoszącym konstruktorem i operatorem przypisania). Taka potrzeba zachodzić najczęściej gdy obiekty takiej klasy są odpowiedzialne lub posiadają jakiś zasób który zamykają lub zwalniają w destruktorze5 . Ponieważ ważne jest by nie nastąpiło więcej niż jedno wywołanie destruktora zwalniającego zasób, to konstruktor kopiujący jest niedostępny. Zaś
konstruktor przenoszący, po prostu przenosi odpowiedzialność za zasób na nowy
obiekt. Przykładowo, klasa std :: fstream z biblioteki standardowej, reprezentująca strumień umożliwiający dostęp do pliku, posiada zasób w postaci otwartego
deskryptora pliku (który jest zamykany przez destruktor). Dlatego std :: fstream
nie posiada konstruktora kopiującego, co w C++03 uniemożliwiło np. zwrócenie
obiektu tej klasy jako wyniku funkcji, np.:
std : : fstream o p e n c o n f i g f i l e () {
s t d : : f s t r e a m r e s u l t ( ”myapp . c f g ” ) ;
// . . .
return r e s u l t ; // a l b o : r e t u r n s t d : : move ( r e s u l t ) ;
} // n iepoprawn e w C++03, poprawne w C++11
Powyższa funkcja jest poprawna w C++11. Plik jest otwierany i zarządza nim
obiekt lokalny result . Następnie konstruktor przenoszący jest używany by zwrócić obiekt i przenieść odpowiedzialność za otwarty deskryptor pliku na zewnątrz
funkcji.
Uwaga: ponieważ result jest obiektem lokalnym funkcji, to w linii z return
staje się r-wartością (za chwile będzie skasowany), nawet gdy nie użyje się
std :: move by jawnie rzutować na r-wartość.
W STL-u istnieją dwa szablony (inteligentne wskaźniki) które ułatwiają zarządzanie zasobami (będących najczęściej w postaci wskaźników):
• std :: unique ptr — analogicznie do std :: fstream, zwalnia zasób (typowo:
kasuje wskazywany obiekt) w destruktorze i jest niekopiowalny;
• std :: shared ptr — zlicza6 liczbę referencji do wskazywanego obiektu i kasuje go gdy licznik wskaże zero. W ten sposób jeden obiekt jest współdzielony przez kilka wskaźników, zaś operacja ich kopiowania jest dobrze
zdeﬁniowana.
5 Zgodnie ze wzorcem RAII (Resource Acquisition Is Initialization) łączącym przejęcie
i zwolnienie zasobu z inicjowaniem i usuwaniem zmiennych.
6 np. w konstruktorze kopiującym zwiększa wartość licznika referencji o 1, zaś w destruktorze ją o 1 zmniejsza

6

5

Użyj C++11 a Twój program sam przyspieszy

STL intensywnie korzysta z nowych możliwości jakie daje C++11 w używaniu r-wartości. Większość szablonów klas (np. kontenerów) nie tylko posiada
konstruktor przenoszący, ale także używa przenoszenia zamiast kopiowania tam
gdzie to tylko możliwe. Przykładowo std :: vector przenosi (zamiast, jak było to
w C++03, kopiować) przechowywane obiekty w trakcie, dokonywanej od czasu
do czasu, realokacji, która jest wymagana by dostosować ilość używanej pamięci
do liczby przechowywanych obiektów.
Napisany w C++03 program, często, by działał szybciej, wystarczy jedynie
skompilować za pomocą C++11.

Literatura
[1] C++ faq lite. http://klub.chip.pl/b.krzemien/c++-faq-pl/index.html.
[dostęp: 2013-04-27]. 2
[2] C++ reference. http://en.cppreference.com. [dostęp: 2013-04-27].
[3] Wikipedia,
the
free
encyclopedia:
http://en.wikipedia.org/wiki/C++11. [dostęp: 2013-04-27].

C++11.

[4] Wikipedia,
wolna
encyklopedia:
http://pl.wikipedia.org/wiki/C++11. [dostęp: 2013-04-27].

C++11.

[5] ISO. ISO/IEC N3337=12-0027 Working Draft, Standard for Programming
Language C++. International Organization for Standardization, 2012. 1

7

