You are on page 1of 36

Mihai TOGAN

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ţă

 Printre facităţile introduse de C++11, putem enumera următoarele:


 Definirea de funcţii în cadrul altor funcţii (expresiile lambda).
 Posibilitatea de a folosi apelul unor constructori în cadrul altor constructori la
nivelul unei clase (delegarea constructorilor).
 Permite deducerea automată a tipului de date (auto).
 Introducerea referinţei rvalue (un alt tip de referinţă faţă de cea cunoscută).
 Implementarea unor mecanisme de optimizare a consumui de memorie şi CPU prin
schimbarea ownership-ului resurselor între obiecte (mecanismul move semantics).
 Definirea constructorului dşi a operatorului de asignare de tip move.
 Definirea unor modalităţi noi de iniţializare a obiectelor (adresate în special
containerelor STL şi tipurilor de date agregate).
 Etc.

 Toate aceste capabilităţi completează limbajul consistent şi îl apropie de alte limbaje


actuale de tip OOP (Java, C#, etc.). Discutăm în acest context de sintaxa Modern C++.

De altfel, Bjarne Stroustrup, creatorul limbajului C++, afirma la un moment dat faptul că
„C++11 feels like a new language”.

 Versiunile C++14 (2014) şi C++17 (2017) completează facilităţile introduse de C++11


sau aduc şi ele facilităţi noi la nivelul limbajului C++.

2
Mihai TOGAN
Expresii lambda

 Facilitate introdusă în C++11 şi completată apoi în C++14 şi C++17.


 Permit scrierea şi utilizarea de funcţii in-place (la nivel local) în cadrul altor funcţii.
 Permit parametrizarea.
 Permit utilizarea (capturarea) variabilelor locale sau globale vizibile la nivelul funcţiei
unde este definită (elimina necesitatea transferului acestora prin parametrizare).

 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>

using namespace std;

void print_it (int i) {


cout << ":" << i << ":";
}

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

// aplica functia print_it pentru fiecare intreg din lista


for_each(int_list.begin(), int_list.end(), print_it);

cout << endl<< endl;


}

Observaţii:
 În exemplul de mai sus, a fost folosită construcţia for_each (facilitate C++03).

for_each( InputIt first, InputIt last, UnaryFunction f );

 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>

using namespace std;

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 << ":"; });

cout << endl<< endl;


}

Rezultatul este identic cu cel obţinut în exemplul 1:

 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).

Sintaxa generală pentru o expresie lambda:

[capture list] (parameter list) {function body}

 Lista de parametrii este ca o listă de parametrii obişnuită la o funcţie.


 Corpul expresiei (function body) se comportă ca la o funcţie obişnuită.
 Tipul şi valoarea de return depind de ce se află in body. Compilatorul deduce tipul în funcţie
de tipul valorii expresiei folosită la return (dacă nu există return, compilatorul asumă void).

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

Exemplul 3: capturarea unor variabile locale în cadrul unei expresii lambda:


5
Mihai TOGAN

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>

using namespace std;

int main()
{
char s[]="Hello World!";
int Uppercase = 0; // va fi modificat de lambda

for_each (s, s+sizeof(s), [&Uppercase] (char c) {


if (isupper(c))
Uppercase++;
});

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

for (int& x : my_array) {


x *= 2;
}

for (auto& x : my_array) {


x *= 2;
}

for (auto x : my_array)


std::cout << x << " ";

std::cout << std::endl;


}

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.

Exemplul 5: Declaraţii de tip auto

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;

typedef decltype (x) INT;


INT y;
y = 9;

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

Utilitatea mecanismului de deducţie automată a tipului este pusă în valoare în special în


cazul în care tipul unui obiect nu este evident de la prima vedere, sau când sintaxa unei
declaraţii poate fi simplificată.

Exemplul 7

void func(const vector<int> &vi)


{
// vector<int>::const_iterator ci=vi.begin(); // varianta complicata a declaratiei
auto ci = vi.begin(); // varianta simplificata a declaratiei

while (ci != vi.end())


{
cout << *ci << " ";
ci++;
}
}

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

 Limbajul C++11 introduce o nouă constantă nullptr. Aceasta defineşte exclusiv un


pointer cu valoarea 0x00000000 (sau 0x0000000000000000).

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

În acest caz, pot fi folosite următorele tipuri de apeluri:

f (0);
f (NULL);
f (nullptr);

 Observaţie: pentru clarificare, următorele expresii sunt echivalente:

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)

int arr[8]={0,1,2,3}; // initializare vector (C-style)

struct tm today={0, 1}; // initializare structura

std::string s ("hello"); // initializare std::string

struct S {
int x;
S(): x(0) { } // lista de initializare membri in constructor
};

C c (0, 0); // apelul constructorului cu 2 param

// ...
}

C++11 extinde posibilităţile de iniţializare:

C c {0,0}; // C++11, echivalent cu: C c(0,0).


// Se poate elimina astfel diferenta (creaza confuzie)
// fata de initializarea datelor de tip struct

int* a = new int[3] { 1, 2, 0 }; // C++11

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;
// ...
}

A(int i): num1(i), num2(0) {


average=(num1+num2)/2;
// ...
}

A(int i, int j) : num1(i), num2(j) {


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

Codul poate fi îmbunătăţit pentru a elimina duplicarea codului. De regulă se implementează


o funcţie parametrizată separată de iniţializare care este apoi apelată în constructori:

class A {
public:
A(): num1(0), num2(0) {
init (0, 0);
cout << "Constructor implicit" << endl;
}

A(int i): num1(i), num2(0) {


init (i, 0);
cout << "Constructor explicit 1 param" << endl;
}
13
Mihai TOGAN

A(int i, int j): num1(i), num2(j) {


init (i, j);
}

private:
int num1;
int num2;
int average;

void init (int x, int y) {


average=(num1+num2)/2;
// ...
}
};

Varianta nouă elimină duplicarea codului. Introduce însă altă problemă:


 Adaugă o funcţie membră separată la nivelul clasei. Această funcţie ar putea fi apelată
accidental în alte funcţii membre ceea poate conduce la rezultate incorecte.

C++11 propune o funcţionalitate nouă faţă de C++03, numită delegarea constructorilor.

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): A(i, 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;

// a si b sunt expresii de tip lvalue:


a = b; // Ok
b = a; // Ok
a = a * b; // Ok

// Expresia a * b este o expresie rvalue


int c = a * b; // Ok, rvalue se afla la dreapta operatorului de asignare
// a * b = 42; // Eroare,rvalue se afla la stanga operator de asignare

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

// Exemple de expresii rvalues:


int foobar();
int j = 0;
j = foobar(); // Ok, foobar() este un rvalue

//int* p2 = &foobar(); // Declaratia ar conduce la o eroare de compilare, deoarece


adresa unui rvalue nu poate fi “preluata”
j = 42; // Ok, 42 este un rvalue

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;

 O referinţă rvalue (rvalue reference) este definită T&&.

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

f(std::move(i)); // 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.

 De regulă, o variabilă – de exemplu variabila i – nu este o expresie rvalue, iar pentru a fi


transformată într-un rvalue se poate folosi funcţia de conversie std::move (i).

 Referinţele rvalue (Rvalue Reference) permite implementarea de funcţii supraîncărcate


(function overloading). Compilatorul alege funcţia care este apelată în funcţie de tipul
parametrului furnizat (lvalue vs. rvalue).

 Î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.

 O referinţă rvalue (C++11) poate fi realizată numai către o valoare interpretată de


compilator ca expresie 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).

int f(string&& str); // str este un LValue...


int x = f(″hello″); // str este asignat aici catre un RValue

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;

//... do something with s here

return s;
}

void main()
{
std::string mystr = func ();
}

În exemplul de mai sus:


1. Când se face return din funcţia func, C++ crează o copie temporară a obiectului s în
zona de memorie de pe stiva funcţiei apelante (în cazul nostru, funcţia main).
2. Obiectul s este distrus (la eliberarea stivei funcţiei func), iar copia temporară este
folosită pentru construcţia prin copiere (copy-constructor) a lui mystr.
3. Copia temporară este distrusă.

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.

Funcţia template std::move este o componentă importantă pentru implementarea


mecanismului de tip move semantics.
18
Mihai TOGAN
Exemplul 10: efectul utilizării lui std::move (T&&)

#include <utility> // std::move


#include <iostream> // std::cout
#include <vector> // std::vector
#include <string> // std::string

int main () {
std::string foo = "foo-string";
std::string bar = "bar-string";
std::vector<std::string> myvector;

myvector.push_back (foo); // copies


myvector.push_back (std::move(bar)); // moves

std::cout << "myvector contains:";


for (std::string& x:myvector) std::cout << ' ' << x;
std::cout << '\n';

std::cout << "foo-string contains: " << foo << '\n';


std::cout << "bar-string contains: " << bar << '\n';

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

 Acest lucru se întâmplă deoarece metoda std::vector::push_back implementează


mecanismul move semantics dacă parametrul lui push_back este un rvalue reference.

Rezultatul programului prezentat sus este următorul:

 Î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

template<class InputIterator, class OutputIterator>


OutputIterator move (InputIterator first, InputIterator last, OutputIterator result)
{
while (first!=last)
{
*result = std::move (*first);
++result;
++first;
}
return result;
}

Exemplul 11: Folosirea lui std::move (first, last, d_first)

#include <iostream> // std::cout


#include <algorithm> // std::move (ranges)
#include <utility> // std::move (objects)
#include <vector> // std::vector
#include <string> // std::string

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

std::vector<std::string> bar (4);

// 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';

std::cout << "bar is in an unspecified but valid state";


std::cout << '\n';
return 0;
}

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.

Exemplul 12: Implementare tradiţională a unei funcţii swap

template <typename T>


void swap(T& a, T& b) {
T tmp(a); // acum, exista 2 copii ale lui a
a = b; // acum, exista 2 copii ale lui b
b = tmp; // acum, exista 2 copii ale lui tmp

// distrugerea (destructor) lui tmp


}

Exemplul 13: Implementare folosind posibilităţile de move din C++11

template <typename T>


void swap(T& a, T& b) {
T tmp(std::move (a)); // muta datele lui a in tmp
a = std::move (b); // muta datele lui b in a
b = std::move (tmp); // muta datele lui tmp in b

// distrugerea (destructor) lui tmp


// nu prea mai exista mare lucru de facut aici
}

21
Mihai TOGAN
Constructor de tip move (Move Constructor)

Implementarea mecanismului move semantics la nivelul unei clase se realizează prin


implementarea constructorului move şi/sau a operatorului de asignare de tip move.

Constructorul de tip move funcţionează este similar ca scop cu un constructor de copiere.


Acesta poate realiza crearea unei instanţe noi prin clonarea unui obiect existent, însă permite
evitarea operaţiei de copiere a resurselor şi înlocuirea operaţiei de copiere cu operaţia de
mutare.

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:

#include <iostream> // std:cout


#include <cstring> // std::memset, std::memcpy, etc.
#include <vector> // std::vector

using namespace std;

class MemoryBuff
{
int *mpData;
int mSize;

public:

MemoryBuff (int size); // Explicit constructor


MemoryBuff (const MemoryBuff& other); // Copy constructor
MemoryBuff& operator= (const MemoryBuff& other); // Operator de asignare (copy)

~MemoryBuff ();

};

MemoryBuff::MemoryBuff (int size)


: mSize (size), mpData (new int[size])
{
cout << "In constructor explicit, this=0x" << this << " (mSize = " << mSize << ")" << endl;
std::memset(mpData, 0, mSize * sizeof (int));
}

MemoryBuff::MemoryBuff (const MemoryBuff &other)


: mSize (other.mSize), mpData (new int[other.mSize])
{
cout << "In copy-constructor, this=0x" << this << ", copiaza resursele din other=0x"
<< &other << " (mSize = " << mSize << ")" << endl;

std::memcpy (mpData, other.mpData, mSize * sizeof (int));


}

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

std::copy (other.mpData, other.mpData + mSize, mpData); // varianta la std::memcopy


return *this;
}

MemoryBuff::~MemoryBuff()
{
cout << "In destructor, this=0x" << this << " (mSize = " << mSize << "). ";

if (mpData != nullptr)
{
cout << "Elibereaza resursele din this=0x" << this << "...";
delete[] mpData;
}

cout << endl;


}

void main()
{
std::vector<MemoryBuff> V;

V.push_back(MemoryBuff (1000)); // add un obiect MemBuff de 1000 de intregi


cout << endl << endl;

V.push_back(MemoryBuff (2000)); // add alt obiect MemBuff de 2000 de intregi


cout << endl << endl;

V.push_back(MemoryBuff (4000)); // add alt obiect MemBuff de 4000 de intregi


cout << endl << endl;
}

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.

Obiectul nou creat:


 Se află la adresa 0x0036F644.
 Este un buffer pentru 1000 întregi.
 Este un obiect temporar. Va fi folosit pentru crearea lui V[0] şi apoi va fi
distrus.

2. La execuţia V.push_back(...), se realizează o clonă a obiectului temporar generat la


pasul 1 (obiectul aflat la adresa 0x0036F644). Clona se realizează prin copierea celor
1000 întregi folosindu-se constructorul de copiere (copy-constructor). Rezultă un
obiect nou în V[0], la adresa 0x003B2F50.

3. Distruge obiectul temporar aflat la adresa 0x0036F644. Se apelează destructorul clasei


care eliberează resursele acestui obiect.

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.

Obiectul nou creat:


 Se află la adresa 0x0036F654.
 Este un buffer de memorie pentru 2000 întregi.
 Este un obiect temporar. Va fi folosit în continuare doar pentru crearea lui V[1]
iar apoi va fi distrus.

5. La execuţia celui de-al doilea V.push_back(...), vectorul V este redimensionat pentru a


păstra cele două obiecte MemoryBuff:

 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.

 Distruge vechiul obiect V[0] aflat la adresa 0x003B2F50. Se apelează destructorul


clasei care eliberează resursele acestui obiect.

6. Adaugă cel de-al doilea element în vectorul V realizându-se o clonă a obiectului


temporar generat la pasul 4 (obiectul de la adresa 0x0036F654). Este folosit
constructorul de copiere (copy-constructor) pentru copierea celor 2000 întregi. Clona
se află în V[1], la adresa 0x003B2FF0.

7. Distruge obiectul temporar generat la pasul 4, aflat la adresa 0x0036F654. Se apelează


destructorul clasei care eliberează resursele acestui obiect.

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.

Obiectul nou creat:


 Se află la adresa 0x0036F664.
 Este un buffer de memorie pentru 4000 întregi.
 Este un obiect temporar care va fi folosit în continuare doar pentru crearea lui
V[1] iar apoi va fi distrus.

9. La execuţia celui de-al treilea V.push_back(...), vectorul V este redimensionat pentru a


păstra cele trei obiecte MemoryBuff:

 Clonează vechile elemente V[0] şi V[1], aflate la adresele 0x003B2FE8, respectiv


0x003B2FF0, în noile elemente V[0] şi V[1]. Clonarea se realizează prin copierea
celor 1000 şi respectiv 2000 întregi folosindu-se constructorul de copiere (copy-
constructor). Elementele V[0] şi V[1] se află acum la adresele 0x003B3038,
respectiv 0x003B3040.
26
Mihai TOGAN

 Distruge vechile obiecte V[0] şi V[1] aflate la adresele 0x003B2FE8, respectiv


0x003B2FF0. Se apelează destructorul clasei care eliberează resursele acestor
obiecte.

10.Adaugă cel de-al treilea element în vectorul V realizându-se o clonă a obiectului


temporar generat la pasul 8 (obiectul de la adresa 0x0036F664). Este folosit
constructorul de copiere (copy-constructor) pentru copierea celor 4000 întregi. Clona
acestuia se află în V[2], la adresa 0x003B3048.

11.Distruge obiectul temporar generat la pasul 8, aflat la adresa 0x0036F664. Se apelează


destructorul clasei care eliberează resursele acestui obiect.

Î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.

De regulă, un constructor de tip move realizează următorele acţiuni:


1. primeşte ca parametru o referinţă de tip rvalue la tipul clasei (T&&)
2. descarcă starea obiectului rvalue (dacă există o astfel de stare)
3. transferă ownership-ul resurselor obiectului rvalue
4. pune obiectul rvalue într-o stare „empty”

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 (int size); // Explicit constructor


MemoryBuff (const MemoryBuff& other); // Copy constructor
MemoryBuff& operator= (const MemoryBuff& other); // Operator de asignare (copy)

~MemoryBuff ();

MemoryBuff (MemoryBuff&& other); // Move constructor

};

MemoryBuff::MemoryBuff (MemoryBuff&& other)


: mSize (0), mpData (nullptr)
{
cout << "In move-constructor, this=0x" << this << ", muta resursele din other=0x"
<< &other << " (mSize = " << other.mSize << ")" << endl;

// transfera ownership-ul asupra resurselor


mpData = other.mpData;
mSize = other.mSize;

// 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;
}

În acest caz, la execuţia programului se obţine următorul rezultat:

28
Mihai TOGAN

Observaţii:
 Cele trei obiecte temporare sunt create la fel ca şi în cazul anterior, prin apelul
constructorului explicit.

 La primul apel V.push_back(...), se apelează automat constructorul de tip move, iar


clonarea se face prin mutarea resurselor de la un obiect la altul.

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

// generarea unui obiect nou prin clonarea unui lvalue (copy-constructor)


MemoryBuff B = A;
cout << endl << endl;

// generarea unui obiect nou prin clonarea unui lvalue (copy-constructor)


MemoryBuff C (A);
cout << endl << endl;

// generarea unui obiect nou prin clonarea unui rvalue (move-constructor)


MemoryBuff D = std::move (A);
cout << endl << endl;

// generarea unui obiect nou prin clonarea unui rvalue (move-constructor)


MemoryBuff E (std::move (A));
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.

 Folosirea mecanismului move semantics (clonarea prin mutare) trebuie realizată cu


atenţie având în vedere golirea resurselor obiectului sursă (vezi cazul obiectelor A şi E, in
exemplul de mai sus).

 Ca regulă, este corectă abordarea de a implementa un move-constructor acolo unde tipul


resurselor clasei necesită acest aspect, însă trebuie avut grijă la scenariile de folosire.

 Spre deosebire de copy-constructor, NU există move-constructor implicit, generat de


compilator.

 Dacă la nivelul clasei NU este implementat un constructor de tip move-constructor,


situaţiile care ar impune apelul acestuia (obiecte sursă interpretate ca fiind rvalues) sunt
tratate de compilator prin apelul constructorului de tip copy-constructor (explicit sau
implicit).

 De exemplu, dacă la la nivelul clasei MemoryBuff eliminăm constructorul de tip move-


constructor, programul din exemplul 16 generează rezultatul din figura următoare. Se
poate observa că cele 5 obiecte A, B, C, D, E (generate la adrese diferite) au resurse
independente (mpData != nullptr).

31
Mihai TOGAN

 Dacă la nivelul clasei nu se implementează un constructor explicit de copiere, situaţiile


care necesită clonarea obiectelor prin copiere (obiecte sursă interpretate ca fiind lvalues)
sunt rezolvate prin apelul constructorului de copiere implicit (bitwise copy).

 Exerciţiu: în exemplul 16, prin comentarea constructorului de tip copy-constructor


(move-constructor rămâne implementat), programul generează o excepţie la ieşirea din
funcţia main:

Q: Explicaţi de ce apare această excepţie.

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.

Mecanismul move semantics poate fi activat şi la operaţia de asignare dacă se supraîncarcă


operatorul de asignare cu un operator de asignare bazat pe un parametru de tip expresie
rvalue (T&&).

Clasa MemoryBuff are include deja implementaarea operatorului de asignare obişnuit


(lvalue).

Exemplul 17:

void main()
{
// generarea unui obiect nou (constructor explicit)
MemoryBuff A (5000);
cout << endl << endl;

// generarea unui obiect nou (constructor explicit)


MemoryBuff B (6000);
cout << endl << endl;

// modificare obiect prin asignarea unui obiect de tip lvalue (copy-asignare)


B = A;
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 (int size); // Explicit constructor


MemoryBuff (const MemoryBuff& other); // Copy constructor
MemoryBuff& operator= (const MemoryBuff& other); // Operator de asignare (copy)

~MemoryBuff ();

MemoryBuff (MemoryBuff&& other); // Move constructor


MemoryBuff& operator= (MemoryBuff&& other); // Operator de asignare (move)

};

MemoryBuff& MemoryBuff::operator= (MemoryBuff&& other)


{
cout << "In operator move-asignare, this=0x" << this << ", muta resursele din other=0x"
<< &other << " (mSize = " << other.mSize << ")" << endl;

if (this == &other)
return *this;

// transfera ownership-ul asupra resurselor


mpData = other.mpData;
mSize = other.mSize;

// 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;
}

Modificăm acum şi funcţia main, astfel:

void main()
{
// generarea unui obiect nou (constructor explicit)
MemoryBuff A (5000);
cout << endl << endl;

// generarea unui obiect nou (constructor explicit)


MemoryBuff B (6000);
cout << endl << endl;

34
Mihai TOGAN
// generarea unui obiect nou (constructor explicit)
MemoryBuff C (7000);
cout << endl << endl;

// modificare obiect prin asignarea unui obiect de tip lvalue (copy-asignare)


B = A;
cout << endl << endl;

// modificare obiect prin asignarea unui obiect de tip rvalue (move-asignare)


C = std::move(A);
cout << endl << endl;
}

În acest caz, programul generează următorul rezultat:

Explicaţii:

 Iniţial, sunt instanţiate trei obiecte (A, B şi C). Construcţia obiectelor este bazată pe
constructorul uzual explicit.

 După prima asignare, B = A, obiectul B va avea un conţinut identic cu A şi resursele sale


vor fi independente de cele ale lui A. Clonarea se face prin copiere (copy-assignment).

 După ce de a adoua asignare, C = A, obiectul C va avea acelaşi conţinut cu cel avut de A


înainte de asignare. Asignarea se face prin mutarea resurselor (schimbare ownership
resurse), cu consecinţa că obiectul A este golit de resurse (acest lucru este necesar pentru
a nu se dezaloca de două ori aceeaşi zonă de memorie).

 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:

 Spre deosebire de varianta copy, variantele move pentru constructor şi operatorul de


asignare nu au parametrul referinţă de tip const. Acest lucru este necesar deoarece
obiectul sursă este de obicei modificat (golit de resurse).

 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

You might also like