Kategorie wartości w C++ – lvalue, rvalue, prvalue – podział wyrażeń
W języku C++ każda ekspresja (wyrażenie) posiada określoną kategorię wartości, która decyduje o sposobie jej przetwarzania przez kompilator. Podział na lvalue, rvalue, prvalue i inne kategorie jest fundamentalny dla zrozumienia mechanizmów takich jak semantyka przenoszenia, przekazywanie argumentów czy optymalizacje. Współczesny standard C++ (od wersji 11) definiuje pięć kategorii wartości, które tworzą hierarchię opartą o dwie niezależne właściwości: posiadanie tożsamości oraz możliwość przenoszenia (movability). Poniższe omówienie szczegółowo analizuje każdą kategorię, ich relacje oraz praktyczne implikacje.
Historyczny rozwój kategorii wartości
Przed standardem C++11 istniał jedynie binarny podział wartości na lvalue i rvalue, oparty na kryterium występowania po lewej stronie operatora przypisania. Ekspresje modyfikowalne (np. zmienne) klasyfikowano jako lvalue, natomiast tymczasowe obiekty (np. wyniki operacji) jako rvalue. Ta uproszczona klasyfikacja okazała się niewystarczająca dla wprowadzonej w C++11 semantyki przenoszenia, która umożliwia efektywne przekazywanie zasobów bez zbędnego kopiowania.
Nowy model, opisany w standardzie ISO/IEC 14882:2011, wprowadził trzy podstawowe kategorie:
- lvalue (np. zmienne, referencje);
- prvalue (czyste r-wartości, np. literały);
- xvalue (eXpiring values, np. obiekty zwracane przez
std::move()).
Dodatkowo zdefiniowano dwie kategorie złożone:
- glvalue (generalized lvalue) = lvalue + xvalue;
- rvalue = prvalue + xvalue.
Hierarchię tę ilustruje diagram relacji:
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
Kluczową innowacją było rozdzielenie pojęć tożsamości (adresowalność) i przenoszalności, co umożliwiło precyzyjne sterowanie ruchem obiektów.
Podstawowe kategorie wartości
Lvalue (Left value)
Definicja – Ekspresje posiadające trwałą tożsamość (adres w pamięci), które mogą wystąpić po lewej stronie operatora przypisania (o ile nie są const-kwalifikowane). Przykłady:
- zmienne (np.
int x; xjest lvalue); - elementy tablic (np.
arr); - referencje zwracane przez funkcje (np.
std::cout << 1); - właściwości klas (np.
obj.member); - ciągi znakowe (np.
"Hello").
Właściwości –
- możliwość pobrania adresu (
&x); - możliwość modyfikacji (o ile niezakwalifikowane jako
const); - długi czas życia – istnieją poza wyrażeniem, w którym zostały użyte.
Przykład błędnego użycia:
const int c = 42; c = 10; // Błąd: c jest const-lvalue!
Prvalue (Pure rvalue)
Definicja – Tymczasowe obiekty bez tożsamości, które służą do inicjalizacji lub obliczeń. Występują wyłącznie po prawej stronie przypisania. Przykłady:
- literały (np.
5,3.14,true); - wyniki operacji arytmetycznych (np.
x + 1); - funkcje zwracające wartość (np.
str.substr(0, 2)); - konstrukcje tymczasowe (np.
std::string("temp")).
Właściwości –
- brak adresu (
&(x + 1)jest niepoprawny); - krótki czas życia – niszczone po zakończeniu wyrażenia;
- automatyczna konwersja do xvalue w kontekstach wymagających przenoszenia (tzw. materializacja).
Przykład materializacji:
int&& r = 42; // prvalue '42' staje się xvalue w inicjalizacji
Xvalue (eXpiring value)
Definicja – Obiekty posiadające tożsamość, ale których zasoby można bezpiecznie przenieść (przejąć), ponieważ są „u kresu życia”. Powstają głównie poprzez:
- wywołanie
std::move()(np.std::move(x)); - rzutowanie na referencję do rvalue (np.
static_cast<int&&>(y)); - dostęp do składowych obiektów tymczasowych (np.
getTmp().data).
Właściwości –
- adresowalne, ale ich stan może być „unieważniony” po przeniesieniu;
- umożliwiają optymalizację poprzez unikanie kopiowania (np. w konstruktorach przenoszących).
Przykład użycia w semantyce przenoszenia:
std::vector<int> createBigData(); std::vector<int> v = createBigData(); // xvalue z createBigData() pozwala uniknąć kopiowania
Kategorie złożone
Glvalue (Generalized lvalue)
Definiowana jako suma lvalue i xvalue. Wszystkie glvalue posiadają tożsamość, co umożliwia:
- operacje typu
typeid; - polimorfizm dynamiczny;
- dostęp do niekompletnych typów.
Rvalue
Składa się z prvalue i xvalue. Kluczową cechą jest przenoszalność (movability), co wykorzystują:
- referencje do rvalue (
T&&); - konstruktory przenoszące;
- operatory przenoszące przypisania.
Praktyczne implikacje
Semantyka przenoszenia
Mechanizm oparty na kategoriach xvalue i prvalue pozwala na optymalizację operacji na obiektach tymczasowych. Funkcja std::move() konwertuje lvalue na xvalue, sygnalizując kompilatorowi możliwość przeniesienia zasobów:
std::string s1 = "Hello";
std::string s2 = std::move(s1); // s1 staje się xvalue, zasoby są przenoszone
Po takiej operacji s1 pozostaje w poprawnym, ale niezdefiniowanym stanie.
Doskonałe przekazywanie (perfect forwarding)
Wzorzec wykorzystujący referencje uniwersalne (T&&), działające zarówno dla lvalue, jak i rvalue. Gwarantuje, że kategoria wartości argumentu jest zachowana podczas przekazywania:
template<typename T>
void wrapper(T&& arg) {
// arg zachowuje oryginalną kategorię (lvalue/rvalue)
process(std::forward<T>(arg)); // std::forward konwertuje do T&&
}
Kluczową rolę odgrywa tu dedukcja typu oraz zasady collapsing referencji.
Inicjalizacja a kategorie
- Bezpośrednia inicjalizacja:
T obj(arg);– wykorzystuje kategorięarg; - Kopiująca inicjalizacja:
T obj = arg;– wymaga konwersji na prvalue.
Przykłady błędów:
int& r = 42; // Błąd: prvalue nie może bindować do lvalue-reference
const int& cr = 42; // Poprawnie: wydłużenie życia prvalue
Zaawansowane scenariusze
Materializacja prvalue
Standard C++17 wprowadził zasadę, że prvalue jest materializowane (staje się xvalue) w kontekstach wymagających obiektu:
- inicjalizacja referencji,
- dostęp do składowych,
- operatory logiczne.
Przykład:
int* p = &(std::string("temp").length()); // Materializacja tymczasowego stringa
Kategorie w wyrażeniach lambda
Domyślne przechwytywanie zmiennych ([=], [&]) różnicuje kategorie:
int x = 10;
auto lambda = [y = x + 1] { … }; // y jest prvalue
auto lambda2 = [&] { return x; }; // x jest lvalue
SFINAE a kategorie
Szablony mogą wykorzystywać kategorie do dopasowania przeciążeń:
template<typename T> void f(T&); // T: lvalue
template<typename T> void f(T&&); // T: rvalue
Podsumowanie
System kategorii wartości w C++ ewoluował od prostego podziału lvalue/rvalue do wyrafinowanego modelu pięciu kategorii, umożliwiającego zaawansowane mechanizmy optymalizacji. Zrozumienie różnic między lvalue, prvalue i xvalue jest kluczowe dla:
- efektywnego wykorzystania semantyki przenoszenia;
- poprawnego stosowania doskonałego przekazywania;
- unikania błędów przy pracy z referencjami i tymczasowymi obiektami.
Przyszłe standardy mogą rozszerzyć ten model o nowe koncepcje (np. pvalue z propozycji P0847), jednak obecna implementacja stanowi fundament bezpieczeństwa i wydajności nowoczesnego kodu C++.
