Почему ++i считается l-значением, а i ++ - нет?
Вопрос
Почему ++ i является l-значением, а i ++ нет?
Решение
Ну, как уже указал другой участник ответа, причина, по которой ++i
является ли значение lvalue для передачи его в ссылку.
int v = 0;
int const & rcv = ++v; // would work if ++v is an rvalue too
int & rv = ++v; // would not work if ++v is an rvalue
Причина второго правила заключается в том, чтобы разрешить инициализацию ссылки с использованием литерала, когда ссылка является ссылкой на const:
void taking_refc(int const& v);
taking_refc(10); // valid, 10 is an rvalue though!
Вы можете спросить, зачем мы вообще вводим значение rvalue.Что ж, эти термины возникают при построении языковых правил для этих двух ситуаций:
- Мы хотим иметь значение локатора.Это будет представлять местоположение, содержащее значение, которое можно прочитать.
- Мы хотим представить значение выражения.
Приведенные выше два пункта взяты из стандарта C99, который включает в себя эту приятную сноску, весьма полезную:
[ Название ‘lvalue’ происходит изначально из выражения присваивания E1 = E2, в котором левый операнд E1 обязательно должен быть (изменяемым) значением lvalue.Возможно, его лучше рассматривать как представляющий объект ‘locator значение’.То, что иногда называют ‘rvalue’, в этом международном стандарте описывается как ‘значение выражения’.]
Вызывается значение локатора значение lvalue, в то время как значение , полученное в результате оценки этого местоположения , вызывается r значение.Это верно также в соответствии со стандартом C ++ (речь идет о преобразовании lvalue в rvalue):
4.1/2:Значение, содержащееся в объекте обозначаемое значением lvalue, является значением rvalue Результат.
Заключение
Используя приведенную выше семантику, теперь ясно, почему i++
является не значением lvalue, а значением rvalue.Поскольку возвращаемое выражение не находится в i
больше (оно увеличивается!), это просто значение, которое может представлять интерес.Изменение этого значения, возвращаемого i++
это не имело бы смысла, потому что у нас нет местоположения, из которого мы могли бы снова прочитать это значение.Итак, в Стандарте указано, что это значение rvalue , и, таким образом, оно может привязываться только к ссылке на const .
Однако в constrast выражение, возвращаемое ++i
является местоположением (lvalue) i
.Провоцирование преобразования lvalue в rvalue, как в int a = ++i;
будет считывать из него значение.В качестве альтернативы, мы можем указать на него точку отсчета и позже зачитать значение: int &a = ++i;
.
Обратите также внимание на другие случаи, когда генерируются rvalues .Например, все временные значения являются rvalues, результатом двоичных / унарных + и minus и всех выражений возвращаемого значения, которые не являются ссылками.Все эти выражения не находятся в именованном объекте, а содержат только значения.Эти значения, конечно, могут быть подкреплены объектами, которые не являются постоянными.
Следующая версия C ++ будет включать так называемые rvalue references
которые, даже если они указывают на nonconst, могут привязываться к rvalue .Смысл заключается в том, чтобы иметь возможность "красть" ресурсы у этих анонимных объектов и избегать копирования, делающего это.Предполагая тип класса, который имеет перегруженный префикс ++ (возвращающий Object&
) и postfix ++ (возвращающий Object
), следующее сначала приведет к копированию, а во втором случае это украдет ресурсы из rvalue:
Object o1(++a); // lvalue => can't steal. It will deep copy.
Object o2(a++); // rvalue => steal resources (like just swapping pointers)
Другие советы
Другие люди рассматривали функциональную разницу между приращением после и до.
Что касается того , чтобы быть значение lvalue обеспокоен, i++
не может быть присвоен, поскольку он не ссылается на переменную.Это относится к вычисленному значению.
С точки зрения назначения, оба следующих действия не имеют смысла в одном и том же виде:
i++ = 5;
i + 0 = 5;
Поскольку pre-increment возвращает ссылку на увеличенную переменную, а не временную копию, ++i
является значением lvalue.
Предпочтение предварительного приращения по соображениям производительности становится особенно хорошей идеей, когда вы увеличиваете что-то вроде объекта iterator (например, в STL), который вполне может быть намного более тяжелым, чем int.
Кажется, что многие люди объясняют, как ++i
является значением lvalue, но не почему, как в, почему включил ли комитет по стандартам C ++ эту функцию, особенно в свете того факта, что C не допускает ни того, ни другого в качестве значений lvalues .От это обсуждение на comp.std.c ++, похоже, что это для того, чтобы вы могли взять его адрес или назначить ссылку.Пример кода, взятый из поста Кристиана Бау:
int i; extern void f (int* p); extern void g (int& p); f (&++i); /* Would be illegal C, but C programmers havent missed this feature */ g (++i); /* C++ programmers would like this to be legal */ g (i++); /* Not legal C++, and it would be difficult to give this meaningful semantics */
Кстати, если i
оказывается встроенным типом, тогда операторы присваивания, такие как ++i = 10
призывать неопределенное поведение, потому что i
изменяется дважды между точками последовательности.
Я получаю ошибку lvalue при попытке скомпилировать
i++ = 2;
но не тогда, когда я меняю его на
++i = 2;
Это происходит потому, что префиксный оператор (++i) изменяет значение в i, затем возвращает i, так что оно все еще может быть присвоено.Постфиксный оператор (i++) изменяет значение в i, но возвращает временную копию старого значение, который не может быть изменен оператором присваивания.
Ответ на первоначальный вопрос:
Если вы говорите об использовании операторов увеличения в инструкции сами по себе, как в цикле for , это действительно не имеет никакого значения.Предварительное приращение кажется более эффективным, потому что postincrement должен увеличивать себя и возвращать временное значение, но компилятор оптимизирует это различие.
for(int i=0; i<limit; i++)
...
это то же самое , что
for(int i=0; i<limit; ++i)
...
Все становится немного сложнее, когда вы используете возвращаемое значение операции как часть более крупного оператора.
Даже два простых утверждения
int i = 0;
int a = i++;
и
int i = 0;
int a = ++i;
они разные.Какой оператор приращения вы решите использовать как часть мультиоператорных инструкций, зависит от предполагаемого поведения.Короче говоря, нет, вы не можете просто выбрать что-то одно.Вы должны понимать и то, и другое.
Предварительное увеличение модуля:
Предварительное приращение должно действовать так, как если бы объект был увеличен до выражения, и его можно было использовать в этом выражении, как если бы это произошло.Таким образом, комитет по стандартам C ++ решил, что это также может быть использовано в качестве l-значения.
Приращение сообщения POD:
Последующее приращение должно увеличивать объект POD и возвращать копию для использования в выражении (см. n2521, раздел 5.2.6).Поскольку копия на самом деле не является переменной, придавать ей l-значение не имеет никакого смысла.
Объекты:
Предварительное и последующее увеличение объектов - это просто синтаксический сахар языка, предоставляющий средства для вызова методов объекта.Таким образом, технически объекты ограничены не стандартным поведением языка, а только ограничениями, налагаемыми вызовами методов.
Разработчик этих методов должен сделать так, чтобы поведение этих объектов отражало поведение объектов POD (это не обязательно, но ожидается).
Предварительное увеличение объектов:
Требование (ожидаемое поведение) здесь заключается в том, чтобы объекты увеличивались (что означает зависимость от object), а метод возвращал значение, которое можно изменять и которое выглядит как исходный объект после того, как произошло увеличение (как если бы увеличение произошло до этого оператора).
Сделать это можно просто и требуется только, чтобы метод возвращал ссылку на себя.Ссылка является l-значением и, следовательно, будет вести себя так, как ожидалось.
Объекты После приращения:
Требование (ожидаемое поведение) здесь заключается в том, чтобы объект был увеличен (таким же образом, как и при предварительном увеличении), а возвращаемое значение выглядело как старое значение и не изменялось (чтобы оно не вело себя как l-значение).
Неизменяемый:
Чтобы сделать это, вы должны вернуть объект.Если объект используется внутри выражения, он будет скопирован во временную переменную.Временные переменные являются const, и, таким образом, они будут неизменяемыми и будут вести себя так, как ожидалось.
Похоже на старое значение:
Это просто достигается путем создания копии оригинала (возможно, с помощью конструктора копирования) перед внесением каких-либо изменений.Копия должна быть глубокой копией, в противном случае любые изменения в оригинале повлияют на копию и, таким образом, состояние изменится в зависимости от выражения, использующего объект.
Таким же образом, как предварительное увеличение:
Вероятно, лучше всего реализовать post increment в терминах pre-increment, чтобы вы получили такое же поведение.
class Node // Simple Example
{
/*
* Pre-Increment:
* To make the result non-mutable return an object
*/
Node operator++(int)
{
Node result(*this); // Make a copy
operator++(); // Define Post increment in terms of Pre-Increment
return result; // return the copy (which looks like the original)
}
/*
* Post-Increment:
* To make the result an l-value return a reference to this object
*/
Node& operator++()
{
/*
* Update the state appropriatetly */
return *this;
}
};
Что касается LValue
В
C
(и Perl, например), ни то , ни другое++i
ниi++
являются LValues. являются LValues.В
C++
,i++
не является и имеет значение, но++i
есть.++i
эквивалентноi += 1
, что эквивалентноi = i + 1
.
В результате мы по-прежнему имеем дело с одним и тем же объектомi
.
Это можно рассматривать как:int i = 0; ++i = 3; // is understood as i = i + 1; // i now equals 1 i = 3;
i++
с другой стороны, можно было бы рассматривать как:
Сначала мы используем значение изi
, затем увеличьте объектi
.int i = 0; i++ = 3; // would be understood as 0 = 3 // Wrong! i = i + 1;
(редактировать:обновлено после неудачной первой попытки).
Основное отличие заключается в том, что i ++ возвращает значение до приращения, тогда как ++ i возвращает значение после приращения.Обычно я использую ++ i, если только у меня нет очень веской причины использовать i ++, а именно, если я действительно делай нужно значение с предварительным приращением.
ИМХО, хорошей практикой является использование формы '++ i'.Хотя разница между предварительным и последующим приращением на самом деле не поддается измерению при сравнении целых чисел или других модулей, дополнительная копия объекта, которую вам приходится создавать и возвращать при использовании 'i ++', может оказать значительное влияние на производительность, если копирование объекта обходится довольно дорого или его часто увеличивают.
Кстати - избегайте использования нескольких операторов приращения для одной и той же переменной в одном и том же операторе.Вы попадаете в неразбериху с вопросом "где находятся точки последовательности" и неопределенным порядком операций, по крайней мере, в C.Я думаю, что кое-что из этого было исправлено в Java и C #.
Возможно, это как-то связано со способом реализации постинкремента.Возможно, это что-то вроде этого:
- Создайте копию исходного значения в памяти
- Увеличьте исходную переменную
- Верните копию
Поскольку копия не является ни переменной, ни ссылкой на динамически выделяемую память, она не может быть l-значением.
Как компилятор переводит это выражение? a++
Мы знаем, что хотим вернуть неискаженный версия a
, старая версия a до того , как приращение.Мы также хотим увеличить a
как побочный эффект.Другими словами, мы возвращаем старую версию a
, который больше не отражает текущее состояние a
, это больше не является самой переменной.
Возвращаемое значение является копией a
который помещается в Зарегистрироваться.Затем переменная увеличивается.Итак, здесь вы возвращаете не саму переменную, но вы возвращаете копию, которая является отдельный сущность!Эта копия временно хранится внутри реестра, а затем возвращается.Напомним, что значение lvalue в C ++ - это объект, который имеет идентифицируемое местоположение в памяти.Но копия хранится внутри регистр в процессоре, а не в памяти. Все rvalues - это объекты, которые не имеют идентифицируемого местоположения в памяти.Это объясняет, почему копия старой версии a
является значением rvalue, потому что оно временно сохраняется в регистре.В общем, любые копии, временные значения или результаты длинных выражений, таких как (5 + a) * b
хранятся в регистрах, а затем они присваиваются переменной, которая является значением lvalue.
Оператор postfix должен сохранить исходное значение в регистре, чтобы он мог возвращать невключенное значение в качестве своего результата.Рассмотрим следующий код:
for (int i = 0; i != 5; i++) {...}
Этот цикл for насчитывает до пяти, но i++
это самая интересная часть.На самом деле это две инструкции в 1.Сначала мы должны переместить старое значение i
в регистр, затем мы увеличиваем i
.В псевдосемблерном коде:
mov i, eax
inc i
eax
регистрация теперь содержит старую версию i
как копия.Если переменная i
находится в основной памяти, процессору может потребоваться много времени, чтобы полностью извлечь копию из основной памяти и переместить ее в регистр.Обычно это происходит очень быстро для современных компьютерных систем, но если ваш цикл for повторяется сто тысяч раз, все эти дополнительные операции начинают накапливаться!Это привело бы к значительному снижению производительности.
Современные компиляторы обычно достаточно умны, чтобы оптимизировать эту дополнительную работу для типов integer и pointer.Для более сложных типов итераторов или, возможно, типов классов эта дополнительная работа потенциально может оказаться более дорогостоящей.
А как насчет приращения префикса ++a
?
Мы хотим вернуть увеличенный версия a
, новая версия a
после приращение.Новая версия a
представляет текущее состояние a
, потому что это сама переменная.
Первый a
увеличивается.Поскольку мы хотим получить обновленную версию a
, почему бы просто не вернуть переменная a
сам по себе?Нам не нужно создавать временную копию в регистре, чтобы сгенерировать rvalue.Это потребовало бы ненужной дополнительной работы.Поэтому мы просто возвращаем саму переменную как значение lvalue .
Если нам не нужно невключенное значение, нет необходимости в дополнительной работе по копированию старой версии a
в регистр, что выполняется оператором postfix.Вот почему вы должны использовать только a++
если вы действительно необходимо вернуть неинкрементированное значение.Для всех других целей просто используйте ++a
.Привычно используя префиксные версии, нам не нужно беспокоиться о том, имеет ли значение разница в производительности.
Еще одно преимущество использования ++a
заключается в том, что это более прямо выражает намерение программы:Я просто хочу увеличить a
!Однако, когда я вижу a++
интересно, почему в чужом коде они хотят вернуть старое значение?Для чего это нужно?
C#:
public void test(int n)
{
Console.WriteLine(n++);
Console.WriteLine(++n);
}
/* Output:
n
n+2
*/