|
|||||||
Что такое lvalue и rvalue в языке C++
Время создания: 19.11.2017 19:38
Текстовые метки: c++, lvalue, rvalue, объяснение
Раздел: Компьютер - Программирование - Язык C++ (Си++)
Запись: xintrea/mytetra_syncro/master/base/1511109494bbprix49o1/text.html на raw.github.com
|
|||||||
|
|||||||
Перевод статьи "Lvalues and Rvalues", "Non-modifiable Lvalues", автор Dan Saks, Embedded Systems Programming [1]. C и C++ имеют тонкие различия по выражениям справа и слева от знака оператора присваивания (от знака "равно", т. е. "=", именуемого в англоязычной литературе assignment operator). Если Вы некоторое время программировали на языке 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? По общему мнению, если Вы пишете только на C, то можете работать дальше, не понимая, что по сути такое lvalues и rvalues. Многие программисты так и поступают. Но понимание lvalues и rvalues предоставляет ценную способность проникновения в суть поведения встроенных операторов, и кода, который генерирует компилятор для выполнения этих операторов. Если Вы программируете на C++, то понимание работы встроенных операторов дает обязательную основу для качественного написания перезагружаемых операторов (overloaded operators). Базовые концепции Керниган и Ричи ввели термин lvalue, чтобы отделить одни выражения от других. В своей книге "C Programming Language" (Prentice-Hall, 1988) они написали: "Объектом является манипулируемая область хранения; lvalue является выражением, ссылающимся на объект... имя 'lvalue' произошло из выражения присваивания E1 = E2, в котором левый операнд E1 должен быть выражением типа lvalue." Другими словами, левые и правые операнды в выражении присваивания являются самостоятельными выражениями. Для того, чтобы присваивание было допустимым, левый операнд должен ссылаться на объект, который должен быть типа 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 можно свести к тезисам:
И снова, как уже упоминалось, все это относится только к rvalue не классового (non-class) типа. Классы в C++ портят эти понятия еще более. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|