Почему литералы и временные переменные не являются значениями?

Я читал, что lvalues - это "вещи с определенным местом хранения".

А также, что переменные литералов и временных переменных не являются lvalues, но для этого утверждения не приводится никаких причин.

Это потому, что литералы и временные переменные не имеют определенного места хранения? Если да, то где они находятся, если не в памяти?

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

Ответы

Ответ 1

А также, что переменные литералов и временных переменных не являются lvalues, но для этого утверждения не приводится никаких причин.

Это верно для всех временных и литералов, кроме строковых литералов. Это на самом деле lvalues (что объясняется ниже).

Это потому, что переменные литералов и временных переменных не имеют определенного места хранения? Если да, то где они находятся, если не в памяти?

Да. Буква 2 самом деле не существует; это просто значение в исходном коде. Поскольку это значение, а не объект, ему не нужно иметь какую-либо память, связанную с ним. Он может быть жестко запрограммирован в сборку, которую создает компилятор, или он может быть помещен куда-то, но так как этого не требуется, все, что вы можете сделать, это рассматривать его как чистое значение, а не как объект.

Есть исключение, и это строковые литералы. У них на самом деле есть память, поскольку строковый литерал - это массив const char[N]. Вы можете взять адрес строкового литерала, а строковый литерал может превратиться в указатель, так что это lvalue, даже если у него нет имени.

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

Foo a = Foo();

Foo() может быть удален, а код семантически преобразован в

Foo a(); // you can't actually do this since it declares a function with that signature.

так что теперь в оптимизированном коде нет даже временного объекта.

Ответ 2

Почему литералы и временные переменные не являются значениями?

У меня есть два ответа: потому что это не имеет смысла (1) и потому что Стандарт говорит так (2). Давайте сосредоточимся на (1).

Это потому, что переменные литералов и временных переменных не имеют определенного места хранения?

Это упрощение, которое здесь не подходит. Упрощение: литералы и временные не являются lvalues, потому что не имеет смысла изменять их 1.

Что означает 5++? Что означает rand() = 0? Стандарт гласит, что временные и литеральные значения не являются lvalues, поэтому эти примеры являются недействительными. И каждый разработчик компилятора счастливее.


1) Вы можете определять и использовать пользовательские типы таким образом, чтобы изменение временного кода имело смысл. Этот временный дожил бы до оценки полного выражения. Франсуа Андрие проводит хорошую аналогию между вызовом f(MyType{}.mutate()) с одной стороны и f(my_int + 1) с другой. Я думаю, что упрощение имеет еще как MyType{}.mutate() можно рассматривать как еще один временный, как MyType{} был, как my_int + 1 можно рассматривать как еще один int, как my_int было. Это все семантика и на основе мнения. Реальный ответ: (2) потому что Стандарт говорит так.

Ответ 3

Существует много распространенных заблуждений в вопросе и других ответах; мой ответ надеется решить эту проблему.

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

Термин временный объект относится к объектам. Это включает в себя объекты типа класса, а также объекты встроенного типа. Термин временный (используется как существительное) является сокращением от временного объекта. Иногда автономный термин value используется для обозначения временного объекта встроенного типа. Эти термины применяются к объектам, а не к выражениям.

Стандарт C++ 17 более согласован в объектной терминологии, чем прошлые стандарты, например, см. [Conv.rval]/1. Теперь он пытается избежать произнесения значения, отличного от значения контекста выражения.


Теперь, почему существуют разные категории выражений? Программа C++ состоит из набора выражений, соединенных друг с другом операторами для создания больших выражений; и вписывается в рамки декларативных конструкций. Эти выражения создают, уничтожают и делают другие манипуляции с объектами. Программирование в C++ можно описать как использование выражений для выполнения операций с объектами.

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

Термин lvalue возник в те времена, хотя сейчас с развитием C++ существует широкий диапазон выражений и контекстов, где полезны категории выражений, а не только левая часть оператора присваивания.

Вот некоторый допустимый код C++: std::string("3") = std::string("5"); , Это концептуально не отличается от 3 = 5; Однако это разрешено. В результате создается временный объект типа std::string и с содержимым "3", а затем этот временный объект изменяется на содержимое "5", а затем временный объект уничтожается. Язык можно было бы спроектировать так, чтобы код 3 = 5; указывает аналогичную серию событий (но это не так).


Почему string пример допустим, а int нет?

Каждое выражение должно иметь категорию. На первый взгляд категория выражения может не иметь очевидной причины, но разработчики языка дали каждому выражению категорию в соответствии с тем, что, по их мнению, является полезным понятием для выражения, а что - нет.

Было решено, что последовательность событий в 3 = 5; как описано выше, это не то, что кто-то хотел бы сделать, и если кто-то написал такую вещь, он, вероятно, допустил ошибку и имел в виду что-то другое, поэтому компилятор должен помочь, сообщив об ошибке.

Теперь та же логика может заключить, что std::string("3") = std::string("5") - это не то, что кто-либо когда-либо захочет делать. Однако другой аргумент заключается в том, что для некоторого другого типа класса T(foo) = x; может быть полезной операцией, например, потому что у T может быть деструктор, который что-то делает. Было решено, что запрет такого использования может быть более вредным для намерений программиста, чем пользой. (Независимо от того, что это было хорошее решение или не является дискуссионным, увидеть этот вопрос для обсуждения).


Теперь мы приближаемся к окончательному решению вашего вопроса :)

Наличие или отсутствие памяти или места хранения больше не является обоснованием для категорий выражений. В абстрактной машине (более подробное объяснение этого ниже) каждый временный объект (включая объект, созданный 3 в x = 3;) существует в памяти.

Как описано ранее в моем ответе, программа состоит из выражений, которые манипулируют объектами. Говорят, что каждое выражение обозначает или ссылается на объект.

В других ответах или статьях по этой теме очень часто делается неверное утверждение, что rvalue может обозначать только временный объект, или, что еще хуже, что rvalue является временным объектом или что временный объект является rvalue. Выражение не является объектом, это то, что происходит в исходном коде для манипулирования объектами!

Фактически временный объект может быть обозначен lvalue или rvalue выражением; и невременный объект может быть обозначен выражением lvalue или rvalue. Это отдельные понятия.

Теперь есть правило категории выражения, которое нельзя применить & к выражению категории rvalue. Цель этого правила и этих категорий состоит в том, чтобы избежать ошибок, когда временный объект используется после его уничтожения. Например:

int *p = &5;    // not allowed due to category rules
*p = 6;         // oops, dangling pointer

Но вы можете обойти это:

template<typename T> auto f(T&&t) -> T& { return t; }
// ...
int *p = f(5); // Allowed
*p = 6;        // Oops, dangling pointer, no compiler error message.

В этом последнем коде f(5) и *p оба являются l-значениями, которые обозначают временный объект. Это хороший пример того, почему существуют правила категории выражений; следуя правилам без хитрого обходного пути, мы получим ошибку для кода, который пытается написать через висячий указатель.

Обратите внимание, что вы также можете использовать это f чтобы найти адрес памяти временного объекта, например, std::cout << &f(5);


Таким образом, вопросы, которые вы фактически задаете, ошибочно связывают выражения с объектами. Таким образом, они не являются вопросами в этом смысле. Временные значения не являются lvalues, потому что объекты не являются выражениями.

Допустимый, но связанный с этим вопрос: "Почему выражение, которое создает временный объект, является значением (а не значением?)"

Ответ на который был таким, как обсуждался выше: наличие lvalue увеличило бы риск создания висячих указателей или висячих ссылок; и как в 3 = 5; , увеличит риск указания избыточных операций, которые программист, вероятно, не намеревался.

Я еще раз повторяю, что категории выражений - это дизайнерское решение, помогающее программистам выразиться; не имеет ничего общего с памятью или местами хранения.


Наконец, к абстрактной машине и правилу "как будто". C++ определяется в терминах абстрактной машины, в которой временные объекты также имеют память и адреса. Ранее я привел пример того, как напечатать адрес временного объекта.

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

Так что для кода, как x = 5; даже если временный объект со значением 5 имеет место в памяти в абстрактной машине; компилятору не нужно выделять физическую память на реальной машине. Нужно только убедиться, что в конце x хранится 5 и есть гораздо более простые способы сделать это, не включая создание дополнительного хранилища.

Правило "как будто" применяется ко всему в программе, хотя мой пример здесь относится только к временным объектам. Невременный объект также может быть оптимизирован, например, int x; int y = 5; x = y;//other code that doesn't use y int x; int y = 5; x = y;//other code that doesn't use y int x; int y = 5; x = y;//other code that doesn't use y может быть изменен на int x = 5; ,

То же самое относится к типам классов без побочных эффектов, которые могли бы изменить вывод программы. Например, std::string x = "foo"; std::cout << x; std::string x = "foo"; std::cout << x; может быть оптимизирован для std::cout << "foo"; даже если lvalue x обозначает объект с хранилищем в абстрактной машине.

Ответ 4

lvalue обозначает значение локатора и представляет объект, который занимает определенное место в памяти.

Термин значение локатора также используется здесь:

С

Язык программирования C придерживался аналогичной таксономии, за исключением того, что роль присваивания больше не была значительной: выражения C делятся на категории между "выражениями lvalue" и другими (функции и значения необъектных), где "lvalue" означает выражение, которое идентифицирует объект, "значение локатора" [4].

Все, что не является lvalue, по исключению является rvalue. Каждое выражение является либо lavalue или rvalue.

Первоначально термин lvalue использовался в C для обозначения значений, которые могут оставаться слева от оператора присваивания. Однако с const keywork это изменилось. Не все lvalues могут быть назначены. Те, которые могут называться modifiable lvalues.

А также, что переменные литералов и временных переменных не являются lvalues, но для этого утверждения не приводится никаких причин.

Согласно этому ответу в некоторых случаях литералы могут быть lvalues.

  • Литералы скалярных типов являются rvalue потому что они имеют известный размер и, скорее всего, будут встроены непосредственно в машинные команды на данной аппаратной архитектуре. Какой будет память на 5?
  • Напротив, как ни странно, строковые литералы являются lvalues так как они имеют непредсказуемый размер, и нет другого способа представить их отдельно от объектов в памяти.

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

int a =5;
int b = 3;
int c = a+b;

оператор + принимает два rvalues. Таким образом, a и b преобразуются в rvalues перед суммированием. Еще один пример конвертации:

int c = 6;
&c = 4; //ERROR: &c is an rvalue

Напротив, вы не можете преобразовать rvalue к lvalue.

Однако вы можете создать действительное lvalue из rvalue например:

int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;   // OK: p + 1 is an rvalue, but *(p + 1) is an lvalue

В С++ 11 ссылки на значения связаны с конструктором перемещения и оператором присваивания перемещения.

Вы можете найти больше деталей в этом ясном и хорошо объясненном посте.

Ответ 5

Где они находятся, если не в памяти?

Конечно, они хранятся в памяти * нет никакого пути к этому. Вопрос в том, может ли ваша программа определить, где именно в памяти они находятся. Другими словами, разрешено ли вашей программе принимать адрес рассматриваемой вещи.

В простом примере a = 5 значение пять или инструкция, представляющая присвоение значения пять, находится где-то в памяти. Однако вы не можете взять адрес пять, потому что int *p = &5 недопустимо.

Обратите внимание, что строковые литералы являются исключением из правила "not a lvalue", поскольку const char *p = "hello" создает адрес строкового литерала.


* Однако это не обязательно может быть память данных. Фактически, они могут даже не быть представлены как константа в памяти программы: например, назначение short a; a = 0xFF00 short a; a = 0xFF00 может быть представлено как присвоение 0xFF в верхнем октете и очистка нижнего октета в памяти.