Q&A: lvalue и rvalue

Автор: Павел Кузнецов
The RSDN Group
Опубликовано: 02.01.2004
Версия текста: 1.0
Что это такое lvalue и rvalue
modifiable lvalues, non-modifiable lvalues
lvalue-to-rvalue conversion
Критерии lvalue/rvalue

Что это такое lvalue и rvalue

Чтобы получить представление о понятиях lvalue и rvalue, проще всего рассмотреть, как выглядит некая переменная с точки зрения компилятора, так сказать "изнутри".

      int i = 10;

С переменной i связаны две вещи: адрес и значение объекта, который она обозначает. Пусть, для определенности, адрес i — 0x1000. Значение, помещенное по этому адресу, как мы видим, — число 10. Незаметно для нас компилятор попеременно использует то одно, то другое из этих двух чисел. Например:

      int j;
j = i;

В этом месте компилятор должен сгенерировать команду (или последовательность команд), которая означает следующее: «извлечь значение переменной i (число 10, записанное по адресу 0x1000) и записать его по адресу переменной j».

Или:

++i;

«Нарастить то, что находится по адресу переменной i (0x1000) на 1».

i = 20;

«Поместить по адресу 0x1000 значение 20».

В отличие от переменной i, с литералом 20 никакие адреса не связаны, только значение. В частности, выражения вида:

++20;
20 = 10;

никакого смысла в С или C++ не имеют. Таким образом, с любым выражением связаны либо адрес и значение, либо только значение.

Для того, чтобы отличать выражения, обозначающие объекты, от выражений, обозначающих только значения, ввели понятия lvalue и rvalue. Изначально слово lvalue использовалось для обозначения выражений, которые могли стоять слева от знака присваивания (left-value); им противопоставлялись выражения, которые могли находиться только справа от знака присваивания (right-value). Развитие языков C и C++ привело к утрате словом lvalue своего первоначального значения. Иногда lvalue трактуют также как locator value.

С каждой функцией компилятор также связывает две вещи: ее адрес и ее тело («значение»). При необходимости выражение, обозначающее функцию, компилятор может привести к указателю на эту функцию. «Значениями» функций в языках C и C++ оперировать нельзя. В спецификации языка C++ термин lvalue относится также и к выражениям, обознающим функции. В стандарте языка C для выражений, обозначающих функции, используется отдельный термин — functiondesignator.

Необходимо подчеркнуть, что lvalue/rvalue является свойством не объектов и/или функций, а выражений. Например:

      char a [10];

i      —   lvalue
++i    —   lvalue
*&i    —   lvalue
a[5]   —   lvalue
a[i]   —   lvalue

однако:

10     —   rvalue
i + 1  —   rvalue
i++    —   rvalue

modifiable lvalues, non-modifiable lvalues

Не все выражения, являющиеся lvalue, могут быть использованы для модификации обозначаемых ими объектов. Например, если t определена как "const int& t = i;", выражение t, будучи lvalue, не может быть использовано для модификации объекта, который оно обозначает. Такие выражения называют non-modifiable lvalues; им противопоставляют modifiablelvalues, которые могут быть использованы для модификации обозначаемых ими объектов.

lvalue-to-rvalue conversion

Очевидно, что если мы будем использовать любое из приведенных ранее lvalue-выражений в контексте, требующем значения объекта (например, в правой части операции присваивания), использование компилятором адресов будет ошибкой. Т.е., хотя выражение i является lvalue ("адрес и значение"), компилятор должен использовать соответствующее rvalue ("значение"). В терминах стандарта C++ это называется преобразованием lvalue к rvalue (lvalue-to-rvalue conversion).

Критерии lvalue/rvalue

В качестве критерия того, является ли некоторое выражение e lvalue, часто предлагают использовать возможность помещения этого выражения по левую сторону от знака присваивания, т.е. если выражение "e = . . ." допустимо, то e — lvalue. Однако этот критерий «не работает». Во-первых он не «пропускает» некоторые lvalues. Например, хотя выражения, состоящие только из имени массива или функции, являются lvalue, они не могут стоять по левую сторону от '='. Во-вторых он может ложно «диагностировать» некоторые rvalues как lvalues. Например, в C++ для rvalues классов можно вызывать функции члены, поэтому некоторые rvalues вполне могут находиться слева от '=':

      class C { };

int main()
{
  C() = C(); // OK
  return 0;
}

Данная программа, хотя и не слишком осмысленна, вполне «законна» с точки зрения спецификации языка C++. При этом выражение С(), стоящее слева от '=', по правилам языка C++ является rvalue,

Одним из более успешно «работающих» критериев может служить возможность применения к выражению операции взятия адреса, т.е. если выражение "&e" допустимо, то e — lvalue. В частности, этот критерий правильно «срабатывает» для большинства временных объектов и non-modifiable lvalues. Однако, в C++ и здесь не все гладко: если в классе C переопределена операция взятия адреса (унарная операция &) и при этом она помещена в секцию private, вне определения класса C или его «друзей» к lvalue-выражению, обозначающему объект типа C, унарная операция & будет неприменима:

      class C {
private:
  C* operator &() { return this; }
};

int main()
{
  C c;
  &c; // ERRORreturn 0;
}

И наоборот: если унарная операция & определена в секции public, к выражению C(), являющемуся rvalue, можно будет применять унарную операцию &:

      class C {
public:
  C* operator &() { return this; }
};

int main()
{
  &C(); // OKreturn 0;
}

В принципе, возможность применения к выражению встроенной операции получения адреса (унарная операция &) можно считать достаточным критерием того, что выражение является lvalue.

При наличии сомнений следует руководствоваться спецификацией языка. Вот перечисление некоторых характерных случаев:

rvalue:

lvalue:


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.