Documente Academic
Documente Profesional
Documente Cultură
Cap. 13 – C++ 11
Obiective:
Expresii lambda
Pointerul nullptr
Sintaxa de iniţializare uniformă
Delegarea constructorilor
Referinţe rvalue
Mecanismul move semantics
Constructorul de tip move
Operatorul de asignare de tip move
1
Mihai TOGAN
Introducere C++11
Versiunile clasice de standarde pentru limbajul C++ au apărut în 1998 (C++98) şi apoi în
2003 (C++03).
Standardul C++11 a apărut în Septembrie 2011, fiind o variantă nouă, major înbunătăţită
faţă de C++03.
Modificările propuse de C++11 sunt atât la nivel de biblioteci dar şi la nivelul limbajului
de bază prin introducerea unor capabilităţi în direcţiile următoare:
Suport pentru multithreading
Suport pentru programare generică
Performanţă
De altfel, Bjarne Stroustrup, creatorul limbajului C++, afirma la un moment dat faptul că
„C++11 feels like a new language”.
2
Mihai TOGAN
Expresii lambda
Expresiile lambda sunt văzute de compilator ca nişte funcţii anonime, fără nume (un-
named functions).
Exemplul 1: Afisarea elementelor din cadrul unei liste std::list<int> folosind o funcţie de
afişare obişnuită (print_it).
#include <iostream>
#include <algorithm>
#include <list>
void main ()
{
list<int> int_list;
int_list.push_back(2);
int_list.push_back(1);
int_list.push_back(5);
int_list.push_back(7);;
Observaţii:
În exemplul de mai sus, a fost folosită construcţia for_each (facilitate C++03).
Acestă construcţie acţionează exact ca un for care iterează toate elementele unei colecţii
aflate în intervalul [first, last] şi aplică funcţia f pentru fiecare element din colecţia
definită de intervalul [first, last].
3
Mihai TOGAN
O implementare posibilă pentru for_each:
Modificăm exemplul de mai sus pentru a folosi o expresie lambda în locul funcţiei print_it.
Exemplul 2: Afisarea elementelor din cadrul unei liste std::list<int> folosind o expresie
lambda.
#include <iostream>
#include <algorithm>
#include <list>
void main ()
{
list<int> int_list;
int_list.push_back(2);
int_list.push_back(1);
int_list.push_back(5);
int_list.push_back(7);;
// aplicam expresia lambda definita in-place, pentru fiecare intreg din lista
for_each(int_list.begin(), int_list.end(), [](int i) {cout << ":" << i << ":"; });
Codul din exemplul 1 este foarte simplu (şi destul de clar). Problema este însă aceea că a fost
generată o funcţie (print_it) care este necesară numai şi numai în acel segment de cod din
cadrul lui for_each.
În general, este o alegere proastă aceea de a genera o funcţie doar pentru un caz special. De
exemplu, funcţia print_it, deşi este folosită numai pentru afişare în for_each, acum are însă
un status general echivalent cu toate celelalte funcţii din cadrul modulului software.
4
Mihai TOGAN
Expresiile lambda oferă posibilitatea de a genera funcţii nenominale (un-named) exact la
locul şi pentru cazul în care se vor apela.
În exemplul de mai sus, a fost posibilă scrierea şi apelul unei funcţii locale (există numai în
contextul lui for_each).
O expresia lambda poate fi definită şi salvată într-o variabilă care poate apoi să fie folosită
pentru a executa expresia lambda:
void main()
{
auto func1 = [](int i) {cout << ":" << i << ":";};
func1(42);
}
Observaţie: în exemplul de mai sus am folosit tipul auto (facilitate C++11, explicată într-o
secţiune mai jos).
void main()
{
if([](int i, int j){return 2*i == j;}(12, 24))
cout << "It's true!" << endl;
else
cout << "It's false!" << endl;
cout << " This lambda makes type conversion and returns " <<
[](double x, double y) -> int {return x + y;} (3.14, 2.7) << endl;
}
Expresiile lambda pot „captura” contextul (variabile locale sau variabile membre la
nivelul unei clase).
void main()
{
int int_var = 42;
double dbl_var = 3.14;
[int_var, dbl_var] ()
{
int i = 7;
cout << int_var << ' ' << dbl_var << ' ' << i << endl;
} ();
}
Contextul capturat poate fi modificat. Trebuie însă folosită referinţa la definirea expresiei
lambda (ca în exemplul de mai jos).
Exemplul 4: capturarea şi modificarea unei variabile locale în cadrul unei expresii lambda.
#include <iostream>
#include <algorithm>
int main()
{
char s[]="Hello World!";
int Uppercase = 0; // va fi modificat de lambda
cout << Uppercase << " uppercase letters in: " << s <<endl;
}
Observaţii:
Folosirea referinţei în exemplul de mai sus implică faptul că body-ul expresiei lambda
primeşte o referinţă la o variabilă din contextul funcţiei în care este definită şi folosită
expresia lambda.
6
Mihai TOGAN
Fară a utiliza referinţa (&Uppercase), transmiterea se face prin valoare (nu va fi
modificat).
În exemplul mai sus, compilatorul ar genera în acest caz o eroare de compilare la linia
Uppercase++;
Observaţie:
Instrucţiunea for a fost există în C++11 pentru a permite folosirea într-un mod mai
simplu pentru iterarea unei mulţimi de elemente:
#include <iostream>
void main()
{
int my_array[5] = {1, 2, 3, 4, 5};
7
Mihai TOGAN
Deducerea automată a tipului de date (Automatic Type Deduction) şi decltype.
În C++03, fiecare variabilă din program trebuie definită menţionând pentru ea un tip de
date clar precizat.
În C++11, tipul variabilelor nu mai trebuie specificat obligatoriu dacă la declaraţie există
şi o iniţializare a variabilei. În loc de a menţiona tipul variabilei, se va folosi cuvântul
cheie auto.
auto x = some_expression;
Observaţie: sensul cuvântului cheie auto a fost total schimbat faţă de cel moştenit din
limbajul C. Explicaţie.
În acest caz, compilatorul poate deduce tipul de date din tipul de date al expresiei de
iniţializare.
void main()
{
auto x = 0; // x este de tipul int deoarece 0 este o constanta int
auto c = 'a'; // char
auto d = 0.5; // double
auto v = 14400000000000LL; // long long
}
Operatorul decltype poate fi folosit pentru a „prelua” tipul unei expresii. Acesta poate fi
folosit apoi mai departe în alte construcţii (ex. pentru a declara alte variabile având
acelaşi tip).
Exemplul 6
#include <iostream>
using namespace std;
void main()
{
auto x = 0; // x este de tipul int deoarece 0 este o constanta int
auto c = 'a'; // char
auto d = 0.5; // double
auto v = 14400000000000LL; // long long
cout << "x = " << x << endl << "c = " << c << endl << "d = " << d << endl << "v = " << v << endl;
decltype (v) t;
t = 1;
cout << "y = " << y << ", sizeof (y): " << sizeof (y) << endl;
cout << "t = " << t << ", sizeof (t): " << sizeof (t) << endl << endl;
}
8
Mihai TOGAN
Observaţie:
Deşi poate părea că stabilirea tipului de date al variabilelor are loc la momentul execuţiei
(runtime), tipul acestora este fixat de compilator încă de la momentul de compilare
(buildtime).
Exemplul 7
void main ()
{
vector<int> vi;
vi.push_back (2);
vi.push_back (4);
vi.push_back (1);
func(vi);
cout << endl;
}
9
Mihai TOGAN
Pointerul constant nullptr
În C++03, pentru constanta de tip pointer cu valoarea 0 (pointerul nul) se foloseşte NULL
(simbol moştenit din C).
În unele implementări, NULL este definit ca o constantă literală întreagă de valoare 0:
#define NULL 0
Este recomandată folosirea constantei nullptr oriunde este necesar, în locul constantei
NULL.
Scopul folosirii lui nullptr este acela de a face codul mai clar şi de a elimina ambiguităţile
care pot apare în anumite situaţii, cum ar fi cele din scenariul următor:
f (int);
f (foo *);
f (0);
f (NULL);
f (nullptr);
X* ptr = nullptr;
X* ptr = NULL;
X* ptr = 0;
10
Mihai TOGAN
Sintaxa de iniţializare uniformă (Uniform initialization syntax)
Pe lângă sintaxa de iniţializare moştenită din C şi C++03, versiunea C++11 introduce noi
posibilităţi (sintaxă nouă) de iniţializare a obiectelor (mai flexibile şi mai intuitive).
În C++03, puteam folosi câteva variante de iniţializare, astfel:
class C
{
int a;
int b;
public:
C(int i, int j): a(i), b(j) {}
};
void main()
{
int x = 5; // initializare explicita
int y = int(); // initializare valoare implicita (0 in VS)
struct S {
int x;
S(): x(0) { } // lista de initializare membri in constructor
};
// ...
}
class D
{
int a = 7; // C++11, initializarea in-class a datelor membre
public:
D ();
};
class X
{
int a[4];
public:
X() : a{1,2,3,4} {} //C++11, initializarea in-class a unui vector, membru al clasei
};
11
Mihai TOGAN
Iniţializarea containerelor STL a fost şi ea simplificată:
// C++11 iniţializare container
std::vector<string> vs = { "first", "second", "third"};
std::map singers = {
{"Lady Gaga", "+1 (212) 555-7890"},
{"Beyonce Knowles", "+1 (212) 555-0987"}
};
În exemplul de mai sus, nu mai este necesară folosirea unei secvenţe de instrucţiuni
push_back.
Observaţii:
Deşi este o capabilitate C++11, varianta de compilator Microsoft (Visual Studio) suportă
sintaxa de iniţializare uniformă de la versiunea VS 2013 în sus.
12
Mihai TOGAN
Delegarea Constructorilor (Delegating Constructors)
În versiunea C++03, o clasă poate avea mai mulţi constructori. Aceştia diferă prin
numărul şi tipul parametrilor, însă de regulă algoritmul de iniţializare a obiectelor este
identic.
#include <iostream>
using namespace std;
class A {
public:
A(): num1(0), num2(0) {
average=(num1+num2)/2;
// ...
}
private:
int num1;
int num2;
int average;
};
void main ()
{
A a, b(1), c(2, 3);
}
Observaţii:
Se observă că iniţializarea este identică în cele trei variante de constructori.
Există cod duplicat la nivelul constructorilor (total incorect).
class A {
public:
A(): num1(0), num2(0) {
init (0, 0);
cout << "Constructor implicit" << endl;
}
private:
int num1;
int num2;
int average;
Aceasta vine să rezolve scenariul prezentat mai sus. Astfel, se poate implementa un
constructor de bază care va concentra efortul de iniţializare, urmând ca celelalte variante de
constructori să apeleze acel constructor:
class A {
public:
A(): A(0) { }
A(int i, int j) {
num1=i;
num2=j;
average=(num1+num2)/2;
}
private:
int num1;
int num2;
int average;
};
Observaţii:
Delegarea constructorilor elimină problema de mai sus. Un constructor nu poate fi
invocat într-o altă metodă decât pentru construcţia unui obiect (initializare stare obiect
nou).
Se poate observa şi din exemplul de mai sus că un constructor poate fi delegat al altui
constructor. Se poate forma în acest fel un aşa numit lanţ de delegare. Atenţie însă la
apelul recursiv al constructorilor (nerecomandat).
Deşi este o capabilitate C++11, varianta de compilator Microsoft (Visual Studio) suportă
delegarea constructorilor de la versiunea VS 2013 în sus.
14
Mihai TOGAN
Referinţe Rvalue (Rvalue References)
O expresie lvalue este o expresie care poate apare în stânga sau în dreapta operatorului de
asignare. Proprietăţi:
Are o adresă de memorie (expresia suportă aplicarea operatorului de adresare).
De regulă, o locaţie de memorie identificată printr-un nume, sau adresată prin
intermediul unui pointer sau printr-o referinţă este o valoare lvalue.
Expresiile lvalue, persistă mai mult decât la o singură utilizare.
De regulă, name lvalue.
O expresie rvalue este o expresie care poate apare numai în dreapta operatorului de asignare.
Proprietăţi:
NU are o adresă de memorie (expresia NU suportă aplicarea operatorului de adresare).
De regulă, valorile literale (pure), obiectele temporare (unnamed objects) sunt valori
rvalue.
Expresiile rvalue, NU persistă mai mult decât la o singură utilizare.
Observaţie: Caracteristicile lvalue sau rvalue sunt proprietăţi ale expresiilor. NU sunt tipuri
de date, obiecte, valori, etc.
int a = 42;
int b = 43;
int i = 42, *pi = &i; // i si *pi sunt lvalue, &i este un rvalue
i = 43; // Ok, i este un lvalue
int* p = &i; // Ok, i este un lvalue
int& foo();
foo() = 42; // Ok, foo() este un lvalue (!)
int* p1 = &foo(); // Ok, foo() este un lvalue
Observaţie: cel mai simplu mod pentru a clasifica dacă o expresie este de tip lvalue sau
rvalue, este aceala de a testa aplicarea operatorului de adresare (&) asupra expresiei
respective.
15
Mihai TOGAN
C++03 oferă posibilitatea de a lucra cu referinţe, însă numai către valori de tip lvalue. O
construcţie de tipul următor are ca şi consecinţă o eroare de compilare:
int& rvalue_ref = 99;
C++11 introduce posibilitatea de a defini referinţe rvalue, adică referinţe către valori de
tip rvalue (conforme cu definiţia funizată mai sus). Exemplu de declarare a unei referinţe
rvalue:
int&& rvalue_ref = 99;
Exemplul 8:
#include <iostream>
void f(int& i) { std::cout << "lvalue ref: " << i << "\n"; }
void f(int&& i) { std::cout << "rvalue ref: " << i << "\n"; }
int main()
{
int i = 77;
f(i); // lvalue ref called
f(99); // rvalue ref called
return 0;
}
În exemplul de mai sus, a fost folosit std::move (T&&) Acesta este o funcţie template de
conversie necondiţionată de tip. Funcţia transformă expresia primită (de obicei un lvalue)
într-o expresie de tip rvalue-reference.
În principal, referinţele rvalue au fost propuse pentru a obţine o performanţă mai bună
(implementarea mecanismului de move în locul celui de deep-copy).
16
Mihai TOGAN
Prin intermediul referinţelor rvalue pot fi obţinute referinţe către obiecte temporare
(anonymous objects, unnamed objects). Exemple tipice în acest sens sunt valorile întoarse
în cadrul funcţiilor sau valorile întoarse de typecast-uri.
Observaţii:
O referinţă lvalue (C++03) poate fi realizată către un obiect modificabil. O astfel de
referinţă NU poate fi realizată către un obiect constant sau către un rvalue.
O referinţă constantă (C++03) poate fi realizată către un obiect constant sau către un
rvalue.
Referinţele de tip rvalue (C++11) au proprietăţi similare cu cele de tip lvalue (C++03):
Trebuie întotdeauna asignate (iniţializate) către un obiect existent.
NU permit reiniţializarea (reasignarea) către un alt obiect.
În particular, parametrii formali ai unei funcţii sunt valori lvalue (au nume).
17
Mihai TOGAN
Move Semantics
În C++03, transmiterea parametrilor prin valoare sau întoarcerea unei valori de return dintr-o
funcţie se realizează prin copiere, ceea ce implică un consum ineficient de memorie şi CPU.
(ex. copierea elementelor unui obiect vector <int> V (10000000, 0)).
În general, copierea are ca efect final faptul că un obiect destinaţie (obiect_d) va avea un
conţinut (stare) clonat al obiectului sursă (obiect_s). În unele cazuri, obiectul sursă nici nu
mai este folosit mai departe (ex. cazul unui obiect temporar creat la întoarcerea unei valori
dintr-o funcţie return obiect_s).
În acest caz, mutarea datelor dintr-o parte în alta ar putea fi o soluţie mult mai bună decât
copierea. Prin mutarea datelor se poate înţelege doar operaţia de schimbare a ownership–ului
resurselor interne ale obiectului.
Notă: mecanismul de schimbare a ownership-ului resurselor unui obiect este numit “resource
pilfering”.
Exemplul 9:
#include <string>
std::string func()
{
std::string s;
return s;
}
void main()
{
std::string mystr = func ();
}
C++11 permite optimizarea procesului de mai sus şi implementează mecanismul de tip move
semantics. Acesta permite evitarea copierii datelor. În locul operaţiei de copiere se realizează
mutarea datelor, operaţie care implică doar schimbarea ownership-ului asupra datelor.
int main () {
std::string foo = "foo-string";
std::string bar = "bar-string";
std::vector<std::string> myvector;
return 0;
}
Explicaţii:
În exemplul de mai sus, se realizează popularea lui myvector cu 2 string-uri folosind
metoda std::vector<T>::push_back.
La primul apel push_back, se face o copie nouă a valorii string-ului foo în myvector[0].
La al doilea apel push_back, se realizează mutarea valorii stringului conţinut de variabila
bar în myvector[1].
Variabila de tip string bar rămâne validă însă nu mai are valoarea iniţială (în acest
moment are o valoare undefined).
În C++ există atât funcţia template std::move (T&&) pentru conversia la un rvalue-
reference, dar şi std::move (InputIt first, InputIt last, OutputIt d_first), cea din urmă fiind
o funcţie template de mutare a unui interval de valori.
O implementare echivalentă pentru std::move (InputIt first, InputIt last, OutputIt d_first)
poate fi următoarea:
19
Mihai TOGAN
int main () {
// std::vector<std::string> foo = {"air","water","fire","earth"}; //variant bazată pe listă de
iniţializare, conform c++11 (!!)
std::vector<std::string> foo;
foo.push_back ("air");
foo.push_back ("water");
foo.push_back ("fire");
foo.push_back ("earth");
// moving ranges:
std::cout << "Moving ranges...\n";
std::move ( foo.begin(), foo.begin() + 4, bar.begin() );
std::cout << "foo contains " << foo.size() << " elements:";
std::cout << " (each in an unspecified but valid state)";
std::cout << '\n';
std::cout << "bar contains " << bar.size() << " elements:";
for (std::string& x: bar) std::cout << " [" << x << "]";
std::cout << '\n';
// moving container:
std::cout << "Moving container...\n";
foo = std::move (bar);
std::cout << "foo contains " << foo.size() << " elements:";
for (std::string& x: foo) std::cout << " [" << x << "]";
std::cout << '\n';
20
Mihai TOGAN
Implementarea unei funcţii de interschimbare a valorilor a două obiecte (swap) este un alt
scenariu care pune în evidenţă diferenţa între cele două mecanisme: deep-copy şi move
semantics.
21
Mihai TOGAN
Constructor de tip move (Move Constructor)
Constructorul de tip move se foloseşte în special când se face instanţierea unui obiect pe
baza unui obiect temporar (ex. un obiect temporar este un obiect întors de o funcţie folosind
instrucţiunea return).
Exemplul 14:
class MemoryBuff
{
int *mpData;
int mSize;
public:
~MemoryBuff ();
};
22
Mihai TOGAN
MemoryBuff& MemoryBuff::operator= (const MemoryBuff& other)
{
cout << "In operator copy-asignare, this=0x" << this << ", copiaza resursele din other=0x"
<< &other << " (mSize = " << other.mSize << ")" << endl;
if (this == &other)
return *this;
if (mpData != nullptr)
delete[] mpData;
mSize = other.mSize;
mpData = new int [mSize];
MemoryBuff::~MemoryBuff()
{
cout << "In destructor, this=0x" << this << " (mSize = " << mSize << "). ";
if (mpData != nullptr)
{
cout << "Elibereaza resursele din this=0x" << this << "...";
delete[] mpData;
}
void main()
{
std::vector<MemoryBuff> V;
După primul apel V.push_back(...), rezultatul programului este prezentat în figura de mai
jos:
23
Mihai TOGAN
Explicaţie:
1. Se crează mai întâi un obiect MemoryBuff (1000) folosindu-se constructorul explicit.
După execuţia celui de-al doilea push_back, rezultatul programului este prezentat mai jos:
24
Mihai TOGAN
Explicaţie:
4. Se crează un nou obiect MemoryBuff (2000) folosindu-se constructorul explicit.
Clonează vechiul element V[0] aflat la adresa 0x003B2F50 în noul V[0]. Clonarea
se realizează prin copierea celor 1000 întregi folosindu-se constructorul de copiere
(copy-constructor). Elementul V[0] este acum la adresa 0x003B2FE8.
25
Mihai TOGAN
La execuţia celui de-al treilea push_back, rezultatul programului este prezentat în figura
următoare:
Explicaţie:
8. Se crează din nou un obiect temporar MemoryBuff (4000) folosindu-se constructorul
explicit.
În final, la ieşirea din funcţia main, se va distruge vectorul V. În acest caz se vor distruge
cele 3 elemente actuale ale lui V (obiectele aflate la adresele 0x003B3038, 0x003B3040,
respectiv 0x003B3048). Resursele sunt eliberate folosindu-se destructorul clasei
MemoryBuff:
Observaţii:
După cum se poate observa din exemplul de mai sus, managementul obiectelor se face
exclusiv prin copierea resurselor acestora (deep-copy).
În programul de mai sus există o serie de obiecte temporare care sunt create pentru o
perioadă foarte scurtă pentru generarea apoi a elementelor vectorului V[0], V[1], V[2].
De asemenea, pentru managementul vectorului std::vector<MemoryBuff>, elementele
vectorului suportă mutarea efectivă în memorie cu tot cu resurse.
Această abordare nu este cea mai optimă. După crearea unui obiect temporar, clonarea
acestuia s-ar putea reduce doar la schimbarea ownership-ului asupra resurselor sale (în
cazul nostru, buferul efectiv de memorie adresat de mpData).
27
Mihai TOGAN
Limbajul C++11 pune la dispoziţie mecanisme noi faţă de varianta C++03 prin care se poate
realiza un tip special de clonare bazat pe schimbarea ownership-ului asupra resurselor
obiectului clonat.
Pentru asta este necesară implementarea a unui constructor de tip move (move constructor)
la nivelul clasei.
Modificăm exemplul anterior şi adăugăm la nivelul clasei constructorul de tip move, astfel:
Exemplul 15:
class MemoryBuff
{
int *mpData;
int mSize;
public:
~MemoryBuff ();
};
// foarte important: invalidarea resurselor din vechiul obiect (il pune intr-o stare "empty")
other.mpData = nullptr; // invalideaza resursele din other (au fost mutate)
// in acest fel, destructorul nu le dezaloca de doua ori
other.mSize = 0;
}
28
Mihai TOGAN
Observaţii:
Cele trei obiecte temporare sunt create la fel ca şi în cazul anterior, prin apelul
constructorului explicit.
Distrugerea obiectelor temporare are loc, însă nu se mai execută delete[] la resursa
mpData, întrucât resursa aparţine acum obiectului V[0].
Extinderea vectorului V are loc şi acum prin clonarea elementelor vechi în cele noi şi prin
distrugerea celor vechi. Diferenţele acum sunt date de faptul că operaţia de clonare se
face prin mutarea resurselor din proprietatea obiectelor vechi în proprietatea celor noi, iar
distrugerea celor vechi nu se face şi cu eliberarea resurselor.
În situaţia de mai sus, au fost folosite posibilităţile de tip move semantics puse la dispozitie
de compilatorul de C++11.
Observaţie:
Pentru folosirea mecanismului move semantics, este necesară implementarea
constructorului de tip move.
Obiectele clonate pot fi interpretate ca fiind expresii lvalue (apel copy-constructor) sau
rvalue (apel move-constructor). Dacă există constructorul de tip move, acesta este
automat apelat de compilator în situaţiile când compilatorul detectează o clonare de obiect
rvalue.
În exemplul nostru, parametrul lui V.push_back (...) este un obiect temporar, din acest
motiv fiind interpretat ca o expresie rvalue. Acest lucru conduce la apelul constructorului
de tip move în locul constructorului de tip copy.
Obiectele clonate pot fi transformate din expresii lvalue în expresii rvalue folosindu-se în
acest scop funcţia de conversie std::move (conduce la apelul unui move-constructor).
29
Mihai TOGAN
Exemplul 16: Utilizarea funcţiei de conversie std::move pentru clonarea unui obiect prin
mutare
void main()
{
// generarea unui obiect nou (constructor explicit)
MemoryBuff A (5000);
cout << endl << endl;
Explicaţii:
În exemplul anterior, se generează un obiect iniţial (buffer pentru 5000 întregi), la adresa
0x0036FD64.
Obiectul A este apoi clonat fiind generate alte două obiecte B şi C obţinute prin copierea
lui A (copy-constructor). În acest caz, obiectul iniţial este interpretat de compilator ca
fiind o expresie lvalue (variabila cu nume A). Noile obiecte B şi C sunt generate în
memorie la adresele 0x0036FD54, respectiv 0x0036FD44, fiecare având câte un buffer
separat de 5000 întregi.
30
Mihai TOGAN
Obiectul A este apoi clonat din nou fiind generat obiectul D. Acesta este obţinut prin
mutarea resurselor lui A (fiind folosită funcţia de conversie std::move, obiectul sursă A
este interpretat acum ca un rvalue, ceea ce implică apel move-constructor). Noul obiect D
obţinut prin clonarea lui A, este generat la adresa 0x0036FD34 şi primeşte ca resursă
bufferul din A (schimbare ownership). A este golit de resurse (resursele lui A sunt
invalidate: A.mSize = 0, A.mpData = nullptr).
Obiectul A (acum este golit de resurse) este clonat din nou în E. Variabila A este
convertită la o expresie rvalue prin apelul lui std:move. Obiectul E este generat la adresa
0x0036FD24 şi primeşte resursele lui A (mSize = 0, mpData = nullptr).
La finalul funcţiei main, cele cinci obiecte A, B, C, D şi E sunt distruse fiind apelat
destructorul pentru fiecare dintre aceste obiecte Doar obiectele B, C şi D au resurse
(mpData != nullptr) care sunt eliberate în destructor.
Concluzii:
Clonarea prin mutarea resurselor (implementare şi apel move-constructor) este o facilitate
introdusă la nivelul limbajului începând cu versiunea C++11 şi care permite optimizarea
consumului de resurse (memorie şi CPU).
În funcţie de tipul expresiei (lvalue vs. rvalue) care generează obiectul sursă, compilatorul
va apela automat constructorul de copiere sau de mutare.
Obiectele sursă pot fi interpretate implicit ca expresii rvalue sau pot fi convertite explicit
la expresii rvalue prin folosirea funcţiei std::move.
31
Mihai TOGAN
32
Mihai TOGAN
Asignarea de tip move
Discuţia din secţiunea anterioară este valabilă în scenariul de generare a unui obiect nou prin
clonarea unui obiect existent (cu apel copy-constructor vs. move constructor).
Situaţia este similară şi la aplicarea operatorului de asignare. După cum ştim, de regulă
clasele care necesită implementarea unui copy-constructor necesită în aceeaşi măsură şi
supraîncărcarea operatorului de asignare.
Exemplul 17:
void main()
{
// generarea unui obiect nou (constructor explicit)
MemoryBuff A (5000);
cout << endl << endl;
În acest caz, asignarea se face prin copierea resurselor. Cele două obiecte A şi B aflate la
adresele 0x0047FDFC, respectiv 0x0047FDEC, ajung după asignare să fie identice şi cu
resurse independente.
Şi în cazul asignării, se poate opta pentru ca asignarea să fie realizată prin mutare
(schimbarea ownership-ului resurselor). Se poate câştiga prin optimizarea consumului de
memorie şi CPU.
33
Mihai TOGAN
Clasa MemoryBuff va fi completată prin supraîncărcarea operatorului de asignare de tip
move (implementarea pentru expresii rvalues):
Exemplul 18:
class MemoryBuff
{
int *mpData;
int mSize;
public:
~MemoryBuff ();
};
if (this == &other)
return *this;
// foarte important: invalidarea resurselor din vechiul obiect (il pune intr-o stare "empty")
other.mpData = nullptr; // invalideaza resursele din other (au fost mutate)
// in acest fel, destructorul nu le dezaloca de doua ori
other.mSize = 0;
return *this;
}
void main()
{
// generarea unui obiect nou (constructor explicit)
MemoryBuff A (5000);
cout << endl << endl;
34
Mihai TOGAN
// generarea unui obiect nou (constructor explicit)
MemoryBuff C (7000);
cout << endl << endl;
Explicaţii:
Iniţial, sunt instanţiate trei obiecte (A, B şi C). Construcţia obiectelor este bazată pe
constructorul uzual explicit.
Distrugerea celor trei obiecte implică de fapt eliberarea resurselor dinamice de memorie
(delete[] mpData) numai pentru obiectele B şi C. Obiectul A nu mai deţine resurse, în
consecinţă nu se dezalocă nimic.
35
Mihai TOGAN
Observaţii:
Dacă nu modificarea nu este necesară (puţin probabil, întrucât în acest caz, de regulă apar
probleme legate de eliberarea multiplă a resurselor), aceste metode pot fi implementate şi
cu parametrii de forma const T &&.
Deşi posibile, în mod uzual implementările bazate pe referinţe rvalue constante nu sunt
de interes în practică.
În practică cele mai multe situaţii care pot obţine avantaje folosind move semantics
(rvalues) necesită mai degrabă constructorul de tip move, şi mai puţin operatorul de
asignare.
36