- 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
Нажмите здесь для печати.