Понятия lvalue и rvalue выражений, rvalue-ссылки

Практический курс по ООП C++: https://stepik.org/a/205781

Смотреть материал на YouTube | RuTube

Представьте, что имеется следующая программа на языке C++:

int main()
{
    int b {3};
    int a = 7 - b;
 
    return 0;
}

Здесь a, b – обычные целочисленные переменные, расположенные где-то в памяти устройства. Про такие переменные можно сказать, что они являются леводопустимыми (lvalue) выражениями, то есть, выражениями, которым можно присваивать значения определенного типа.

Конечно, простые переменные – это частный случай леводопустимых выражений. Другой пример – это указатель на выделенную область памяти:

    double* ptr_b = new double;
    *ptr_b = 6.43;
    delete ptr_b;

Или доступ к отдельным элементам массива:

    char str[] = "Hello";
    str[0] = 'D';

Или ссылка на тот или иной тип данных:

    int& count = b;

Или объект класса (структуры):

class Point {
public:
    int x{0}, y{0};
};
…
Point pt = {1, 2};

Или даже метод (функция), который возвращает конструкцию, связанную с областью памяти:

class Point {
public:
    int x{0}, y{0};
public:
    int& get_x() { return x; }
};
…
    Point pt = {1, 2};
    pt.get_x() = 10;

также является леводопустимым.

Характерной особенностью всех lvalue выражений является то, что они связаны прямо или косвенно с областью памяти, в которой хранятся данные определенного типа. И часто эти данные можно изменить (присвоить им другие значения), если конечно, нет ограничений модификатора const.

Выражения rvalue

Однако не все выражения в программе можно воспринимать, как lvalue. Например, нельзя целочисленной константе присвоить какое-либо другое значение:

int main()
{
    int b {3};
    int a = 7 - b;
 
    5 = a; // ошибка
    
    return 0;
}

Такие величины относятся к rvalue выражениям. Или, если взять две переменные и попытаться что-либо присвоить их сумме:

    a + b = 10;

то будет ошибка, так как каждая из переменных – это lvalue выражение, но их сумма уже не связана с какой-либо постоянной областью памяти, в которую можно занести новое значение, а потому относится к rvalue выражению. Даже обычный унарный плюс перед переменной:

    +b = 10;

превращает ее в rvalue выражение.

То же самое будет наблюдаться и с любыми другими lvalue выражениями:

int main()
{
    int b {3};
    int a = 7 - b;
 
    short ar[5] {0};
    ar[0] + b = 5;      // ошибка
 
    int* ptr_a = &a;
    int& lnk_b = b;
 
    (lnk_b * 5) = 10;      // ошибка
    (*ptr_a + b) = 10;     // ошибка
 
    return 0;
}

Одно из немногих исключений – это операция разыменывания адреса указателя:

    *(ptr_a + b) = 10;     // ok

Но это уже относится к адресной арифметике, и ничего необычного в этом нет.

Ссылки на lvalue и rvalue выражения

Почему в языке C++ так важно различать lvalue и rvalue выражения? Одна из причин, чтобы мы корректно в программах формировали конструкции для присвоения тех или иных значений в допустимые области памяти. Другая причина состоит в том, что только на lvalue выражения можно формировать знакомые нам ссылки. Например:

int main()
{
    int b {3};
    int a = 7 - b;
 
    Point pt;
 
    int& lnk_a = a;
    Point& lnk_pt = pt;
 
    return 0;
}

Соответственно, на rvalue выражения ссылки вести не могут, кроме константных:

int main()
{
    int b {3};
    int a = 7 - b;
 
    Point pt;
 
    int& lnk_a = 7 - b;         // rvalue
    Point& lnk_pt = Point();    // rvalue
    
    const int& lnk_a_cnst = 7 - b;         // ok
    const Point& lnk_pt_cnst = Point();    // ok
 
    return 0;
}

Однако, начиная со стандарта C++11, появился новый тип ссылок для rvalue выражений, который записывается с двумя амперсандами следующим образом:

int main()
{
...
    int&& lnk_a = 7 - b;         // rvalue
    Point&& lnk_pt = Point();    // rvalue
...
    return 0;
}

При этом временные объекты, на которые они ссылаются, продолжают существовать, пока существуют на них эти ссылки. Соответственно, через такие ссылки мы совершенно спокойно можем менять состояние объектов или выражений:

    std::cout << lnk_a << " " << lnk_pt.x << std::endl;
    lnk_a = 10;
    lnk_pt.x = 5;
    std::cout << lnk_a << " " << lnk_pt.x << std::endl;

Конечно, то же самое мы могли бы сделать и через константные ссылки, но, во-первых, это было бы неправильно, т.к. убирать модификатор const – часто порочная практика, а, во-вторых, в C++ имеются специальные конструкции, которые работают исключительно со ссылками на rvalue выражения, о которых речь пойдет на следующем занятии.

Функция std::move

Обратите внимание, что rvalue-ссылкам нельзя напрямую присваивать lvalue-ссылки и вообще любые lvalue выражения:

int main()
{
    int b {3};
    int a = 7 - b;
...
    int &lnk_la = a;
    int* ptr_a = &a;
 
    int&& lnk_ra_1 = a;          // ошибка
    int&& lnk_ra_2 = *ptr_a;  // ошибка
    int&& lnk_ra_3 = lnk_a;  // ошибка
 
    return 0;
}

Однако мы можем обойти это ограничение, если воспользоваться операцией преобразования типов, либо специальной функцией std::move следующим образом:

    int&& lnk_ra_1 = static_cast<int&&>(a);
    int&& lnk_ra_2 = std::move(*ptr_a);
    int&& lnk_ra_3 = std::move(lnk_a);

На практике предпочтение следует отдавать функции std::move, т.к. она, по сути, является надстройкой над оператором static_cast<T&&> и обеспечивает более безопасное преобразование типов ссылок.

А вот обратное присвоение lvalue ссылкам rvalue ссылок можно делать без каких-либо операций преобразования типов:

    lnk_la = lnk_ra_1;
    *ptr_a = lnk_ra_1;

Итак, из этого занятия вы должны четко различать lvalue и rvalue выражения, и знать, как формируются ссылки на rvalue выражения.

Практический курс по ООП C++: https://stepik.org/a/205781

Видео по теме