Категории значений и ссылки на них

В этом разделе мы представим вам и опишем несколько категорий значений (и ссылок на значения), которые существуют в C++

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

Вы наверняка уже слышали про значения lvalue и rvalue. Но в том контексте, который представлен в этом разделе, вы о них вряд ли думали.

Каждое выражение в C++ возвращает значение с одной из пяти названных выше категорий. Существуют аспекты языка C++ — его средства и правила, которые требуют правильного понимания этих категорий значений, а также ссылок на них. В качестве примеров таких аспектов можно назвать получение адреса значения, копирование значения, перенос значения и пересылка значение в другую функцию. В этом разделе не приводится подробное описание всех этих аспектов, но он содержит базовые сведения для ясного понимания принципа их работы.

Сведения в этом разделе представлены с точки зрения анализа категорий значений на основе двух независимых свойств идентификации и перемещаемости, предложенного Бьерном Страуструпом [Stroustrup, 2013].

У lvalue есть идентификатор

Что означает, что у значения есть идентификатор? Если у значения есть конкретный адрес, который вы можете получить и безопасно использовать, такое значение имеет идентичность. Это означает, что вы можете сравнивать не только содержимое значений, но и различать их по адресу в памяти.

У lvalue есть идентификатор. Сейчас этот вопрос представляет лишь исторический интерес, но "l" в имени lvalue представляет собой сокращение от "left" (слева) (то есть левая сторона присваивания). В C++ lvalue может отображаться в левой или правой части присваивания. Теперь буква "l" в имени lvalue уже не помогает понять назначение этого типа. Необходимо только понимать, что lvalue — это значение, которое имеет идентификатор.

Примеры выражений с lvalue: именованная переменная или константа; или функция, возвращающая ссылку. Примеры выражений, которые не содержат lvalue: временное значение или функция, возвращающая результат по значению.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

Идентичность есть не только у значений lvalue, но и у значений xvalue. Мы подробнее рассмотрим значения xvalue позже в этом разделе. А пока просто сообщим, что существует еще и категория значений glvalue (сокращение от "generalized lvalue" — обобщенное значение lvalue). Набор значений glvalue объединяет в себе как "классические" значения lvalue, так и значения xvalue. Поэтому, хотя утверждение, что у lvalue есть идентификатор, и верно, полный набор значений с идентификатором — это набор значений glvalue, как показано на этом рисунке.

An lvalue has identity

Значение rvalue является перемещаемым, а lvalue — нет

Но есть значения, которые не относятся к категории glvalue. Другими словами, существуют значения, для которых невозможно получить адрес в памяти (или нельзя гарантировать возможность его использовать). Мы видели несколько таких значений в приведенном выше примере кода.

Отсутствие надежного адреса в памяти можно счесть недостатком. Но у такого значения есть и важное преимущество  — эти данные можно переместить (что обычно не требует больших затрат), а не копировать (для этой операции затраты довольно высоки). Перемещение значения означает, что оно не сохраняется в прежнем месте. Поэтому следует избегать попыток обратиться к значениям, которые могут быть перемещены. Когда и как следует перемещать переменную выходит за рамки данного раздела. Для этой статьи нам достаточно знать, что значение с возможностью перемещения называется rvalue (или "классическое" значение rvalue).

Буква "r" в rvalue представляет собой сокращение от "right" (справа) (то есть правая сторона присвоения). Но значения rvalue, а также ссылки на rvalue, можно использовать и за пределами присвоений. Так что теперь на букву "r" в имени rvalue можно не обращать особого внимания. Необходимо только понимать, что rvalue — это значение, которое можно перемещать.

И наоборот, значение lvalue не является перемещаемым, как показано на этом рисунке. Возможность переместить lvalue напрямую противоречила бы основному определению типа lvalue. Такое перемещение создало бы в коде неожиданную проблему, нарушая возможность обращаться к lvalue по известному адресу.

An rvalue is movable; an lvalue is not

Итак, значение lvalue переместить невозможно. Но есть еще значение типа glvalue (набор значений с идентификатором), которое вы можете переместить, если, конечно, вы знаете, что делаете (в том числе достаточно внимательны, чтобы не обращаться к нему после перемещения) — это значение xvalue. Мы вернемся к этой идее позже в этой статье, когда перейдем к общей картине всех категорий.

Ссылки на rvalue и правила привязки ссылок

В этом разделе описывается синтаксис ссылки на rvalue. Нам придется подождать выпуска другого раздела, чтобы углубиться в суть перемещения и пересылки, но сейчас достаточно будет сказать что для решения этих проблем необходимы ссылки на rvalue. Прежде чем мы рассмотрим ссылки на rvalue, необходимо подробнее рассмотреть T& то, что ранее мы называли просто "ссылкой". Это действительно "ссылка на lvalue" (непостоянная), которая ссылается на значение, которое может записать пользователь ссылки.

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

Ссылку на lvalue можно привязать к lvalue, но не к rvalue.

Существуют постоянные ссылки на lvalue (T const&), которые ссылаются на объекты, в которые пользователь, использующий ссылку, не может записывать данные (например, константа).

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

Постоянную ссылку на lvalue можно привязать к lvalue или rvalue.

Синтаксис ссылки на rvalue типа T записывается как T&&. rvalue-ссылка указывает на перемещаемое значение, содержимое которого не нужно сохранять после использования (например, временное значение). Так как весь смысл состоит в перемещении (то есть изменении) значения, привязанного к ссылке на rvalue, квалификаторы const и volatile (также известные как CV-квалификаторы) не применяются к ссылкам на rvalue.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

Ссылка на rvalue привязывается к rvalue. На самом деле, с точки зрения разрешения перегрузки, rvalue предпочтительнее привязывать к ссылке на rvalue, чем к постоянной ссылке на lvalue. Но ссылку на rvalue невозможно привязать к lvalue, ведь, как мы сказали, ссылка на rvalue указывает на значение, содержимое которого не предполагается сохранять (например, параметр для конструктора перемещения).

Можно также передать значение rvalue туда, где ожидается передача аргумента по значению, используя конструкцию копирования (или перемещения, если значение rvalue представляет собой xvalue).

У значения glvalue имеется идентификатор, а у значения prvalue — нет

На этом этапе мы знаем, что такое идентификатор. И мы знаем, что может быть перемещено, а что — нет. Но мы еще не назвали набор значений, у которых нет идентификатора. Этот набор называется значениями prvalue, или чистыми значениями rvalue.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

A glvalue has identity; a prvalue does not

Полная картина категорий значений

Остается только объединить сведения и рисунки, приведенные выше, в одно большие изображение.

The complete picture of value categories

glvalue (i)

У glvalue (универсальное значение lvalue) имеется идентификатор. Мы будем использовать "i" как обозначение свойства "has identity" (имеет идентичность).

lvalue (i&!m)

У lvalue (относится к типу glvalue) имеется идентификатор, но его невозможно перемещать. Обычно это доступные для чтения и записи значения, передаваемые с помощью ссылки, постоянной ссылки или значения, если копирование не требует больших затрат. Невозможно привязать значение lvalue к ссылке на rvalue.

xvalue (i&m)

Значение xvalue (относящееся к типу glvalue, но также являющееся разновидностью rvalue) имеет идентификатор и может быть перемещено. Это может быть прежнее значение lvalue, которое вы решили переместить, так как копирование является ресурсоемким, и вы не допустите обращений к нему после этого. Вот как значение lvalue можно превратить в значение xvalue.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

В приведенном выше примере кода мы еще ничего не перемещали. Мы просто создали значение xvalue, приведя значение lvalue к типу неименованной rvalue-ссылки. Его по-прежнему можно идентифицировать по имени lvalue. А так как это значение xvalue, его можно перемещать. Зачем нам может понадобиться такое перемещение и как его выполнять — это тема для отдельной статьи. Однако можно считать, что "x" в имени xvalue означает "expert-only" (только для специалистов), если вам так проще. Приведя значение lvalue к xvalue (которое является подтипом rvalue, как вы помните), мы делаем возможной его привязку к rvalue-ссылке.

Ниже приведены другие примеры значений xvalue, вызов функции, возвращающей неименованную ссылку на rvalue, которая обращается к элементу xvalue.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

Значение prvalue ("чистое значение rvalue", относится к типу rvalue) не имеет идентификатора, но может быть перемещено. Обычно так используются временные значения, результаты по значению, полученные из вызова функции, или результаты вычисления другого выражения с типом, отличным от glvalue.

rvalue (m)

Значение rvalue допускает перемещение. Мы будем использовать "m" как обозначение свойства "is movable" (является перемещаемым).

Ссылка на rvalue всегда указывает на rvalue (значение, содержимое которого не предполагается сохранять).

Но является ли ссылка на rvalue самим значением rvalue? Неименованная ссылка на rvalue (см. примеры кода с xvalue выше) является значением xvalue, то есть она представляет собой значение rvalue. Ее предпочтительнее привязывать к параметру функции ссылки на rvalue, например, конструктора перемещения. И наоборот (что может противоречить логике), если у ссылки на rvalue есть имя, то выражение, состоящее из этого имени, является значением lvalue. Поэтому его невозможно привязать к параметру ссылки на rvalue. Но это легко обеспечить, нужно просто привести его еще раз к неименованной ссылке на rvalue (xvalue).

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

Мы еще не рассмотрели тип значения, у которого нет идентификатора и которое невозможно переместить. Но его можно проигнорировать, так как эта категория ничем не полезна для языка C++.

Правила сворачивания ссылок

Несколько ссылок & в выражении (ссылка на lvalue на ссылку на lvalue или ссылка на rvalue на ссылку rvalue) отменяют друг друга.

  • A& & сворачивается в A&.
  • A&& && сворачивается в A&&.

Несколько ссылок && в выражении сворачиваются в ссылку на lvalue.

  • A& && сворачивается в A&.
  • A&& & сворачивается в A&.

Пересылаемые ссылки

В заключительном разделе ссылки на rvalue, которые мы уже изучили, сравниваются с другим понятием — пересылаемыми ссылками. До изобретения термина "передаваемая ссылка" эта же концепция часто называлась "универсальная ссылка".

void foo(A&& a) { ... }
  • A&& является ссылкой на rvalue, как было показано ранее. Понятия "постоянный" и "переменный" не применяются к ссылкам на rvalue.
  • foo принимает только значения rvalue типа A.
  • Смысл ссылок на rvalue (например, A&&) состоит в том, что можно создать перегрузку, которая оптимизирована на случай передачи временного значения (или другого значения rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& — это пересылаемая ссылка. В зависимости от того, что передается в bar, типом _Ty может быть константа или не константа независимо от того, переменный он или нет.
  • bar принимает любое значение lvalue или rvalue типа _Ty.
  • При передаче значения lvalue пересылаемая ссылка принимает вид _Ty& &&, который сворачивается в ссылку на lvalue _Ty&.
  • При передаче значения rvalue передаваемая ссылка принимает вид rvalue-ссылки _Ty&&.
  • Пересылаемые ссылки (например, _Ty&&) предназначены не для оптимизации, а для того, чтобы можно было просто и эффективно переслать передаваемое значение. Вероятно, вы столкнетесь с пересылаемыми ссылками только в том случае, если будете создавать (или внимательно изучать) код библиотеки, например, стандартную функцию, которая пересылает аргументы конструктора.

Источники

  • [Страуструп, 2013 г] B. Б. Страуструп. Язык программирования C++, 4-е издание. Addison-Wesley. за 2013 год