Last modified: March 19, 2026

This article is written in: 🇵🇱

L-wartości i R-wartości w C++ (wreszcie “po ludzku”)

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

Na start — dwa najważniejsze typy referencji:

Intuicja: “czy ma nazwę i adres?”

Najprostsza (praktyczna) heurystyka:

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ści (lvalues)

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

Najprostsze przykłady

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

L-wartość może być modyfikowalna lub nie

int x = 1;            // modyfikowalna L-wartość
const int cx = 2;     // niemodyfikowalna L-wartość

x = 5;                // OK
// cx = 6;            // BŁĄD: const, nie wolno zmieniać

Operatory, które często “dają L-wartość”

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ści (rvalues)

R-wartość to wyrażenie, które jest zwykle tymczasową wartością — “wynikiem obliczeń”. Najczęściej pojawia się po prawej stronie przypisania.

Typowe przykłady

int x = 3;        // 3 to R-wartość (literal)
int y = x + 4;    // (x + 4) to R-wartość (wynik wyrażenia)

Czego nie wolno z R-wartością?

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

Funkcja zwracająca przez wartość → rvalue

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

Podsumowanie: L vs R w jednym miejscu

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ść)

L/R w kontekście funkcji i referencji

Tu robi się naprawdę praktycznie.

Przekazanie przez wartość

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ści

Taka 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ątek

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

Semantyka przenoszenia (move semantics) — krótko i konkretnie

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.

Minimalny przykład z logowaniem kopii i przeniesienia

#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)
}

Co z obiektem “po przeniesieniu”?

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.

Najczęstsze błędy i proste reguły