Что скрывается за буквами l, r, x, gl и pr? Разбираемся с категориями значений в C++
Если вы когда-нибудь писали на C++ код и получали ошибку вроде "cannot bind non-const lvalue reference to an rvalue", то вы уже сталкивались с этой «магией». Категории значений — lvalue, rvalue, xvalue, glvalue и prvalue — это не просто академическая классификация. Это фундамент, на котором держится семантика перемещения (move semantics) и идеальная передача (perfect forwarding).
Давайте раз и навсегда разберемся, что означает каждая буква, как их различать на практике и почему без этого знания невозможно писать современный эффективный C++.
Краткая история: откуда взялись эти пять терминов?
Изначально, ещё во времена C, было всего два понятия: lvalue (левое значение — то, что может стоять слева от присваивания) и rvalue (правое значение — то, что стоит справа). Всё было просто: переменная — lvalue, число — rvalue.
С появлением C++11 и семантики перемещения эта простая картина перестала работать. Появились ссылки на rvalue (&&) и необходимость различать «умирающие» временные объекты. Так родилась новая классификация, которую стандарт C++ описывает через пять категорий.
Взгляните на иерархию (диаграмма в виде текста):
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
Запомните эту картинку — это ключ ко всему.
lvalue (left value) — «живучие» объекты
lvalue — это выражение, которое ссылается на объект, имеющий адрес в памяти. Такой объект не исчезнет после окончания выражения. Вы можете взять его адрес, изменить его, и он продолжит существовать.
Примеры lvalue:
- Имя переменной:
int x = 5; x— это lvalue. - Разыменованный указатель:
*ptr. - Ссылка на переменную:
int& ref = x; ref. - Строковый литерал:
"hello"(да, это lvalue, хотя и константное).
Код:
int main() {
int a = 10;
int* p = &a; // Взятие адреса — ок, a — lvalue
int& ref = a; // Ссылка на lvalue
// &(a + 1); // Ошибка! a+1 — rvalue, адреса нет
return 0;
}
rvalue (right value) — «одноразовые» значения
rvalue — это выражение, которое не имеет постоянного адреса в памяти. Оно существует только в момент вычисления выражения. Вы не можете взять его адрес (за редкими исключениями).
К rvalue относятся:
- Литералы (кроме строковых):
42,3.14f,true. - Результаты арифметических операций:
a + b. - Временные объекты:
std::string("temp"). - Лямбда-выражения.
Код:
int foo() { return 5; }
int main() {
int x = 42; // 42 — rvalue
int y = x + 10; // x+10 — rvalue
// int* p = &(x + 10); // Ошибка! Нельзя взять адрес rvalue
return 0;
}
prvalue (pure rvalue) — «чистые» rvalue
prvalue — это классическое rvalue из C++98. Это временное значение, которое не имеет имени и не привязано к памяти. Именно prvalue мы обычно и называем rvalue в разговорной речи.
Примеры prvalue:
- Числа, перечисления, результаты операторов.
- Вызов функции, возвращающей не-ссылку:
std::to_string(123). - Временный объект, созданный явно:
MyClass().
xvalue (expiring value) — «умирающие» объекты
Вот тут начинается самое интересное. xvalue — это объект, который скоро будет уничтожен, но его ресурсы ещё можно «спасти» (переместить). Это мост между lvalue и rvalue.
Типичный способ получить xvalue — использовать std::move() или explicit cast к rvalue-ссылке.
Код:
#include <string>
#include <utility>
int main() {
std::string s = "Hello, world!";
std::string&& ref = std::move(s); // std::move(s) — xvalue!
// Теперь ref — это rvalue-ссылка, s — в перемещённом состоянии
return 0;
}
Важно: std::move(s) не перемещает данные. Он просто создаёт xvalue — «метку», которая говорит компилятору: «Эй, этот объект можно разобрать на запчасти». А уже конструктор перемещения или оператор присваивания решают, что делать.
glvalue (generalized lvalue) — обобщённое левое значение
glvalue — это объединение lvalue и xvalue. Простыми словами, это любое выражение, которое имеет идентичность (адрес). Независимо от того, умрёт оно завтра или нет, glvalue — это то, на что можно ссылаться по адресу.
Все lvalue — это glvalue. Все xvalue — тоже glvalue. А вот prvalue — это не glvalue.
Практика: как это работает в коде? Разбор типичных примеров
Пример 1. Перегрузка функций по категориям
#include <iostream>
void process(int& x) {
std::cout << "lvalue: " << x << std::endl;
}
void process(int&& x) {
std::cout << "rvalue: " << x << std::endl;
}
int main() {
int a = 10;
process(a); // lvalue → вызов process(int&)
process(20); // prvalue → вызов process(int&&)
process(std::move(a)); // xvalue → вызов process(int&&)
return 0;
}
Вывод:
lvalue: 10
rvalue: 20
rvalue: 10
Обратите внимание: std::move(a) превратило lvalue a в xvalue, и компилятор выбрал версию с rvalue-ссылкой.
Пример 2. Ошибка новичка: rvalue-ссылка как lvalue
<