- PVSM.RU - https://www.pvsm.ru -
Категории выражений, такие как lvalue и rvalue, относятся, скорее, к фундаментальным теоретическим понятиям языка C++, чем к практическим аспектам его использования. По этой причине многие даже опытные программисты достаточно смутно представляют себе, что они означают. В этой статье я постараюсь максимально просто объяснить значение этих терминов, разбавляя теорию практическими примерами. Сразу оговорюсь: статья не претендует на максимально полное и строгое описание категорий выражений, за подробностями я рекомендую обращаться непосредственно в первоисточник: Стандарт языка C++.
В статье будет довольно много англоязычных терминов, связано это с тем, что некоторые из них сложно перевести на русский, а другие переводятся в разных источниках по-разному. Поэтому я буду часто указывать англоязычные термины, выделяя их курсивом.
Термины lvalue и rvalue появились ещё в языке C. Стоит отметить, что путаница была заложена в терминологию изначально, потому как относятся они к выражениям (expressions), а не к значениям (values). Исторически lvalue – это то, что может быть слева (left) от оператора присваивания, а rvalue – то, что может быть только справа (right).
lvalue = rvalue;
Однако, такое определение несколько упрощает и искажает суть. Стандарт C89 определял lvalue как object locator, т.е. объект с идентифицируемым местом в памяти. Соответственно, всё, что не подходило под это определение, входило в категорию rvalue.
В языке C++ терминология категорий выражений достаточно сильно эволюционировала, в особенности после принятия Стандарта C++11, где вводились понятия rvalue-ссылок и семантики перемещения (move semantics). История появления новой терминологии интересно описана в статье Страуструпа “New” Value Terminology [1].
В основу новой более строгой терминологии легли 2 свойства:
Обладающие идентичностью выражения обобщены под термином glvalue (generalized values), перемещаемые выражения называются rvalue. Комбинации двух этих свойств определили 3 основные категории выражений:
| Обладают идентичностью | Лишены идентичности | |
|---|---|---|
| Не могут быть перемещены | lvalue | – |
| Могут быть перемещены | xvalue | prvalue |
На самом деле, в Стандарте C++17 появилось понятие избегание копирования (copy elision) – формализация ситуаций, когда компилятор может и должен избегать копирования и перемещения объектов. В связи с этим, prvalue не обязательно могут быть перемещены. Подробно и с примерами об этом можно почитать вот тут [2]. Впрочем, это не влияет на понимание общей схемы категорий выражений.
В современном Стандарте C++ структура категорий приводится в виде вот такой схемы:

Разберём в общих чертах свойства категорий, а также выражения языка, которые входят в каждую из категорий. Сразу отмечу, что приведённые ниже списки выражений для каждой категории не могут считаться полными, для более точной и подробной информации следует обратиться напрямую к Стандарту C++.
Выражения категории glvalue обладают следующими свойствами:
Выражения категории rvalue обладают следующими свойствами:
class A {
public:
A() = default;
A(const A&) { std::cout << "A::A(const A&)n"; }
A(A&&) { std::cout << "A::A(A&&)n"; }
};
.........
A a;
A b(a); // Вызывается A(const A&)
A c(std::move(a)); // Вызывается A(A&&)
Технически, A&& является rvalue и может использоваться для инициализации как константной lvalue-ссылки, так и rvalue-ссылки. Но благодаря этому свойству никакой неоднозначности нет, выбирается вариант конструктора, принимающий rvalue-ссылку.
Свойства:
&);К категории lvalue относятся следующие выражения:
void func() {}
.........
auto* func_ptr = &func; // порядок: получаем указатель на функцию
auto& func_ref = func; // порядок: получаем ссылку на функцию
int&& rrn = int(123);
auto* pn = &rrn; // порядок: получаем адрес объекта
auto& rn = rrn; // порядок: инициализируем lvalue-ссылку
=, +=, /= и т. д.), встроенные преинкремент и предекремент (++a, --b), встроенный оператор разыменования указателя (*p);a[n] или n[a]), когда один из операндов – lvalue массив;"Hello, world!".Строковый литерал отличается от всех остальных литералов в языке C++ именно тем, что является lvalue (хотя и неизменяемым). Например, можно получить его адрес:
auto* p = &”Hello, world!”; // тут константный указатель, на самом деле
Свойства:
К категории prvalue относятся следующие выражения:
42, true или nullptr;str.substr(1, 2), str1 + str2, it++) или выражение преобразования к нессылочному типу (например static_cast<double>(x), std::string{}, (int)42);a++, b--), встроенные математические операции (a + b, a % b, a & b, a << b, и т.д.), встроенные логические операции (a && b, a || b, !a, и т. д.), операции сравнения (a < b, a == b, a >= b, и т.д.), встроенная операция взятия адреса (&a);[](int x){ return x * x; }.Свойства:
Примеры выражений категории xvalue:
и в самом деле, для результата вызова std::move() нельзя получить адрес в памяти или инициализировать им ссылку, но в то же время, это выражение может быть полиморфным:
struct XA {
virtual void f() { std::cout << "XA::f()n"; }
};
struct XB : public XA {
virtual void f() { std::cout << "XB::f()n"; }
};
XA&& xa = XB();
auto* p = &std::move(xa); // ошибка
auto& r = std::move(xa); // ошибка
std::move(xa).f(); // выведет “XB::f()”
a[n] или n[a]), когда один из операндов – rvalue-массив.Для встроенного оператора запятая (comma operator) категория выражения всегда соответствует категории выражения второго операнда.
int n = 0;
auto* pn = &(1, n); // lvalue
auto& rn = (1, n); // lvalue
1, n = 2; // lvalue
auto* pt = &(1, int(123)); // ошибка, rvalue
auto& rt = (1, int(123)); // ошибка, rvalue
Вызовы функций, возвращающих void, выражения преобразования типов к void, а также выбрасывания исключений (throw) считаются выражениями категории prvalue, но их нельзя использовать для инициализации ссылок или в качестве аргументов функций.
Определение категории выражения a ? b : c – случай нетривиальный, всё зависит от категорий второго и третьего аргументов (b и c):
b или c имеют тип void, то категория и тип всего выражения соответствуют категории и типу другого аргумента. Если оба аргумента имеют тип void, то результат – prvalue типа void;b и c являются glvalue одного типа, то и результат является glvalue этого же типа;Для тернарного оператора определён целый ряд правил, по которым к аргументам b и c могут применяться неявные преобразования, но это несколько выходит за темы статьи, интересующимся рекомендую обратиться к разделу Стандарта Conditional operator [expr.cond].
int n = 1;
int v = (1 > 2) ? throw 1 : n; // lvalue, т.к. throw имеет тип void, соответственно берём категорию n
((1 < 2) ? n : v) = 2; // тоже lvalue, выглядит странно, но работает
((1 < 2) ? n : int(123)) = 2; // так не получится, т.к. теперь всё выражение prvalue
Для выражений вида a.m и p->m (тут речь о встроенном операторе ->) действуют следующие правила:
m – элемент перечисления или нестатический метод класса, то всё выражение считается prvalue (хотя ссылку таким выражением инициализировать не получится);a – это rvalue, а m – нестатическое поле нессылочного типа, то всё выражение относится к категории xvalue;Для указателей на члены класса (a.*mp и p->*mp) правила похожие:
mp – это указатель на метод класса, то всё выражение считается prvalue;a – это rvalue, а mp – указатель на поле данных, то всё выражение относится к xvalue;Битовые поля – удобный инструмент для низкоуровнего программирования, однако, их реализация несколько выпадает из общей структуры категорий выражений. Например, обращение к битовому полю вроде бы является lvalue, т. к. может присутствовать в левой части оператора присваивания. В то же время, взять адрес битового поля или инициализировать им неконстантную ссылку не получится. Константную ссылку на битовое поле инициализировать можно, но при этом будет создана временная копия объекта:
Bit-fields [class.bit]
If the initializer for a reference of type const T& is an lvalue that refers to a bit-field, the reference is bound to a temporary initialized to hold the value of the bit-field; the reference is not bound to the bit-field directly.
struct BF {
int f:3;
};
BF b;
b.f = 1; // OK
auto* pb = &b.f; // ошибка
auto& rb = b.f; // ошибка
Как я и упоминал во вступлении, приведённое описание не претендует на полноту, а лишь даёт общее представление о категориях выражений. Это представление позволит немного лучше понимать параграфы Стандарта и сообщения об ошибках компилятора.
Автор: igorsemenov
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/309905
Ссылки в тексте:
[1] “New” Value Terminology: http://www.stroustrup.com/terminology.pdf
[2] вот тут: https://medium.com/@barryrevzin/value-categories-in-c-17-f56ae54bccbe
[3] Источник: https://habr.com/ru/post/441742/?utm_campaign=441742
Нажмите здесь для печати.