Что такое lvalue в с
Понимание lvalue и rvalue в C и С++
Привет, Хабр! Представляю вашему вниманию перевод статьи Eli Bendersky, Understanding of lvalues and rvalues in C and C++.
От переводчика: предлагаю Вашему вниманию перевод интересной статьи об lvalue и rvalue в языках C/C++. Тема не нова, но знать об этих понятиях никогда не поздно. Статья рассчитана на новичков, либо на программистов переходящих с C (или других языков) на C++. Поэтому будьте готовы к подробному разжёвыванию. Если вам интересно, добро пожаловать под кат
Термины lvalue и rvalue не являются чем-то таким, с чем часто приходится сталкиваться при программировании на C/C++, а при встрече не сразу становится ясным, что именно они означают. Наиболее вероятное место столкнуться с ними — это сообщения компилятора. Например, при компиляции следующего кода компилятором gcc :
Вы уведите нечто следующее:
Согласен, что этот код немного надуманный, и вряд ли Вы будете писать нечто подобное, однако сообщение об ошибке упоминает lvalue — термин, который не так часто увидишь в туториалах по C/C++. Другой пример нагляден при компиляции следующего кода при помощи g++ :
Вы увидите следующую ошибку:
Опять же, в сообщение об ошибке упоминается мистическое rvalue. Что же в C и C++ понимается под lvalue и rvalue? Это и есть тема данной статьи.
Простое определение
Для начала нарочито дадим определения lvalue и rvalue в упрощённой форме. В дальнейшем эти понятия будут рассмотрены под увеличительным стеклом.
lvalue (locator value) представляет собой объект, который занимает идентифицируемое место в памяти (например, имеет адрес).
rvalue определено путём исключения, говоря, что любое выражение является либо lvalue, либо rvalue. Таким образом из определения lvalue следует, что rvalue — это выражение, которое не представляет собой объект, который занимает идентифицируемое место в памяти.
Элементарные примеры
Термины, определённые выше, могут показаться немного нечёткими. Поэтому стоит сразу рассмотреть несколько простых поясняющих примеров. Предположим, мы имеем дело с переменной целого типа:
Оператор присваивания ожидает lvalue с левой стороны, и var является lvalue, потому что это объект с идентифицируемым местом в памяти. С другой стороны, следующие заклинания приведут к ошибкам:
Однако, не все присваивания результату вызова функции ошибочны. Например, использование ссылок в C++ делает это возможным:
Здесь foo возвращает ссылку, которая является lvalue, то есть ей можно придать значение. Вообще, в C++ возможность возвращать lvalue, как результат вызова функции, существенна для реализации некоторых перегруженных операторов. Как пример приведём перегрузку оператора [] в классах, которые реализуют доступ по результатам поиска. Например std::map :
Присваивание mymap[10] работает, потому что неконстантная перегрузка std::map::operator[] возвращает ссылку, которой может быть присвоено значение.
Изменяемые lvalue
Таким образом не всем lvalue можно присвоить значение. Те, которым можно, называются изменяемые lvalue (modifiable lvalues). Формально C99 стандарт определяет изменяемые lvalue как:
Преобразования между lvalue и rvalue
Образно говоря, конструкции языка, оперирующие значениями объектов, требуют rvalue в качестве аргументов. Например, бинарный оператор ‘+’ принимает два rvalue в качестве аргументов и возвращает также rvalue:
Как мы уже видели раньше, a и b оба lvalue. Поэтому в третьей строке они подвергаются неявному преобразованию lvalue-в-rvalue. Все lvalue, которые не являются массивом, функцией и не имеют неполный тип, могут быть преобразованы в rvalue.
Что насчёт преобразования в другую сторону? Можно ли преобразовать rvalue в lvalue? Конечно нет! Это бы нарушило суть lvalue, согласно его определению (Отсутствие неявного преобразования означает, что rvalue не могут быть использованы там, где ожидается lvalue).
Это не означает, что lvalue не могут быть получены из rvalue явным способом. Например, унарный оператор ‘*’ (разыменование) принимает rvalue в качестве аргумента, но возвращает lvalue в качестве результата. Рассмотрим следующий верный код:
Обратно, унарный оператор ‘&’ (адрес) принимает lvalue как аргумент и производит rvalue:
Символ «&» играет несколько другую роль в C++ — он позволяет определить ссылочный тип. Его называют «ссылкой на lvalue». Неконстантной ссылке на lvalue не может быть присвоено rvalue, так как это потребовало бы неверное rvalue-в-lvalue преобразование:
Константным ссылкам на lvalue можно присвоить rvalue. Так как они константы, значение не может быть изменено по ссылке и поэтому проблема модификации rvalue просто отсутствует. Это свойство делает возможным одну из основополагающих идиом C++ — допуск значений по константной ссылке в качестве аргументов функций, что позволяет избежать необязательного копирования и создания временных объектов.
CV-специфицированные rvalues
lvalue (3.10) на тип T, не являющимся функциональным, или массивом, может быть преобразован в rvalue. [. ] Если T не класс, типом rvalue является cv-неспецифицированная версия типа T. Иначе, типом rvalue является T.
Так что же значит «cv-неспецифицированный»? CV-спецификатор — это термин, используемый для описания const и volatile спецификаторов типа.
Каждый тип, который является cv-неспецифицированным полным или неполным объектным типом или типом void (3.9), имеет соответственно три cv-специфицированные версии: тип со спецификатором const, тип со спецификатором volatile и тип со спецификаторами const volatile. [. ] CV-специфицированные и cv-неспецифицированные типы являются различными, однако они имеют одинаковое представление и требования по выравниванию.
Ссылки на rvalue (C++11)
Ссылки на rvalue и сопутствующий концепт семантики переноса являются одним из наиболее мощным инструментом, добавленным в язык C++11. Подробная дискуссия на эту тему выходит за рамки этой скромной статьи (вы можете найти кучу материала, просто погуглив «rvalue references». Вот некоторые ресурсы, которые я нахожу полезными: этот, этот и особенно вот этот), но всё же я хотел бы привести простой пример, потому что считаю, что данная глава является наиболее подходящим место, чтобы продемонстрировать как понимание lvalue и rvalue расширяет наши возможности рассуждать о нетривиальных концепциях языка.
Добрая половина статьи была потрачена на объяснение того, что одним из самых главных различий между lvalue и rvalue является тот факт, что lvalue можно изменять, в то время как rvalue — нет. Что же, C++11 добавляет одну важнейшую характерную особенность в этом различии, разрешая нам иметь ссылки на rvalue и тем самым изменять их в некоторых случаях.
Как пример рассмотрим наипростейшую реализацию динамического массива целых чисел. Давайте посмотрим лишь на методы относящиесе к теме данной главы:
Давайте запустим простой код, который копирует содержимое v1 в v2 :
И вот, что мы увидим:
Что совершенно логично, так как это точно отражает, что происходит внутри оператора присваивания. Но давайте предположим, что мы хотим присвоить v2 некоторое rvalue:
Хотя здесь я только присваиваю значение свеже созданному вектору, это является одной из демонстраций общего случая, когда некоторое временное rvalue создаётся и присваивается v2 (это может случится например, если функция возвращает вектор). Вот что мы увидим на экране:
Ого! Выглядит очень хлопотно. В частности, здесь потребовалась дополнительная пара вызовов конструктора с деструктором, чтобы создать, а потом удалить временный объект. И это печально, так как внутри копирующего оператора присваивания другой временный объект создаётся и удаляется. Дополнительная работа за зря.
Но, нет! C++11 даёт нам ссылки на rvalue, с помощью которых можно реализовать «семантику переноса», а в частности «переносящий оператор присваивания» (теперь понятно почему я всё время называл operator= копирующим оператором присваивания. В C++11 эта разница становится важной). Давайте добавим другой operator= в IntVec :
Двойной асперсанд — это ссылка на rvalue. Он означает как раз то, что и обещает — даёт ссылку на rvalue, который будет уничтожен после вызова. Мы можем использовать этот факт, чтобы просто «стащить» внутренности rvalue — они ему всё равно не нужны! Вот, что выведется на экран:
Хочу только отметить ещё раз, что этот пример только вершина айсберга семантики переноса и ссылок на rvalue. Как вы можете догадаться, это сложная тема с множеством частных случаев и загадок. Я пытался лишь продемонстрировать очень интересное применение различий между lvalue и rvalue в C++. Компилятор очевидно может их различать и позаботится о вызове правильного конструктора во время компиляции.
Заключение
Можно написать много C++ кода, не задумываясь о разногласиях rvalue и lvalue, опуская их как непонятный жаргон компилятора в сообщениях об ошибках. Однако, как я пытался показать в этой статье, лучшее владение этой темы обеспечит более глубокое понимание определённых конструкций C++, и сделает части стандарта C++ и дискуссии между экспертами языка для вас более доступными.
В стандарте C++11 эта тема является ещё более важной, так как C++11 вводит понятия ссылок на rvalue и семантики переноса. Чтобы действительно понять новые особенности языка, строгое понимание rvalue и lvalue просто необходимо.
Что такое rvalue и lvalue?
2 ответа 2
glvalue (“generalized” lvalue)
Выражение, чьё вычисление определяет сущность объекта, битового поля или функции.
prvalue (“pure” rvalue)
xvalue (“eXpiring” lvalue)
Это glvalue, которое обозначает объект или битовое поле, чьи ресурсы могут быть повторно использованы (обычно потому, что они находятся около конца своего времени жизни). Например, результат вызова std::move даёт выражение xvalue.
lvalue
Это glvalue, которое не является xvalue. Например, имя переменной, функции или члена-данных, независимо от их типа, даже переменная, имеющая тип rvalue-ссылки, образует выражение lvalue.
rvalue
— A glvalue is an expression whose evaluation determines the identity of an object, bit-field, or function.
— A prvalue is an expression > whose evaluation initializes an object or a bit-field, or computes the value of the operand of an operator, as specified by the context in which it appears.
— An xvalue is a glvalue that denotes an object or bit-field whose resources can be reused (usually because it is near the end of its lifetime).
— An lvalue is a glvalue that is not an xvalue.
— An rvalue is a prvalue or an xvalue.
//.
[Note: Historically, lvalues and rvalues were so-called because they could appear on the left- and right-hand side of an assignment (although this is no longer generally true); glvalues are “generalized” lvalues, prvalues are “pure” rvalues, and xvalues are “eXpiring” lvalues. Despite their names, these terms classify expressions, not values. — end note]
Что такое lvalue в с
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, чтобы отделить одни выражения от других. В своей книге «C Programming Language» (Prentice-Hall, 1988) они написали: «Объектом является манипулируемая область хранения; lvalue является выражением, ссылающимся на объект. имя ‘lvalue’ произошло из выражения присваивания E1 = E2, в котором левый операнд E1 должен быть выражением типа lvalue.»
Другими словами, левые и правые операнды в выражении присваивания являются самостоятельными выражениями. Для того, чтобы присваивание было допустимым, левый операнд должен ссылаться на объект, который должен быть типа lvalue. Правый операнд может быть любым выражением, он не обязательно должен обладать свойствами lvalue. Например:
декларирует n как объект, имеющий тип int. Когда Вы используете n в выражении присваивания, типа:
n является выражением (точнее подвыражением выражения присваивания), ссылающимся на объект int. Выражение n является lvalue.
Предположим, что Вы переставили местами левый и правый операторы:
Не знаю, откуда произошел термин rvalue. Ни один из стандартов C не использует его, кроме как в сноске, где указано, что «иногда стандарт описывает rvalue как ‘значение выражения'».
Цифровые литералы, такие как 3 и 3.14159, являются rvalue. К rvalue относятся и символьные литералы, такие как ‘a’. Идентификатор, который относится к объекту, является lvalue, но идентификатор, именующий перечисляемую константу (enumeration constant), является rvalue. Например:
Второе присваивание вызовет ошибку при компиляции, потому что blue является rvalue.
Хотя Вы не можете использовать rvalue в качестве lvalue, Вы можете использовать lvalue в качестве rvalue. Например, определите:
Вы можете присвоить значение n объекту, который обозначен через m:
Это выражение использует 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 является rvalue.
Как другой пример, унарный (одноместный) оператор & (взятие адреса) требует lvalue в качестве операнда. Поэтому &n является допустимым выражением только если n является lvalue. Таким образом, выражение типа &3 приведет к ошибке. Число 3 не ссылается на объект, так что адрес его получить нельзя.
Хотя унар & требует lvalue в качестве операнда, результат его будет rvalue. Пример:
В отличие от унара &, унар * в качестве результата выдает lvalue. Ненулевой указатель p всегда указывает на объект, поэтому *p является lvalue. Пример:
Хотя результат унара * является lvalue, его операнд может быть rvalue, как тут:
[Хранилище данных для rvalue]
По базовой концепции rvalue является просто значением, оно не ссылается на объект. На практике rvalue может ссылаться на объект. Просто необязательно, что rvalue ссылается на объект. Поэтому и C, и C++ настаивают, чтобы Вы программировали, как будто rvalue не ссылаются на объекты.
Предположение, что rvalue не ссылается на объект, дает компиляторам C и C++ значительную свободу в генерации кода для выражений rvalue. Предположим, что есть присваивание такого вида:
Здесь n является целым (int). Компилятор может сгенерировать именованное хранилище данных, инициализированное значением 1, как будто бы 1 было lvalue. Это сгенерировало бы код для копирования из инициализированного хранилища в место хранения, выделенного для n. На языке ассемблера это могло бы выглядеть так:
Многие процессоры предоставляют инструкции (команды ассемблера) для непосредственной адресации операнда (immediate operand addressing), в котором операнд источника может быть частью инструкции, а не отдельными данными. На языке ассемблера это может выглядеть так:
В этом случае rvalue 1 никогда не появляется как объект в пространстве данных. Скорее он появляется как инструкции в пространстве кода.
Очистка объекта устанавливает его содержимое в 0. Инкрементирование дает 1. И все же данные, представляющие величины 0 и 1, нигде не появляются в коде.
Хотя верно, что rvalue на языке C не ссылаются на объекты, для языка C++ это не так. В C++ rvalue, имеющие тип класса, ссылаются на объекты, но они все еще не lvalue. Таким образом все, что я уже сказал для rvalue верно, пока мы не имеем дело с rvalue типа класса.
[Немодифицируемые lvalue]
Хотя lvalue обозначают объекты, не все lvalue могут появляться как левая часть оператора присваивания. Как известно, выражение является последовательностью операторов и операндов, которые задают некие вычисления. Вычисления могут давать в результате значение, или производить сторонние эффекты (side effects). Выражение присваивание имеет форму
где 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 появляется в декларации, и модифицирует тип в декларации, или некоторую часть её, например:
Здесь декларируется объект типа «const int» (постоянное целое). Выражение n ссылается на объект, как будто там нет константы, за исключением того, что n относится к такому объекту, который программа поменять не может. Например, присваивание типа такого выдаст ошибку компиляции:
Как выражение, ссылающееся на const object, такой как n, чем-то отличается от rvalue? В конце концов, если переписать эти выражения для литерального целого числа вместо n. то получим ту же самую ошибку:
Если Вы больше не можете модифицировать n, как будто это rvalue, то почему нельзя сказать, что n также является rvalue? Различие состоит в том, что Вы можете получить адрес от const object, но получить адрес от литерального целого нельзя. Например:
Заметьте, что p, объявленный чуть выше, должен быть указателем именно на const int. Если Вы пропустите ключевое слово const в объявлении указателя, например напишете так:
то тогда следующее присваивание выдаст ошибку:
Когда Вы получаете адрес от объекта const int, то получаете значение типа «указатель на const int,» которое нельзя преобразовать в «указатель на int» без использования приведения типа (cast). Вот пример такого приведения типа:
Поскольку cast принуждает компилятор не жаловаться на преобразование, то делать нечто подобное нежелательно, иначе это может стать причиной трудно находимых ошибок.
Таким образом выражение, которое относится к объекту const, действительно является lvalue, а не rvalue. Однако это специальный вид lvalue, называемый немодифицируемый lvalue (non-modifiable lvalue), который нельзя использовать для модифицирования объекта, на который ссылается lvalue. Это в отличие от поддающегося изменению lvalue (который декларирован без const), который можно использовать для изменения объекта, на который ссылается lvalue.
Поскольку теперь может влиять фактор наличия ключевого слова const, то больше не будет точным называть левую часть оператора присваивания, как lvalue. Скорее, его нужно назвать возможным для изменения lvalue (modifiable lvalue). Фактически, каждый арифметический оператор присваивания, такой как += и *=, требует модифицируемого lvalue в качестве левого операнда. Для всех скалярных типов:
за исключением того, что x оценивается только один раз. Поскольку x в этом арифметическом присваивании должен быть модифицируемым lvalue, также должно быть и в простом присваивании. Не каждый оператор, который требует операнда lvalue, требует именно модифицируемого lvalue. Унарный оператор & принимает в качестве операнда как modifiable lvalue, так и non-modifiable lvalue. Например, если задано:
то &m является допустимым выражением, которое вернет тип «указатель на int», и &n будет также допустимым выражением, возвращающим результат «указатель на const int».
[Что же на самом деле является немодифицируемым]
Тут мы имеем, что p указывает на n, так что и *p, и просто n являются двумя разными выражениями, ссылающимися на один и тот же объект. Однако, *p и n имеют разные типы. Как было рассказано в заметке «What const Really Means» (Что на самом деле означает const) [1], присваивание использует конверсию квалификации, чтобы преобразовать значение типа «указатель на int» в значение типа «указатель на const int». Выражение n имеет тип «не const int». Это модифицируемое lvalue, так что Вы можете модифицировать объект, на которое оно указывает:
С другой стороны, p имеет тип «указатель на const int», поэтому *p имеет тип const int. Выражение *p является немодифицируемым lvalue, и Вы не можете использовать *p для того, чтобы модифицировать n (даже если Вы можете использовать для модификации выражение n):
Такова семантика определения константы const в C и C ++.
[Общие выводы]
Каждое выражение C и C++ является либо lvalue, либо rvalue. Lvalue является выражением, которое обозначает объект (ссылается на него с указателем или без). Каждое lvalue бывает, в свою очередь, или модифицируемым, или немодифицируемым. К rvalue относится любое выражение, которое не является lvalue. Оперативно различия между rvalue и lvalue можно свести к тезисам:
— modifiable lvalue является адресуемым (может быть операндом унара &) и присваиваемым (может являться левым операндом =).
— non-modifiable lvalue также является адресуемым. Но оно не является присваиваемым.
— rvalue не является ни адресуемым, ни присваиваемым.
И снова, как уже упоминалось, все это относится только к rvalue не классового (non-class) типа. Классы в C++ портят эти понятия еще более.