Программирование ARM Что такое lvalue и rvalue? Fri, December 06 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Что такое lvalue и rvalue? Печать
Добавил(а) microsin   

C и C++ проводят в жизнь тонкие различия по выражениям справа и слева от знака оператора присваивания (=, assignment operator). Здесь и далее перевод колонок "Lvalues and Rvalues", "Non-modifiable Lvalues", автор Dan Saks, Embedded Systems Programming [1].

Если Вы некоторое время программировали на языке C или на языке C++, то вероятно слышали о терминах lvalue (читается как "EL-value") и rvalue (читается как "AR-value"), потому что они иногда появляются в сообщениях об ошибках компилятора. Есть также некий шанс, что у Вас нет определенного понимания, что же это все-таки означает.

Большинство книг по C или C++ объясняют lvalues и rvalues не очень хорошо (я просмотрел дюжину книг, и не мог найти ни одно понравившееся мне объяснение). Причина может быть в том, что нет последовательного определения lvalue и rvalue даже среди языковых стандартов. Спецификация 1999 C Standard определяет lvalue не так, как спецификация 1989 C Standard, и каждая из них отличается от C++ Standard. Причем ни один из стандартов не дает четкого определения.

Учитывая неоднозначность в определениях для lvalue и rvalue среди языковых стандартов, я не подготовлен предложить точные определения. Однако могу объяснить базовую основу концепций, общую для всех стандартов.

lvalue-rvalue

Как часто бывает с малопонятными концепциями языка, Вы можете задать себе вопрос - зачем нужно заботиться о знании смысла lvalue и rvalue? По общему мнению, если Вы пишете только на C, то можете работать дальше, не понимая, что по сути такое lvalues и rvalues. Многие программисты так и поступают. Но понимание lvalues и rvalues предоставляет ценную способность проникновения в суть поведения встроенных операторов, и кода, который генерирует компилятор для выполнения этих операторов. Если Вы программируете на C++, то понимание работы встроенных операторов дает обязательную основу для качественного написания перезагружаемых операторов (overloaded operators).

[Базовые концепции]

Керниган и Ричи ввели термин lvalue, чтобы отделить одни выражения от других. В своей книге "C Programming Language" (Prentice-Hall, 1988) они написали: "Объектом является манипулируемая область хранения; lvalue является выражением, ссылающимся на объект... имя 'lvalue' произошло из выражения присваивания E1 = E2, в котором левый операнд E1 должен быть выражением типа lvalue."

The-C-Programming-Language

Другими словами, левые и правые операнды в выражении присваивания являются самостоятельными выражениями. Для того, чтобы присваивание было допустимым, левый операнд должен ссылаться на объект, который должен быть типа lvalue. Правый операнд может быть любым выражением, он не обязательно должен обладать свойствами lvalue. Например:

int n;

декларирует n как объект, имеющий тип int. Когда Вы используете n в выражении присваивания, типа:

n = 3;

n является выражением (точнее подвыражением выражения присваивания), ссылающимся на объект int. Выражение n является lvalue.

Предположим, что Вы переставили местами левый и правый операторы:

3 = n;

Если Вы не бывший программист языка Фортран, то очевидно это самая глупая вещь, которую можно сделать. Присвоение типа 3 = n пытается изменить значение целочисленной константы. К счастью, компиляторы C и C++ не допустят этого и выдадут ошибку. (Прим. переводчика: поскольку я иногда допускаю ошибку, когда нечаянно пишу вместо == знак присваивания =, то специально в операторе проверки == ставлю слева константу, чтобы компилятор сообщил об ошибке присваивания. Если же слева будет lvalue, то компилятор не заметит подвоха, и не сообщит об ошибке.) Основание отклонения такой операции - то, что левый операнд 3 в выражении не является lvalue. Он является rvalue и не ссылается на объект; он просто представляет собой некое значение.

Не знаю, откуда произошел термин rvalue. Ни один из стандартов C не использует его, кроме как в сноске, где указано, что "иногда стандарт описывает rvalue как 'значение выражения'".

Спецификация C++ Standard использует термин rvalue, косвенно его определяя в следующем высказывании: "Каждое выражение относится либо к lvalue, либо к rvalue.". Таким образом, rvalue - любое выражение, которое не является lvalue.

Цифровые литералы, такие как 3 и 3.14159, являются rvalue. К rvalue относятся и символьные литералы, такие как 'a'. Идентификатор, который относится к объекту, является lvalue, но идентификатор, именующий перечисляемую константу (enumeration constant), является rvalue. Например:

enum color { red, green, blue };
color c;
...
c = green;    // ok
blue = green; // ошибка

Второе присваивание вызовет ошибку при компиляции, потому что blue является rvalue.

Хотя Вы не можете использовать rvalue в качестве lvalue, Вы можете использовать lvalue в качестве rvalue. Например, определите:

int m, n;

Вы можете присвоить значение n объекту, который обозначен через m:

m = n;

Это выражение использует lvalue-выражение n в качестве rvalue. Строго говоря, компилятор выполняет то, что стандарт C++ Standard называет преобразование от lvalue к rvalue (lvalue-to-rvalue conversion), чтобы получить значение, сохраненное в объекте, который обозначен через n.

[Lvalue в других выражениях]

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

Например, оба операнда встроенного двоичного оператора + должны быть выражениями. Очевидно, у этих выражений должны быть подходящие типы. После преобразований оба выражения должны иметь одинаковый арифметический тип, или одно выражение должно иметь тип указателя, и другое должно иметь целый тип. Но любое из них может быть или lvalue или rvalue. таким образом, оба выражения x + 2 и 2 + x являются допустимыми.

Хотя операнды бинарного оператора + могут быть lvalue, результат у него всегда rvalue. Например, даны целые объекты m и n, и следующее выражение приведет к ошибке:

m + 1 = n;   //это выражение даст ошибку при компиляции

Оператор + имеет более высокий приоритет, чем оператор =. Таким образом, выражение присваивания эквивалентно следующему:

(m + 1) = n; //это выражение даст ошибку при компиляции

Ошибка происходит потому, что m + 1 является rvalue.

Как другой пример, унарный (одноместный) оператор & (взятие адреса) требует lvalue в качестве операнда. Поэтому &n является допустимым выражением только если n является lvalue. Таким образом, выражение типа &3 приведет к ошибке. Число 3 не ссылается на объект, так что адрес его получить нельзя.

Хотя унар & требует lvalue в качестве операнда, результат его будет rvalue. Пример:

int n, *p;
...
p = &n; // ok
&n = p; // ошибка: &n является rvalue

В отличие от унара &, унар * в качестве результата выдает lvalue. Ненулевой указатель p всегда указывает на объект, поэтому *p является lvalue. Пример:

int a[N];
int *p = a;
...
*p = 3; // ok

Хотя результат унара * является lvalue, его операнд может быть rvalue, как тут:

*(p + 1) = 4; // ok

[Хранилище данных для rvalue]

По базовой концепции rvalue является просто значением, оно не ссылается на объект. На практике rvalue может ссылаться на объект. Просто необязательно, что rvalue ссылается на объект. Поэтому и C, и C++ настаивают, чтобы Вы программировали, как будто rvalue не ссылаются на объекты.

Предположение, что rvalue не ссылается на объект, дает компиляторам C и C++ значительную свободу в генерации кода для выражений rvalue. Предположим, что есть присваивание такого вида:

n = 1;

Здесь n является целым (int). Компилятор может сгенерировать именованное хранилище данных, инициализированное значением 1, как будто бы 1 было lvalue. Это сгенерировало бы код для копирования из инициализированного хранилища в место хранения, выделенного для n. На языке ассемблера это могло бы выглядеть так:

one:  .word 1
      ...
      mov (one), n

Многие процессоры предоставляют инструкции (команды ассемблера) для непосредственной адресации операнда (immediate operand addressing), в котором операнд источника может быть частью инструкции, а не отдельными данными. На языке ассемблера это может выглядеть так:

      mov #1, n

В этом случае rvalue 1 никогда не появляется как объект в пространстве данных. Скорее он появляется как инструкции в пространстве кода.

На некоторых процессорах самый быстрый способ поместить значение 1 в объект - очистить его и затем инкрементировать, например так:

      clr n
      inc n

Очистка объекта устанавливает его содержимое в 0. Инкрементирование дает 1. И все же данные, представляющие величины 0 и 1, нигде не появляются в коде.

Хотя верно, что rvalue на языке C не ссылаются на объекты, для языка C++ это не так. В C++ rvalue, имеющие тип класса, ссылаются на объекты, но они все еще не lvalue. Таким образом все, что я уже сказал для rvalue верно, пока мы не имеем дело с rvalue типа класса.

[Немодифицируемые lvalue]

Хотя lvalue обозначают объекты, не все lvalue могут появляться как левая часть оператора присваивания. Как известно, выражение является последовательностью операторов и операндов, которые задают некие вычисления. Вычисления могут давать в результате значение, или производить сторонние эффекты (side effects). Выражение присваивание имеет форму

e1 = e2

где e1 и e2 сами по себе выражения. Правый операнд e2 может быть любым выражением, однако левый операнд должен быть выражением lvalue, он должен ссылаться на объект (об этом говорилось ранее).

Хотя lvalue получило свое имя от вида выражения, которое должно появиться в левой части оператора присваивания, Керниган и Ричи определили его не от этого. В первой редакции книги "C Programming Language" (Prentice-Hall, 1978) они определили lvalue как выражение, ссылающееся на объект.". В тот момент времени набор выражений, относящийся к объектам, был точно тем же самым набором выражений, которые имели право появляться в левой части оператора присваивания. Но это было до того, как квалификатор const стал частью C и C++.

Ключевое слово const делает базовое понятие lvalue неадекватным семантике выражений. Мы должны быть в состоянии отличить друг от друга разные виды lvalue.

Как уже упоминалось, оператор присваивания = является не единственным, который требует lvalue в качестве операнда. Унарный оператор взятия адреса & также требует lvalue как собственного операнда. Мало того, что каждый операнд является или lvalue или rvalue, но и каждый оператор дает в результате или lvalue, или rvalue. Принимая все это во внимание, посмотрим, как квалификатор const усложняет понятие lvalue.

Квалификатор const появляется в декларации, и модифицирует тип в декларации, или некоторую часть её, например:

int const n = 127;

Здесь декларируется объект типа "const int" (постоянное целое). Выражение n ссылается на объект, как будто там нет константы, за исключением того, что n относится к такому объекту, который программа поменять не может. Например, присваивание типа такого выдаст ошибку компиляции:

n = 0; // ошибка, нельзя модифицировать n
++n;   // ошибка, нельзя модифицировать n

Как выражение, ссылающееся на const object, такой как n, чем-то отличается от rvalue? В конце концов, если переписать эти выражения для литерального целого числа вместо n. то получим ту же самую ошибку:

7 = 0; // ошибка, нельзя модифицировать литерал
++7;   // ошибка, нельзя модифицировать литерал

Если Вы больше не можете модифицировать n, как будто это rvalue, то почему нельзя сказать, что n также является rvalue? Различие состоит в том, что Вы можете получить адрес от const object, но получить адрес от литерального целого нельзя. Например:

int const *p;
...
p = &n; // ok
p = &7; // ошибка

Заметьте, что p, объявленный чуть выше, должен быть указателем именно на const int. Если Вы пропустите ключевое слово const в объявлении указателя, например напишете так:

int *p;

то тогда следующее присваивание выдаст ошибку:

p = &n; // ошибка, недопустимое преобразование

Когда Вы получаете адрес от объекта const int, то получаете значение типа "указатель на const int," которое нельзя преобразовать в "указатель на int" без использования приведения типа (cast). Вот пример такого приведения типа:

p = (int *)&n; // как бы ok, если мы в здравом уме и понимаем, что делаем

Поскольку cast принуждает компилятор не жаловаться на преобразование, то делать нечто подобное нежелательно, иначе это может стать причиной трудно находимых ошибок.

Таким образом выражение, которое относится к объекту const, действительно является lvalue, а не rvalue. Однако это специальный вид lvalue, называемый немодифицируемый lvalue (non-modifiable lvalue), который нельзя использовать для модифицирования объекта, на который ссылается lvalue. Это в отличие от поддающегося изменению lvalue (который декларирован без const), который можно использовать для изменения объекта, на который ссылается lvalue.

Поскольку теперь может влиять фактор наличия ключевого слова const, то больше не будет точным называть левую часть оператора присваивания, как lvalue. Скорее, его нужно назвать возможным для изменения lvalue (modifiable lvalue). Фактически, каждый арифметический оператор присваивания, такой как += и *=, требует модифицируемого lvalue в качестве левого операнда. Для всех скалярных типов:

x += y;    // арифметическое присваивание

является эквивалентом

x = x + y; // простое присваивание

за исключением того, что x оценивается только один раз. Поскольку x в этом арифметическом присваивании должен быть модифицируемым lvalue, также должно быть и в простом присваивании. Не каждый оператор, который требует операнда lvalue, требует именно модифицируемого lvalue. Унарный оператор & принимает в качестве операнда как modifiable lvalue, так и non-modifiable lvalue. Например, если задано:

int m;
int const n = 10;

то &m является допустимым выражением, которое вернет тип "указатель на int", и &n будет также допустимым выражением, возвращающим результат "указатель на const int".

[Что же на самом деле является немодифицируемым]

Ранее я говорил, что non-modifiable lvalue это lvalue, который Вы не можете использовать, чтобы изменить объект. Заметьте, что я не говорил, что non-modifiable lvalue относится к объекту, который Вы не можете изменить - я сказал, что Вы не можете использовать lvalue, чтобы изменить объект. (Примечание переводчика: как я люблю эти извращенские головоломки языка C!) Различие тонкое, но тем не менее важное, как будет показано в следующем примере. Предположим:

int n = 0;
int const *p;
...
p = &n;

Тут мы имеем, что p указывает на n, так что и *p, и просто n являются двумя разными выражениями, ссылающимися на один и тот же объект. Однако, *p и n имеют разные типы. Как было рассказано в заметке "What const Really Means" (Что на самом деле означает const) [1], присваивание использует конверсию квалификации, чтобы преобразовать значение типа "указатель на int" в значение типа "указатель на const int". Выражение n имеет тип "не const int". Это модифицируемое lvalue, так что Вы можете модифицировать объект, на которое оно указывает:

n += 2;

С другой стороны, p имеет тип "указатель на const int", поэтому *p имеет тип const int. Выражение *p является немодифицируемым lvalue, и Вы не можете использовать *p для того, чтобы модифицировать n (даже если Вы можете использовать для модификации выражение n):

*p += 2; //ошибка!

Такова семантика определения константы const в C и C ++.

[Общие выводы]

Каждое выражение C и C++ является либо lvalue, либо rvalue. Lvalue является выражением, которое обозначает объект (ссылается на него с указателем или без). Каждое lvalue бывает, в свою очередь, или модифицируемым, или немодифицируемым. К rvalue относится любое выражение, которое не является lvalue. Оперативно различия между rvalue и lvalue можно свести к тезисам:

- modifiable lvalue является адресуемым (может быть операндом унара &) и присваиваемым (может являться левым операндом =).
- non-modifiable lvalue также является адресуемым. Но оно не является присваиваемым.
- rvalue не является ни адресуемым, ни присваиваемым.

И снова, как уже упоминалось, все это относится только к rvalue не классового (non-class) типа. Классы в C++ портят эти понятия еще более.

[Ссылки]

1. Lvalues and Rvalues site:embedded.com.
2. Non-modifiable Lvalues site:embedded.com.

 

Комментарии  

 
0 #11 Морган 26.04.2021 07:41
Цитирую Морган:
Спасибо за ответ.
int a = 10;
int *b;
*b=a;
Получается здесь разыменование указателя вернет значение по адресу в b?. Что то мне так не кажется. Но так как любая операция имеет значение, то какое в данном случае разыменование имеет значение?

microsin: показанный Вами код недопустим без присваивания значения указателю b. Компилятор предупредит об этом. Вот так можно: b = &переменная. Как вариант: b = &a.


Хорошо пусть так. Ошибся я. Вы указали на ошибку, но не ответили на основной вопрос, к сожалению :sad: .
int a=10;
int *b;

b=&a;
*b=15;
Какое значение имеет операция разыменования в данном случае? Разве тут будет возврат значения по адресу в b?

microsin: кто бы объяснил, о чем спрашиваете... Смысл следующий, если правильно понял вопрос: сначала указателю b присваивается адрес памяти, где находится переменная a (которая пока что равна 10). Во второй операции с помощью разименования (не нравится мне этот термин, но ничего не поделаешь) по адресу из b записывается 15. В результате значение переменной a становится равным 15. Теперь по второму вопросу, опять-таки если правильно понял. Выражение *b = 15 действительно возвращает значение по адресу в b. Например, совершенно справедливо выражение int c = *b = 15.
Цитировать
 
 
0 #10 Морган 25.04.2021 06:58
Цитирую Морган:
Может я задаю глупый вопрос. Но какое значение возвращает операция разыменования, когда мы осуществляем присваивание по адресу в указателе?

microsin: если правильно понял вопрос: операция разыменования возвращает значение по адресу, как с присваиванием, так и без.

Спасибо за ответ.
int a = 10;
int *b;
*b=a;
Получается здесь разыменование указателя вернет значение по адресу в b?. Что то мне так не кажется. Но так как любая операция имеет значение, то какое в данном случае разыменование имеет значение?

microsin: показанный Вами код недопустим без присваивания значения указателю b. Компилятор предупредит об этом. Вот так можно: b = &переменная. Как вариант: b = &a.
Цитировать
 
 
0 #9 Морган 23.04.2021 14:54
Может я задаю глупый вопрос. Но какое значение возвращает операция разыменования, когда мы осуществляем присваивание по адресу в указателе?

microsin: если правильно понял вопрос: операция разыменования возвращает значение по адресу, как с присваиванием, так и без.
Цитировать
 
 
0 #8 alk 26.07.2018 13:04
Цитирую ВОВА:
Простая локальная переменная целого типа может быть регистровой. Теоретически адреса у неё никакого нет, но она от этого не перестаёт быть lvalue.

(1). Всякое взятие адреса заставляет компилятор размещать её в памяти, когда можно было бы разместить в регистре; или же
(2) компилятор перепишет всякую функцию function(int* x) вызываемую по отношению к этой переменной так чтобы она работала не с указателем а с регистром?

Не надо мешать в кучу семантику и оптимизацию. Семантика первостепенна, оптимизация вторична. В зависимости от компилятора оптимизации могут различаться, вплоть до полного удаления блока кода, если это возможно. Семантика же неизменна.
Цитировать
 
 
0 #7 Владимир 21.04.2017 10:59
"Поскольку cast принуждает компилятор жаловаться на преобразование, то делать нечто подобное нежелательно, иначе это может стать причиной трудно находимых ошибок."

Очевидно, надо читать "не жаловаться"?

microsin: благодарю, исправил.
Цитировать
 
 
0 #6 Владимир 21.04.2017 10:56
"Если Вы больше не можете модифицировать n, как будто это rvalue, то почему нельзя сказать, что n также является rvalue?"

очевидно, вместо первого rvalue надо читать lvalue?

microsin: не знаю, возможно я неправильно перевел. В оригинале было вот так: "You can't modify n any more than you can an rvalue, so why not just say n is an rvalue, too?"
Цитировать
 
 
-6 #5 Maxim 05.02.2017 19:24
Вы говорите, что нельзя взять адрес у rvalue операнда? Но это не так: char *p = &"Hello, world"; :lol:
Цитировать
 
 
+7 #4 Алекс 15.05.2016 22:05
Спасибо за подробное описание. Очень хорошее разъяснение.
Цитировать
 
 
0 #3 xanm 06.11.2015 15:58
Отличная статья :-)
Долго пытаюсь найти внятное описание этих терминов и вот наконец.
Цитировать
 
 
0 #2 ВОВА 06.07.2015 18:42
Простая локальная переменная целого типа может быть регистровой. Теоретически адреса у неё никакого нет, но она от этого не перестаёт быть lvalue.

(1). Всякое взятие адреса заставляет компилятор размещать её в памяти, когда можно было бы разместить в регистре; или же
(2) компилятор перепишет всякую функцию function(int* x) вызываемую по отношению к этой переменной так чтобы она работала не с указателем а с регистром?
Цитировать
 

Добавить комментарий


Защитный код
Обновить

Top of Page