Категория значения (value category) отвечает на вопрос, материален ли результат выражения — существует ли он в виде объекта в памяти. Материальная категория — lvalue, нематериальная — prvalue.

Зачем нужно

Без категорий непонятно, почему одним выражениям можно присвоить, а другим нет:

(x = y) = 5;  // компилируется, x == 5
x + y = 10;   // ошибка

Присваивание возвращает саму переменную x, а сложение — просто число, у которого нет места в памяти. Записывать некуда.

Как работает

Каждое выражение характеризуется двумя вещами: типом возвращаемого значения и категорией значения. Пусть изначально x == 5, строки выполняются по порядку:

ВыражениеТипПобочный эффектКатегория
x + yintнетprvalue
x = 5intв x хранится 5lvalue (x)
x *= 2intв x хранится 10lvalue (x)
y + (x *= 6)intв x хранится 60prvalue
x / 2.doubleнетprvalue

lvalue соответствует месту в памяти, поэтому может стоять слева от присваивания (за некоторыми исключениями) и быть операндом инкремента. Отсюда тексты ошибок:

5 = x;       // error: lvalue required as left operand of assignment
x * y = 10;  // error: lvalue required as left operand of assignment
++5;         // error: lvalue required as increment operand

Критерий: можно ли взять адрес

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

&x, &(x = 8), &(++x);   // lvalue: можно
&5, &(x + 8), &(x++);   // prvalue: нельзя
lvalueprvalue
имя переменной xлитерал 5
присваивание x = 8, x += 1арифметика x + 8
префиксный ++x, --xпостфиксный x++, x--
разыменование *p

Разыменование указателя возвращает lvalue — поэтому через *p можно присваивать.

Тонкости

Не из лекции

  • «Исключения» из правила «lvalue можно присвоить» — например, const-переменная: она lvalue, адрес взять можно, а присвоить нельзя.
  • Стандарт делит выражения на три категории: lvalue, prvalue и xvalue. Третья появится вместе с std::move и move-семантикой; общее название lvalue и xvalue — glvalue, prvalue и xvalue — rvalue.
  • Строковый литерал "abc" — исключение среди литералов: он lvalue, у него есть адрес.

Частые ошибки

  • Путать тип и категорию: x = 5 и x + 5 оба int, но категории разные.
  • Считать, что всё, что «содержит переменную», — lvalue. y + (x *= 6) — prvalue: снаружи сложение.
  • Ожидать, что x++ вернёт сам x.

Углубление — L05

Категория значения решает, к чему можно привязать ссылку:

void PrintX(const int& x);
void PrintRef(int& x);
 
int x = 0;
PrintX(x);    // x — lvalue: годится для обеих
PrintX(10);   // 10 — prvalue: const int& привязывается к временному объекту
PrintRef(10); // CE: int& к prvalue не привязывается

При PrintX(10) создаётся временный объект со значением 10, он живёт до конца всего выражения вызова. Лекция обещает, что подробные правила категорий понадобятся позже; пока достаточно этих двух примеров.

Не из лекции

Функция PrintRef придумана для контраста, в лекции её нет. Ошибка GCC 16.2 в похожем случае: cannot bind non-const lvalue reference of type 'float&' to an rvalue of type 'float'.