Semantica mutării
rvalue
În C++ valorile folosite pot fi împărțite în două grupe: lvalue și rvalue.
lvalue reprezintă o valoare denumită, de exemplu variabile, parametri, constante. La un lvalue este asociată o adresă în memorie unde este stocată permanent o anumită valoare. Și putem atribui unei variabile lvalue o valoare.
rvalue este ceva ce poate fi doar atribuit, de exemplu literali sau rezultatele unor expresii.
Exemplu:
int n = 5;
Aici n este un lvalue, iar numărul 5 este un rvalue. Aceste denumiri provin de la faptul că n se află în partea stângă a operatorului de atribuire (left value), iar valoarea atribuită — numărul 5 — în partea dreaptă (right value).
Un alt exemplu:
int n{5};
int k{n + 7};
Aici n și k sunt lvalue, iar 5 și expresia n + 7 sunt rvalue.
Referința rvalue
Referința rvalue poate referi rezultatul unei expresii, chiar dacă acel rezultat este o valoare temporară. Legarea unei referințe la un rvalue prelungește durata de viață a valorii temporare. Memoria acestei valori nu va fi eliberată cât timp referința rvalue este în domeniul de vizibilitate.
Pentru a declara o referință rvalue se folosesc două semne & după numele tipului:
#include <iostream>
int main()
{
int n {5};
int&& tempRef {n+3}; // referință rvalue
std::cout << tempRef << std::endl; // 8
}
În acest caz, rezultatul expresiei n + 3 este stocat în memorie (stack), iar tempRef este o referință la această valoare temporară. Când funcția main se termină, și domeniul de vizibilitate al variabilei tempRef se încheie, iar valoarea temporară la care aceasta face referire este ștearsă.
Este important de menționat că, deși tempRef stochează referința la un rvalue, el însuși este un lvalue.
Funcția std::move
Nu putem lega o referință rvalue la un lvalue, de exemplu:
int n {5};
int&& tempRef = n; // ! Nu se poate
Aici n este un lvalue, iar o referință rvalue poate primi doar un rvalue. Totuși, uneori este nevoie să convertim un lvalue în rvalue. Pentru aceasta folosim funcția încorporată std::move(), disponibilă în biblioteca standard C++:
#include <iostream>
int main()
{
int n {5};
int&& tempRef = std::move(n); // convertim int în int&&
std::cout << tempRef << std::endl; // 5
}
Aici valoarea variabilei n este convertită din tipul int în int&& — o referință rvalue la int. Deși în acest exemplu conversia nu aduce un beneficiu practic, mai târziu, în exemplul constructorului de mutare, vom vedea avantajul acestei funcții.
De asemenea, trebuie remarcat că, atunci când convertim o valoare constantă, rezultatul este o referință constantă:
const int m{2};
const int&& mRef = std::move(m); // rezultatul este o referință constantă
Referința rvalue ca parametru al unei funcții
Referința rvalue poate fi utilizată ca parametru al unei funcții.
#include <iostream>
void print(std::string&& text)
{
std::cout << text << std::endl;
}
int main()
{
print("hello");
}
Pentru a indica faptul că parametrul este o referință rvalue, după tip se pun două ampersand-uri &&. Astfel, funcția print primește o referință rvalue la un obiect de tip std::string. La apelarea funcției putem transmite un rvalue:
print("hello");
Dar nu putem transmite un lvalue, deci următoarele linii nu vor compila:
std::string message = "hi world";
print(message); // ! Eroare - transmitem un lvalue
Totuși, am putea folosi funcția std::move() pentru a converti variabila într-un rvalue:
print(std::move(message));
Returnarea unui rvalue din funcție
Când returnăm valoarea unei variabile locale sau a unui parametru, compilatorul tratează valoarea ca rvalue. Dar dacă valoarea returnată este o variabilă, compilatorul poate aplica optimizarea NRVO (named return value optimization):
#include <iostream>
void print(std::string&& text)
{
std::cout << text << std::endl;
}
std::string defaultMessage()
{
std::string message{"hello world"};
return message;
}
int main()
{
print(defaultMessage()); // transmitem un rvalue
}
Funcția defaultMessage returnează un rvalue, deci rezultatul acestei funcții îl putem transmite funcției print. Optimizarea NRVO înseamnă că compilatorul păstrează obiectul rezultat direct în spațiul de memorie destinat valorii returnate, deci nu se mai alocă memorie suplimentară pentru o variabilă automată cu numele message. Astfel, la rularea acestui program se creează un singur obiect std::string.
La fel se întâmplă când păstrăm valoarea returnată într-o variabilă externă:
#include <iostream>
std::string defaultMessage()
{
std::string message{"hello world"};
return message;
}
int main()
{
std::string text = defaultMessage();
std::cout << text << std::endl;
}
Și aici se creează doar un singur obiect std::string.
Aceasta a fost o introducere în valorile rvalue și cum să lucrăm cu ele. În articolele următoare vom vedea aplicații practice.