Last modified: October 10, 2024

This article is written in: 🇵🇱

L-wartości i R-wartości

W języku C++ pojęcia L-wartości (ang. l-value) i R-wartości (ang. r-value) są fundamentalne dla zrozumienia mechanizmów przypisywania, przekazywania argumentów do funkcji, zarządzania pamięcią oraz optymalizacji kodu. Precyzyjne rozróżnienie między tymi kategoriami wartości jest kluczowe dla pisania efektywnego i bezpiecznego kodu.

L-wartości

L-wartości to wyrażenia, które reprezentują obiekty mające trwałe miejsce w pamięci (czyli posiadają adres). Są to obiekty, do których możemy się odwołać po ich utworzeniu i które mogą być umieszczone po lewej stronie operatora przypisania. Termin "L-wartość" pochodzi od "left value", czyli wartości mogącej wystąpić po lewej stronie przypisania.

Właściwości L-wartości

Przykłady L-wartości

int main() {
    int i = 3;        // 'i' jest modyfikowalną L-wartością
    const int ci = 5; // 'ci' jest niemodyfikowalną L-wartością

    int *pi = &i     // '&i' zwraca adres, 'pi' jest L-wartością
    int &ri = i;      // 'ri' jest referencją do L-wartości 'i'

    i = 10;           // Przypisanie do L-wartości 'i'
    // ci = 6;        // Błąd! 'ci' jest niemodyfikowalną L-wartością

    return 0;
}

Uwaga na temat L-wartości i operatorów

Niektóre operatory w C++ zwracają L-wartości, np. operator indeksowania [], operator dereferencji * czy operator inkrementacji/dekrementacji w formie prefiksowej.

int arr[5];
arr[0] = 10;     // 'arr[0]' jest L-wartością
*(arr + 1) = 20; // '*(arr + 1)' jest L-wartością

int j = ++i;     // '++i' zwraca L-wartość

R-wartości

R-wartości to wyrażenia, które nie mają trwałego miejsca w pamięci, czyli nie można pobrać ich adresu w sposób bezpośredni. Są to tymczasowe wartości, które zazwyczaj istnieją tylko w trakcie ewaluacji wyrażenia. Nazwa "R-wartość" pochodzi od "right value", ponieważ najczęściej pojawiają się po prawej stronie operatora przypisania.

Właściwości R-wartości

Przykłady R-wartości

int main() {
    int i = 3;         // '3' jest R-wartością
    int sum = i + 4;   // 'i + 4' jest R-wartością

    // int *p = &(i + 4); // Błąd! Nie można pobrać adresu R-wartości
    // (i + 4) = 10;      // Błąd! Nie można przypisać do R-wartości

    return 0;
}

Funkcje zwracające R-wartości

Funkcje, które zwracają wartości przez kopię, zwracają R-wartości.

int add(int a, int b) {
    return a + b; // 'a + b' jest R-wartością
}

int main() {
    int result = add(5, 7); // 'add(5, 7)' jest R-wartością
    return 0;
}

L-wartości vs R-wartości

Typ wartości Trwały adres w pamięci Modyfikowalność Strona operatora przypisania
L-wartości Tak Modyfikowalne lub niemodyfikowalne Mogą być po lewej stronie operatora przypisania
R-wartości Nie Niemodyfikowalne Mogą być tylko po prawej stronie operatora przypisania

Przykłady użycia

int x = 5;     // 'x' jest L-wartością, '5' jest R-wartością
int y = x + 2; // 'x + 2' jest R-wartością

x = y;         // Przypisanie do L-wartości 'x' wartości R-wartości 'y'

R-wartości i L-wartości w kontekście funkcji

Przekazywanie argumentów przez wartość

Przy przekazywaniu argumentów przez wartość do funkcji przekazywana jest kopia argumentu. Możemy przekazywać zarówno L-wartości, jak i R-wartości.

void func(int x) {
    // 'x' jest lokalną kopią przekazanego argumentu
}

int main() {
    int a = 10;
    func(a);    // 'a' jest L-wartością
    func(20);   // '20' jest R-wartością
    return 0;
}

Przekazywanie argumentów przez referencję do L-wartości

Przekazywanie argumentów przez referencję do L-wartości (&) umożliwia funkcji operowanie na oryginalnym obiekcie.

void func(int &x) {
    x = x * 2;  // Modyfikujemy oryginalną wartość
}

int main() {
    int a = 10;
    func(a);    // 'a' jest L-wartością
    // func(20); // Błąd! '20' jest R-wartością, nie można przekazać do 'int &'
    return 0;
}

R-wartościowe referencje (C++11 i nowsze)

Wprowadzone w C++11 referencje do R-wartości (&&) pozwalają na przechwytywanie R-wartości, co jest kluczowe dla implementacji semantyki przenoszenia.

void func(int &&x) {
    x = x * 2;  // Modyfikujemy tymczasową wartość
}

int main() {
    // int a = 10;
    // func(a);     // Błąd! 'a' jest L-wartością
    func(20);      // '20' jest R-wartością
    func(std::move(a)); // 'std::move(a)' zamienia 'a' w R-wartość
    return 0;
}

Uwaga na temat std::move

Funkcja std::move z biblioteki <utility> konwertuje L-wartość na R-wartość, umożliwiając przeniesienie zasobów.

#include <utility>

int main() {
    std::string str = "Hello";
    std::string newStr = std::move(str); // Przeniesienie zawartości 'str' do 'newStr'
    // 'str' jest teraz pusty lub w stanie "moved-from"
    return 0;
}

Semantyka przenoszenia (Move Semantics)

Semantyka przenoszenia w C++11 umożliwia efektywne przenoszenie zasobów (np. pamięci, uchwytów do plików) z obiektów tymczasowych lub tych, które nie są już potrzebne, do nowych obiektów, bez kosztownego kopiowania.

Konstruktor przenoszący i operator przypisania przenoszącego

Aby klasa mogła korzystać z semantyki przenoszenia, powinna zdefiniować:

Przykład klasy z semantyką przenoszenia

#include <iostream>
#include <vector>

class MoveExample {
public:
    std::vector<int> data;

    // Konstruktor domyślny
    MoveExample() = default;

    // Konstruktor kopiujący
    MoveExample(const MoveExample &other) : data(other.data) {
        std::cout << "Konstruktor kopiujący\n";
    }

    // Konstruktor przenoszący
    MoveExample(MoveExample &&other) noexcept : data(std::move(other.data)) {
        std::cout << "Konstruktor przenoszący\n";
    }

    // Operator przypisania kopiującego
    MoveExample& operator=(const MoveExample &other) {
        std::cout << "Operator przypisania kopiującego\n";
        if (this != &other) {
            data = other.data;
        }
        return *this;
    }

    // Operator przypisania przenoszącego
    MoveExample& operator=(MoveExample &&other) noexcept {
        std::cout << "Operator przypisania przenoszącego\n";
        if (this != &other) {
            data = std::move(other.data);
        }
        return *this;
    }

    // Funkcja do wyświetlania zawartości
    void display() const {
        for (int i : data) {
            std::cout << i << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MoveExample me1;
    me1.data = {1, 2, 3, 4, 5};

    MoveExample me2 = me1;            // Wywołanie konstruktora kopiującego
    MoveExample me3 = std::move(me1); // Wywołanie konstruktora przenoszącego

    me2.display(); // Wyświetla: 1 2 3 4 5
    me3.display(); // Wyświetla: 1 2 3 4 5

    me2 = me3;             // Wywołanie operatora przypisania kopiującego
    me2 = MoveExample();   // Wywołanie operatora przypisania przenoszącego

    return 0;
}

Wyjaśnienie działania:

Zalety semantyki przenoszenia

Spis Treści

    L-wartości i R-wartości
    1. L-wartości
      1. Właściwości L-wartości
      2. Przykłady L-wartości
      3. Uwaga na temat L-wartości i operatorów
    2. R-wartości
      1. Właściwości R-wartości
      2. Przykłady R-wartości
      3. Funkcje zwracające R-wartości
    3. L-wartości vs R-wartości
      1. Przykłady użycia
    4. R-wartości i L-wartości w kontekście funkcji
      1. Przekazywanie argumentów przez wartość
      2. Przekazywanie argumentów przez referencję do L-wartości
      3. R-wartościowe referencje (C++11 i nowsze)
      4. Uwaga na temat std::move
    5. Semantyka przenoszenia (Move Semantics)
      1. Konstruktor przenoszący i operator przypisania przenoszącego
      2. Przykład klasy z semantyką przenoszenia
      3. Zalety semantyki przenoszenia