Вопрос

Почему ++ 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
*/
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top