Last modified: March 19, 2026
This article is written in: 🇵🇱
W C++ bardzo dużo rzeczy kręci się wokół pytania: czy dane wyrażenie wskazuje na „konkretny obiekt w pamięci”, czy jest tylko tymczasowym wynikiem obliczeń. Z tego biorą się L-wartości (lvalues) i R-wartości (rvalues). Zrozumienie tego tematu odblokowuje m.in.:
T&, a raz nie,std::move istnieje i co naprawdę robi.Na start — dwa najważniejsze typy referencji:
T& → lvalue reference (referencja do L-wartości)T&& → rvalue reference (referencja do R-wartości)Najprostsza (praktyczna) heurystyka:
&x) i “żyje dłużej niż chwilę”.Uwaga: w nowoczesnym C++ są jeszcze pojęcia
prvalue,xvalue,glvalue— ale do większości codziennych przypadków wystarczy solidnie opanować “lvalue vs rvalue”.
L-wartość to wyrażenie, które identyfikuje konkretny obiekt (z miejscem w pamięci). Klasycznie: coś, co może stać po lewej stronie = (jeśli jest modyfikowalne).
int x = 10; // x jest L-wartością
x = 20; // OK: przypisanie do L-wartości
int* px = &x; // OK: można pobrać adres L-wartości
int x = 1; // modyfikowalna L-wartość
const int cx = 2; // niemodyfikowalna L-wartość
x = 5; // OK
// cx = 6; // BŁĄD: const, nie wolno zmieniać
Indeksowanie [] – zwraca element, który zwykle jest L-wartością:
int a[3] = {0, 0, 0};
a[1] = 7; // a[1] jest L-wartością
Dereferencja * – wynik jest L-wartością (jeśli wskaźnik wskazuje na obiekt):
int x = 10;
int* p = &x;
*p = 99; // *p jest L-wartością
Prefiksowe ++x zwraca L-wartość:
int x = 1;
++x = 10; // działa (choć rzadko tak się pisze)
R-wartość to wyrażenie, które jest zwykle tymczasową wartością — “wynikiem obliczeń”. Najczęściej pojawia się po prawej stronie przypisania.
int x = 3; // 3 to R-wartość (literal)
int y = x + 4; // (x + 4) to R-wartość (wynik wyrażenia)
Nie możesz przypisywać “do wyniku obliczenia”:
int x = 5;
// (x + 1) = 10; // BŁĄD: (x + 1) jest R-wartością
Zwykle nie pobierzesz też adresu tymczasowej R-wartości:
int x = 5;
// int* p = &(x + 1); // BŁĄD: nie bierze się adresu R-wartości
int add(int a, int b) {
return a + b; // zwraca tymczasowy wynik
}
int main() {
int r = add(2, 3); // add(2,3) jest R-wartością
}
| Cecha | L-wartość | R-wartość |
| “Ma własny obiekt / miejsce w pamięci” | zazwyczaj tak | zazwyczaj nie (tymczasowa) |
Adres & |
zwykle można | zwykle nie |
Może stać po lewej stronie = |
jeśli nie-const | nie |
| Przykłady | x, a[i], *p |
42, x+1, f() (zwraca przez wartość) |
Tu robi się naprawdę praktycznie.
Funkcja dostaje kopię, więc może przyjąć i L-wartość, i R-wartość:
void f(int v) { }
int main() {
int x = 10;
f(x); // L-wartość OK
f(20); // R-wartość OK
}
T& — referencja do L-wartościTaka funkcja wymaga czegoś trwałego (L-wartości), bo chce “podpiąć się” pod istniejący obiekt.
void g(int& v) {
v *= 2;
}
int main() {
int x = 10;
g(x); // OK (L-wartość)
// g(20); // BŁĄD (R-wartość nie pasuje do int&)
}
const T& — ważny wyjątekTo jest bardzo częsty wzorzec: można podać i L-wartość, i R-wartość, bo nie modyfikujesz argumentu, a kompilator może bezpiecznie “przedłużyć życie” tymczasowego obiektu na czas wywołania.
void h(const int& v) { }
int main() {
int x = 10;
h(x); // OK
h(20); // OK
}
T&& — referencja do R-wartości (C++11+)Pozwala przechwytywać tymczasowe obiekty albo obiekty, które jawnie “oddajesz do przeniesienia”.
#include <utility>
void k(int&& v) {
// v to nazwana zmienna, ale związana z R-wartością
}
int main() {
k(20); // OK: literal to R-wartość
int x = 10;
// k(x); // BŁĄD: x jest L-wartością
k(std::move(x)); // OK: “traktuj x jak R-wartość”
}
Co robi std::move?
Nie przenosi samo z siebie. To tylko rzutowanie (konwersja) mówiące: “od teraz możesz traktować ten obiekt jak R-wartość (czyli wolno z niego ‘zabrać’ zasoby)”.
Przenoszenie ma sens wtedy, gdy obiekt posiada zasób (np. bufor w pamięci). Zamiast kopiować cały zasób, można “przepiąć” wskaźniki/uchwyty.
Najczytelniej widać to na std::vector, std::string, itp.
#include <iostream>
#include <utility>
#include <vector>
struct Box {
std::vector<int> data;
Box() = default;
Box(const Box& other) : data(other.data) {
std::cout << "COPY\n";
}
Box(Box&& other) noexcept : data(std::move(other.data)) {
std::cout << "MOVE\n";
}
};
int main() {
Box a;
a.data = {1,2,3};
Box b = a; // COPY (a jest L-wartością)
Box c = std::move(a); // MOVE (a “oddane” do przeniesienia)
}
Obiekt źródłowy (np. a) jest w stanie moved-from: ma być poprawny (da się go zniszczyć, przypisać mu nową wartość), ale jego zawartość jest zwykle “pusta/neutralna”. Dla std::vector najczęściej będzie pusty.
T&.const T&.T&& (lub przeciążenia / forwarding).std::move(x) nie przenosi “magicznie” — tylko pozwala wybrać ścieżkę przenoszącą (konstruktor/operatory move).