Почему ++ я считаю l-значением, а i ++ нет? - PullRequest
39 голосов
/ 16 декабря 2008

Почему ++ i - это l-значение, а i ++ - нет?

Ответы [ 11 ]

32 голосов
/ 16 декабря 2008

Другие люди занимались функциональной разницей между постом и предварительным приращением.

Что касается lvalue , то i++ нельзя присвоить, поскольку он не ссылается на переменную. Это относится к расчетному значению.

С точки зрения назначения, оба следующих понятия не имеют смысла одним и тем же способом:

i++   = 5;
i + 0 = 5;

Поскольку предварительное увеличение возвращает ссылку на инкрементную переменную, а не на временную копию, ++i является lvalue.

Предпочтение предварительного приращения по соображениям производительности становится особенно хорошей идеей, когда вы увеличиваете что-то вроде объекта итератора (например, в STL), который вполне может быть более тяжелым, чем int.

24 голосов
/ 17 декабря 2008

Ну, как уже сказал другой ответчик, причина, по которой ++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!

Почему мы вообще вводим значение, которое вы можете спросить? Итак, эти термины возникают при построении языковых правил для этих двух ситуаций:

  • Мы хотим иметь значение локатора. Это будет представлять местоположение, которое содержит значение, которое можно прочитать.
  • Мы хотим представить значение выражения.

Вышеупомянутые два пункта взяты из Стандарта C99, который включает эту полезную сноску, весьма полезную:

[Название val val lvalue ’’ происходит от из выражения присваивания E1 = Е2, в котором левый операнд Е1 должен быть (изменяемым) значением. Возможно, это лучше рассматривать как представляющий объект ‘‘ локатор значение''. Что иногда называют Val val rvalue ’’ в этом международном Стандарт описывается как значение ‘‘ выражение''. ]

Значение локатора называется lvalue , а значение, полученное в результате оценки этого местоположения, называется rvalue . Это верно и в соответствии со стандартом C ++ (речь идет о преобразовании lvalue-в-значение):

4.1 / 2: значение, содержащееся в объекте обозначенный lvalue является rvalue результат.

Заключение

Используя вышеуказанную семантику, теперь ясно, почему i++ не является lvalue, а является rvalue. Поскольку возвращаемое выражение больше не находится в i (оно увеличивается!), Оно может представлять интерес только для значения. Изменение значения, возвращаемого i++, не имеет смысла, потому что у нас нет места, из которого мы могли бы снова прочитать это значение. Таким образом, Стандарт говорит, что это значение, и поэтому может связываться только со ссылкой на const.

Однако, в противоположность, выражение, возвращаемое ++i, является местоположением (lvalue) i. Провоцирование преобразования lvalue в rvalue, как в int a = ++i;, будет считывать значение из него. В качестве альтернативы, мы можем указать на него контрольную точку и прочитать значение позже: int &a = ++i;.

Обратите внимание также на другие случаи, когда генерируются значения r. Например, все временные значения - это rvalues, результат двоичных / унарных + и минус, а также все выражения возвращаемых значений, которые не являются ссылками. Все эти выражения не находятся в именованном объекте, а содержат только значения. Эти значения, конечно, могут быть подкреплены объектами, которые не являются постоянными.

Следующая версия C ++ будет включать в себя так называемые rvalue references, которые, даже если они указывают на nonconst, могут связываться с rvalue. Смысл в том, чтобы иметь возможность «украсть» ресурсы у этих анонимных объектов и избежать копирования. Если предположить, что тип класса имеет перегруженный префикс ++ (возвращающий Object&) и postfix ++ (возвращающий Object), то сначала будет вызвана копия, а во втором случае будут украдены ресурсы из значения r:

Object o1(++a); // lvalue => can't steal. It will deep copy.
Object o2(a++); // rvalue => steal resources (like just swapping pointers)
10 голосов
/ 16 декабря 2008

Кажется, что многие люди объясняют, как ++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 изменяется дважды между точками последовательности.

5 голосов
/ 16 декабря 2008

Я получаю сообщение об ошибке lvalue, когда пытаюсь скомпилировать

i++ = 2;

но не тогда, когда я изменяю его на

++i = 2;

Это связано с тем, что префиксный оператор (++ i) изменяет значение в i, а затем возвращает i, поэтому его все еще можно присвоить. Постфиксный оператор (i ++) изменяет значение в i, но возвращает временную копию старого значения , которое не может быть изменено оператором присваивания.

<Ч />

Ответ на оригинальный вопрос :

Если вы говорите об использовании операторов приращения в инструкции самостоятельно, как в цикле for, это действительно не имеет значения. Преинкремент кажется более эффективным, потому что постинкремент должен сам себя увеличивать и возвращать временное значение, но компилятор устранит эту разницу.

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;

разные. Какой оператор приращения вы выберете использовать как часть операторов с несколькими операторами, зависит от предполагаемого поведения. Короче говоря, нет, вы не можете просто выбрать один. Вы должны понимать оба.

4 голосов
/ 16 декабря 2008

POD Pre инкремент:

Предварительное увеличение должно действовать так, как если бы объект был увеличен перед выражением, и использоваться в этом выражении, как если бы это произошло. Таким образом, комитет по стандартам C ++ решил, что его также можно использовать как l-значение.

POD Пост приращения:

Постинкремент должен увеличивать объект POD и возвращать копию для использования в выражении (см. N2521, раздел 5.2.6). Поскольку копия на самом деле не является переменной, делающей ее, значение l не имеет никакого смысла.

Предметы:

До и после приращения на объектах является просто синтаксическим сахаром языка, предоставляющим средства для вызова методов на объекте. Таким образом, технически Объекты не ограничены стандартным поведением языка, а только ограничениями, налагаемыми вызовами методов.

Именно разработчик этих методов должен заставить поведение этих объектов отражать поведение объектов POD (это не обязательно, но ожидается).

Предварительное увеличение объектов:

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

Для этого достаточно siple, и он требует только, чтобы метод возвращал ссылку на себя. Ссылка является l-значением и, следовательно, будет вести себя как ожидалось.

Объекты Постинкремент:

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

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

Похоже на старое значение:
Это просто достигается созданием копии оригинала (возможно, с использованием конструктора копирования) перед внесением каких-либо изменений. Копия должна быть глубокой копией, в противном случае любые изменения в оригинале будут влиять на копию, и, следовательно, состояние будет изменяться по отношению к выражению, использующему объект.

Аналогично предварительному приращению:
Вероятно, лучше всего реализовать постинкремент в терминах предварительного приращения, чтобы получить то же поведение.

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;
     }
};
2 голосов
/ 16 декабря 2008

Относительно LValue

  • В C (и Perl, например), ни ++i, ни i++ не являются LValues.

  • В C++, i++ нет и LValue, но ++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;
    

(редактировать: обновляется после первой попытки очистки).

0 голосов
/ 19 июля 2018

Как компилятор переводит это выражение? a++

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

Возвращаемое значение является копией a, которая помещается в регистр . Затем переменная увеличивается. Итак, здесь вы не возвращаете саму переменную, а возвращаете копию, которая является отдельной сущностью! Эта копия временно сохраняется в регистре, а затем возвращается. Напомним, что lvalue в C ++ - это объект, который имеет идентифицируемое местоположение в памяти . Но копия хранится в регистре в ЦП, а не в памяти. Все значения являются объектами, которые не имеют идентифицируемой позиции в памяти . Это объясняет, почему копия старой версии a является значением, поскольку она временно сохраняется в реестре. Как правило, любые копии, временные значения или результаты длинных выражений, таких как (5 + a) * b, сохраняются в регистрах, а затем они присваиваются переменной, которая является lvalue.

Оператор постфикса должен сохранить исходное значение в регистре, чтобы он мог возвращать неинкрементированное значение в качестве своего результата. Рассмотрим следующий код:

for (int i = 0; i != 5; i++) {...}

Этот цикл for насчитывает до пяти, но i++ - самая интересная часть. Это на самом деле две инструкции в 1. Сначала мы должны переместить старое значение i в регистр, затем мы увеличиваем i. В псевдо-ассемблерном коде:

mov i, eax
inc i
Регистр

eax теперь содержит старую версию i в качестве копии. Если переменная i находится в основной памяти, процессору может потребоваться много времени, чтобы полностью скопировать копию из основной памяти и переместить ее в регистр. Это обычно очень быстро для современных компьютерных систем, но если ваш цикл for повторяется сто тысяч раз, все эти дополнительные операции начинают складываться! Это было бы значительным штрафом за производительность.

Современные компиляторы обычно достаточно умны, чтобы оптимизировать эту дополнительную работу для целочисленных типов и указателей. Для более сложных типов итераторов или, может быть, типов классов эта дополнительная работа потенциально может быть более дорогостоящей.

Как насчет увеличения префикса ++a?

Мы хотим вернуть увеличенную версию a, новую версию a после приращения. Новая версия a представляет текущее состояние a, потому что это сама переменная.

Первый a увеличивается. Поскольку мы хотим получить обновленную версию a, почему бы просто не вернуть саму переменную a ? Нам не нужно делать временную копию в регистр для генерации значения. Это потребует ненужной дополнительной работы. Поэтому мы просто возвращаем саму переменную как lvalue.

Если нам не нужно необязательное значение, нет необходимости в дополнительной работе по копированию старой версии a в регистр, которая выполняется оператором postfix. Вот почему вы должны использовать a++, только если вам действительно нужно вернуть неинкрементированное значение. Для всех остальных целей просто используйте ++a. При обычном использовании префиксных версий нам не нужно беспокоиться о том, имеет ли значение разница в производительности.

Еще одним преимуществом использования ++a является то, что оно выражает намерение программы более непосредственно: я просто хочу увеличить a! Однако, когда я вижу a++ в чужом коде, я задаюсь вопросом, почему они хотят вернуть старое значение? Для чего это?

0 голосов
/ 16 декабря 2008

Может быть, это как-то связано с реализацией постинкремента. Возможно, это что-то вроде этого:

  • Создать копию исходного значения в памяти
  • Увеличить исходную переменную
  • Вернуть копию

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

0 голосов
/ 16 декабря 2008

Кстати, избегайте использования нескольких операторов приращения для одной и той же переменной в одном выражении. Вы попадаете в беспорядок «где точки последовательности» и неопределенный порядок операций, по крайней мере, в C. Я думаю, что некоторые из них были убраны в Java и C #.

0 голосов
/ 16 декабря 2008

Основное отличие состоит в том, что i ++ возвращает значение перед приращением, тогда как ++ i возвращает значение после приращения. Я обычно использую ++ i, если только у меня нет веских причин использовать i ++, а именно, если мне действительно действительно нужно значение перед приращением.

ИМХО, это хорошая практика - использовать форму "++ i". Хотя разница между пре- и постинкрементным измерением на самом деле не измерима, когда вы сравниваете целые числа или другие POD, дополнительная копия объекта, которую вы должны сделать и вернуть при использовании i ++, может оказать существенное влияние на производительность, если объект достаточно дорог копировать или часто увеличивать.

...