Avatar ·

Что такое rvalues, lvalues, xvalues, glvalues и prvalues в C++?

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

Основные категории значений в C++11 и новее

Вы правы: в C++11 классификация стала сложнее из-за move-семантики. Вместо двух категорий (lvalue/rvalue) появилось пять. Они образуют иерархию:

  • lvalue (locator value) — выражение, которое ссылается на объект с определённым адресом в памяти.
  • prvalue (pure rvalue) — выражение, которое не имеет адреса (временные объекты, литералы, результаты арифметики).
  • xvalue (eXpiring value) — выражение, которое ссылается на объект, чьи ресурсы могут быть "украдены" (например, результат std::move).
  • glvalue (generalized lvalue) = lvalue + xvalue (все, у кого есть адрес).
  • rvalue = prvalue + xvalue (все, что можно перемещать).

Как это работает на практике

Рассмотрим ваш пример:

int x = 10;          // x — lvalue (есть адрес)
int y = x + 5;       // x+5 — prvalue (временный результат)
int&& z = std::move(x); // std::move(x) — xvalue

std::move(x) не перемещает данные, а просто преобразует lvalue в xvalue. xvalue — это "умирающее" значение: оно имеет адрес (как lvalue), но его можно переместить (как rvalue). Благодаря этому компилятор может выбрать конструктор перемещения вместо копирования.

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

std::string a = "hello";
std::string b = std::move(a); // a — lvalue, но std::move(a) — xvalue
// После этого a пуста (её строка перемещена в b)

Здесь std::move(a) — xvalue, потому что:

  • У a есть адрес (как у lvalue).
  • Но мы явно говорим компилятору: "это значение можно переместить" (как у rvalue).

Правила привязки ссылок:

  • lvalue-ссылка (T&) может привязаться только к lvalue.
  • rvalue-ссылка (T&&) может привязаться к rvalue (prvalue или xvalue).
int&& r1 = 42;           // 42 — prvalue, OK
int&& r2 = std::move(x); // xvalue, OK
int&  r3 = x;            // x — lvalue, OK
int&  r4 = 42;           // Ошибка: 42 — prvalue

Итоговая таблица:

КатегорияПримерИмеет адрес?Можно переместить?
lvaluex, a[0]ДаНет (без std::move)
prvalue42, x+y, std::string("temp")НетДа
xvaluestd::move(x), static_cast<int&&>(x)ДаДа
glvaluelvalue + xvalueДа—
rvalueprvalue + xvalue—Да

Таким образом, xvalue — это мост между lvalue и rvalue. Он позволяет "перемещать" объекты, которые обычно считаются lvalue, если вы явно разрешаете это через std::move.

<<>>

Альтернативный взгляд: думайте о времени жизни

Можно подойти к категориям с точки зрения того, когда объект будет уничтожен:

  • lvalue — объект живёт дольше текущего выражения (обычно до конца блока).
  • prvalue — объект умрёт в конце текущего выражения (если не привязан к ссылке).
  • xvalue — объект живёт, но мы "разрешаем" ему умереть (через std::move).

Например, временный объект std::string("temp") — prvalue. Если вы напишете:

std::string s = std::string("temp"); // prvalue, будет перемещён или скопирован

В C++11 компилятор может "украсть" ресурсы у временного объекта, потому что он всё равно будет уничтожен.

А если вы пишете:

std::string s2 = std::move(s); // xvalue — "умирающее" lvalue

Вы говорите: "я разрешаю переместить данные из s, даже если s ещё жив". После этого s остаётся в "пустом" состоянии.

Таким образом, prvalue — это временные объекты, которые не имеют имени, а xvalue — это именованные объекты, которые вы временно "превратили" во временные. Это помогает понять, почему move-семантика работает эффективно: вы явно указываете, что объект можно "разобрать на запчасти".