Avatar ·

Что такое rvalue, lvalue, xvalue, glvalue и prvalue в C++11?

Я изучаю C++11 и наткнулся на новые термины: rvalue, lvalue, xvalue, glvalue и prvalue. Раньше в старом C++ я думал, что всё просто: lvalue — это то, что стоит слева от присваивания (например, переменная), а rvalue — то, что справа (например, число 42). Но с появлением семантики перемещения и std::move всё стало запутанно. Например, я написал такой код: ```cpp int x = 10; // x — lvalue, 10 — prvalue int&& rref = std::move(x); // Что такое std::move(x)? И rref? int y = x + 5; // (x + 5) — это что? ``` Я слышал, что есть пять категорий, но не понимаю, как их различать на практике. В документации пишут про "идентичность" и "перемещаемость", но мне нужны простые примеры. Почему std::move(x) возвращает rvalue, хотя x — lvalue? И что такое xvalue? Объясните, пожалуйста, для новичка, с примерами кода. Мне кажется, это важно для понимания move-семантики и идеальной передачи (perfect forwarding). Буду благодарен за подробный разбор! <<>>

Краткая теория: дерево категорий

В C++11 все выражения делятся на три основные группы, которые образуют иерархию:

  • lvalue (left value) — выражение, которое имеет адрес в памяти и может стоять слева от присваивания. Пример: имя переменной, ссылка, элемент массива.
  • rvalue (right value) — выражение, которое не имеет постоянного адреса (временное). Раньше это были только prvalue, но теперь добавились xvalue.
  • xvalue (eXpiring value) — "умирающее" значение. Это объект, который скоро будет уничтожен, но у него всё ещё есть адрес. Например, результат std::move().

Полная иерархия: glvalue (generalized lvalue) = lvalue + xvalue; rvalue = xvalue + prvalue; prvalue (pure rvalue) — чистые временные объекты без адреса.

Примеры с пояснениями

Разберём ваш код:

int x = 10;          // x — lvalue (имеет адрес), 10 — prvalue (литерал, нет адреса)
int&& rref = std::move(x); // std::move(x) — xvalue (ссылка на x, но "умирающая")
int y = x + 5;       // (x + 5) — prvalue (результат арифметики, временное значение)

Почему std::move(x) — xvalue?
Функция std::move не перемещает ничего сама. Она просто преобразует lvalue в rvalue-ссылку (xvalue), чтобы компилятор мог выбрать конструктор перемещения. У xvalue есть адрес (он ссылается на x), но он "помечен" как временный, поэтому его можно "обворовать" (переместить ресурсы).

Как определить категорию на практике?

  • lvalue: можно взять адрес (&x работает). Пример: переменные, ссылки (int&), строковые литералы ("hello" — lvalue, но это исключение).
  • prvalue: нельзя взять адрес. Пример: литералы чисел (42), результаты операций (a + b), вызовы функций, возвращающих не-ссылки (std::string("abc")).
  • xvalue: результат std::move(), доступ к члену rvalue (std::string("abc").data()), тернарный оператор с xvalue.

Проверочный код

#include <utility>
#include <iostream>

void check(int&)  { std::cout << "lvalue\n"; }
void check(int&&) { std::cout << "rvalue\n"; }

int main() {
    int a = 5;
    check(a);            // lvalue
    check(10);           // prvalue → rvalue
    check(std::move(a)); // xvalue → rvalue
    return 0;
}

Вывод: lvalue, rvalue, rvalue. Обратите внимание: check(10) и check(std::move(a)) оба попадают в перегрузку с int&&, хотя природа разная (prvalue vs xvalue). Компилятору это безразлично — для него и то, и другое rvalue.

Зачем это нужно?

Различие между prvalue и xvalue важно для идеальной передачи (perfect forwarding). Например, в шаблонах:

template<typename T>
void wrapper(T&& arg) {
    // T&& может быть как int&, так и int&&
    // std::forward<T>(arg) сохраняет категорию
    other(std::forward<T>(arg));
}

Если arg — lvalue, std::forward вернёт lvalue-ссылку. Если arg — xvalue, вернёт xvalue. Если prvalue — тоже xvalue (через свёртку ссылок). Без понимания категорий вы не сможете правильно писать универсальные функции.

Итог: lvalue — "живёт долго", prvalue — "чисто временное", xvalue — "временное, но с адресом". Все rvalue (prvalue + xvalue) можно перемещать, но только xvalue имеет идентичность (адрес).

<<>>

Альтернативный взгляд: мнемоническое правило

Если вам сложно запомнить пять терминов, используйте упрощённую модель:

  • lvalue — то, на что можно ссылаться по имени и брать адрес. Думайте как "location value" (значение с местоположением).
  • rvalue — то, что не имеет постоянного местоположения. Делится на:
    • prvalue — "чистые" временные значения (результаты функций, литералы).
    • xvalue — "eXpiring" (истекающие) — это lvalue, которое мы сознательно превратили во временное с помощью std::move.

На практике вам нужно помнить только три категории для работы с move-семантикой:

  1. lvalue — копируем (не перемещаем).
  2. prvalue — можно перемещать (компилятор делает это автоматически).
  3. xvalue — тоже можно перемещать, но только если вы явно вызвали std::move.

Пример для закрепления:

std::vector<int> v1 = {1,2,3};
std::vector<int> v2 = v1;          // lvalue → копия
std::vector<int> v3 = std::move(v1); // xvalue → перемещение
std::vector<int> v4 = {4,5,6};     // prvalue → перемещение (RVO может оптимизировать)

Не зацикливайтесь на терминах "glvalue" и "xvalue" в повседневной работе. Они нужны только авторам стандарта и разработчикам компиляторов. Для вас главное — различать, когда происходит копирование, а когда перемещение.