Что такое rvalue, lvalue, xvalue, glvalue и prvalue в C++11?
Краткая теория: дерево категорий
В 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-семантикой:
- lvalue — копируем (не перемещаем).
- prvalue — можно перемещать (компилятор делает это автоматически).
- 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" в повседневной работе. Они нужны только авторам стандарта и разработчикам компиляторов. Для вас главное — различать, когда происходит копирование, а когда перемещение.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги