Категория значения (value category) отвечает на вопрос, материален ли результат выражения — существует ли он в виде объекта в памяти. Материальная категория — lvalue, нематериальная — prvalue.
Зачем нужно
Без категорий непонятно, почему одним выражениям можно присвоить, а другим нет:
(x = y) = 5; // компилируется, x == 5
x + y = 10; // ошибкаПрисваивание возвращает саму переменную x, а сложение — просто число, у
которого нет места в памяти. Записывать некуда.
Как работает
Каждое выражение характеризуется двумя вещами:
типом возвращаемого значения и категорией значения. Пусть изначально
x == 5, строки выполняются по порядку:
| Выражение | Тип | Побочный эффект | Категория |
|---|---|---|---|
x + y | int | нет | prvalue |
x = 5 | int | в x хранится 5 | lvalue (x) |
x *= 2 | int | в x хранится 10 | lvalue (x) |
y + (x *= 6) | int | в x хранится 60 | prvalue |
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: нельзя| lvalue | prvalue |
|---|---|
имя переменной 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'.