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

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

  • glvalue
  • Lvalue
  • xlvalue
  • prvalue
  • r-значение

Вы, несомненно, слышали о lvalues и rvalues. Но вы можете не рассматривать их в тех понятиях, которые используются в этом разделе.

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

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

У l-значения есть идентичность

Что значит, что значение имеет идентичность? Если у вас есть (или вы можете получить) адрес в памяти, по которому хранится значение, и можете безопасно его использовать, то это значение обладает идентичностью. Таким образом, вы можете не только сравнивать содержимое значений, но и сравнивать или различать их по идентичности.

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

Примерами выражений, являющихся lvalue, являются именованная переменная, константа или функция, возвращающая ссылку. Примеры выражений, которые не являются l-значениями: временный объект или функция, возвращающая значение.

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 (для "обобщенного lvalue "). Множество glvalue является супермножеством как lvalue (также известных как классические lvalue), так и xvalue. Таким образом, хотя утверждение «lvalue обладает идентичностью» верно, полное множество сущностей, обладающих идентичностью, — это множество glvalue-выражений, как показано на этой иллюстрации.

l-значение имеет идентичность

r-значение можно перемещать; l-значение — нет

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

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

"r" в "rvalue" является сокращением "right" (как и в правой части назначения). Но вы можете использовать r-значения и ссылки на r-значения вне операций присваивания. Таким образом, «r» в «rvalue» — не то, на чём стоит заострять внимание. Необходимо только понять, что то, что мы называем rvalue, является значением, которое можно перемещать.

Значение lvalue, наоборот, не перемещается, как показано на этом рисунке. Если бы объект категории lvalue мог перемещаться, это противоречило бы самому определению lvalue. И это стало бы неожиданной проблемой для кода, который вполне обоснованно рассчитывал бы и дальше иметь доступ к lvalue-значению.

Значение rvalue можно перемещать; значение lvalue перемещать нельзя

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

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

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

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. Фактически, с точки зрения разрешения перегрузки, r-значение предпочитает привязку к ссылке на r-значение, а не к константной ссылке на l-значение. Но ссылка 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.
}

glvalue имеет идентичность; prvalue — нет

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

Остается объединить сведения и иллюстрации выше в один большой рисунок.

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

glvalue (i)

Glvalue (обобщённое lvalue) обладает идентичностью. Мы будем использовать «i» как сокращение для «имеет идентичность».

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» означает «только для экспертов», если это помогает. Приведя 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.

r-значение (m)

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

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

Но является ли сама 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-ссылки, которые мы уже обсуждали, сопоставляются с другим понятием — forwarding reference. До того, как термин "переадресация ссылки" был введен, некоторые люди использовали термин "универсальная ссылка".

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

Sources

  • [Stroustrup, 2013] B. Stroustrup: язык программирования C++, четвертый выпуск. Addison-Wesley. 2013.