Приращение в C ++ - когда использовать x ++ или ++ x?

93

В настоящее время я изучаю C ++ и некоторое время назад узнал об увеличении. Я знаю, что вы можете использовать «++ x» для увеличения до и «x ++», чтобы сделать это после.

Тем не менее, я действительно не знаю, когда использовать любой из двух ... Я никогда не использовал "++ x", и до сих пор все работало нормально - так когда мне его использовать?

Пример: когда в цикле for предпочтительнее использовать «++ x»?

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

Джесси Эмонд
источник

Ответы:

118

Это вопрос не предпочтений, а логики.

x++увеличивает значение переменной x после обработки текущего оператора.

++xувеличивает значение переменной x перед обработкой текущего оператора.

Так что просто определитесь с логикой, которую вы пишете.

x += ++iбудет увеличивать i и добавлять i + 1 к x. x += i++добавит i к x, затем увеличит i.

Оливер Фридрих
источник
27
и обратите внимание, что в цикле for на примитивах нет абсолютно никакой разницы. Многие стили кодирования рекомендуют никогда не использовать оператор приращения, если это может быть неправильно понято; т.е. x ++ или ++ x должны существовать только в отдельной строке, а не как y = x ++. Лично мне это не нравится, но это редкость
Майкедж
2
И если использовать его в отдельной строке, сгенерированный код почти наверняка будет таким же.
Nosredna
14
Это может показаться педантизмом (в основном потому, что это так :)), но в C ++ x++это rvalue со значением xдо приращения, x++это lvalue со значением xпосле приращения. Ни одно из выражений не гарантирует, что фактическое увеличенное значение сохраняется обратно в x, гарантируется только то, что это произойдет до следующей точки последовательности. «после обработки текущего оператора» не является строго точным, поскольку некоторые выражения имеют точки последовательности, а некоторые операторы являются составными операторами.
CB Bailey
10
На самом деле ответ вводит в заблуждение. Момент времени, когда изменяется переменная x, вероятно, практически не отличается. Разница в том, что x ++ определен для возврата rvalue предыдущего значения x, в то время как ++ x по-прежнему относится к переменной x.
sellibitze
5
@BeowulfOF: Ответ подразумевает заказ, которого не существует. В стандарте ничего не говорится о том, когда будут происходить приращения. Компилятор имеет право реализовать "x + = i ++" как: int j = i; я = я + 1; x + = j; "(т.е. 'i' увеличивался перед" обработкой текущего оператора "). Вот почему" i = i ++ "имеет неопределенное поведение, и поэтому я думаю, что ответ требует" настройки ". Описание" x + = ++ i "правильно, поскольку нет предложения по порядку:" будет увеличивать i и добавлять i + 1 к x "
Ричард Корден,
53

Скотт Мейерс советует вам отдавать предпочтение префиксу, за исключением тех случаев, когда логика подсказывает, что постфикс уместен.

Пункт №6 «Более эффективный C ++» - это для меня достаточный авторитет.

Для тех, кто не владеет книгой, вот уместные цитаты. Со страницы 32:

Из ваших дней программирования на C вы можете вспомнить, что префиксная форма оператора инкремента иногда называется «инкремент и выборка», а постфиксная форма часто известна как «выборка и увеличение». Эти две фразы важно запомнить, потому что они действуют как формальные спецификации ...

И на странице 34:

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

Duffymo
источник
4
Если компилятор не понимает, что значение до приращения неестественно, он может реализовать постфиксное приращение в нескольких инструкциях - скопировать старое значение, а затем увеличить. Приращение префикса всегда должно быть одной инструкцией.
gnud
8
Мне довелось проверить это вчера с помощью gcc: в цикле for, в котором значение отбрасывается после выполнения i++или ++i, сгенерированный код тот же.
Джорджио
Попробуйте за пределами цикла for. Поведение в задании должно быть другим.
duffymo
Я категорически не согласен со Скоттом Мейерсом по его второму пункту - обычно это не имеет значения, поскольку 90% или более случаев «x ++» или «++ x» обычно изолированы от любого присваивания, а оптимизаторы достаточно умны, чтобы признать, что временные переменные не нужны создаваться в таких случаях. В этом случае две формы полностью взаимозаменяемы. Смысл этого состоит в том, что старые кодовые базы, пронизанные «x ++», следует оставить в покое - вы с большей вероятностью внесете незначительные ошибки, изменив их на «++ x», чем повысьте производительность где-либо. Возможно, лучше использовать «x ++» и заставить людей задуматься.
omatai
2
Вы можете доверять Скотт Мейерс все , что вы хотите, но если ваш код настолько производительность зависит , что какая -то разница в производительности между ++xи на x++самом деле имеет значение, это гораздо более важно , что вы на самом деле использовать компилятор , который не может полностью и правильно оптимизировать либо версии независимо от того, что в контекст. «Поскольку я использую этот хреновый старый молоток, я могу забивать гвозди только под углом 43,7 градуса» - плохой аргумент в пользу строительства дома, забивая гвозди только под углом 43,7 градуса. Используйте лучший инструмент.
Эндрю Хенле,
28

Из cppreference при увеличении итераторов:

Если вы не собираетесь использовать старое значение, вам следует предпочесть оператор предварительного приращения (++ iter) оператору пост-приращения (iter ++). Постинкремент обычно реализуется следующим образом:

   Iter operator++(int)   {
     Iter tmp(*this); // store the old value in a temporary object
     ++*this;         // call pre-increment
     return tmp;      // return the old value   }

Очевидно, это менее эффективно, чем предварительное увеличение.

Предварительное приращение не создает временный объект. Это может иметь большое значение, если создание вашего объекта дорогое.

Филип Нган
источник
8

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

пример:

pre.cpp:

#include <iostream>

int main()
{
  int i = 13;
  i++;
  for (; i < 42; i++)
    {
      std::cout << i << std::endl;
    }
}

post.cpp:

#include <iostream>

int main()
{

  int i = 13;
  ++i;
  for (; i < 42; ++i)
    {
      std::cout << i << std::endl;
    }
}

_

$> g++ -S pre.cpp
$> g++ -S post.cpp
$> diff pre.s post.s   
1c1
<   .file   "pre.cpp"
---
>   .file   "post.cpp"
голавль
источник
5
Для примитивного типа, такого как целое число, да. Вы проверили, какая разница оказывается для чего-то вроде a std::map::iterator? Конечно, там два оператора разные, но мне любопытно, будет ли компилятор оптимизировать постфиксный префикс, если результат не используется. Я не думаю, что это разрешено - учитывая, что версия postfix может содержать побочные эффекты.
seh
Кроме того, « компилятор, вероятно, поймет, что вам не нужен побочный эффект и оптимизирует его », не должно быть оправданием для написания небрежного кода, в котором используются более сложные постфиксные операторы без какой-либо причины, кроме предположительно того факта, что так много предполагаемые учебные материалы используют постфикс без видимой причины и копируются оптом.
underscore_d
6

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

Однако стоит упомянуть еще одну вещь: большинство компиляторов смогут оптимизировать такие ненужные вещи, когда это возможно, например, оба варианта приведут к одному и тому же коду здесь:

for (int i(0);i<10;++i)
for (int i(0);i<10;i++)
rmn
источник
5

Я согласен с @BeowulfOF, хотя для ясности я всегда выступал бы за разделение заявлений, чтобы логика была абсолютно ясной, то есть:

i++;
x += i;

или

x += i;
i++;

Итак, мой ответ: если вы пишете четкий код, это редко имеет значение (а если это имеет значение, ваш код, вероятно, недостаточно ясен).

Ставки
источник
2

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

Шайлеш Кумар
источник
2
Я просто хочу подчеркнуть, что такой акцент, скорее всего, вводит в заблуждение. Если вы смотрите на какой-то цикл, который заканчивается изолированным «x ++», и думаете: «Ага! - вот почему он работает так медленно!» и вы меняете его на "++ x", то не ждите точно никакой разницы. Оптимизаторы достаточно умны, чтобы понимать, что не нужно создавать временные переменные, когда никто не собирается использовать их результаты. Подразумевается, что старые кодовые базы, пронизанные «x ++», следует оставить в покое - вы с большей вероятностью внесете ошибки, изменив их, чем повысьте производительность где-либо.
omatai
1

Вы правильно объяснили разницу. Это просто зависит от того, хотите ли вы, чтобы x увеличивался перед каждым запуском цикла или после этого. Что подходит, зависит от логики вашей программы.

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

Изменить: исправлено смешение префикса и суффикса.

Бьорн Поллекс
источник
Разговор «до / после» итерации цикла имеет смысл только в том случае, если в условии происходит прибавление / декремент до / после. Чаще он будет в предложении продолжения, где он не может изменить какую-либо логику, хотя для типов классов может быть медленнее использовать постфикс, и люди не должны использовать это без причины.
underscore_d
1

Постфиксная форма оператора ++, - следует правилу " используйте-затем-измените" ,

Форма префикса (++ x, - x) соответствует правилу « измени-затем-используй» .

Пример 1:

Когда несколько значений каскадированы с << с использованием cout, тогда вычисления (если есть) выполняются справа налево, но печать выполняется слева направо, например (если val, если изначально 10)

 cout<< ++val<<" "<< val++<<" "<< val;

приведет к

12    10    10 

Пример 2:

В Turbo C ++, если в выражении обнаружено несколько вхождений ++ или (в любой форме), то сначала вычисляются все префиксные формы, затем вычисляется выражение и, наконец, вычисляются постфиксные формы, например,

int a=10,b;
b=a++ + ++a + ++a + a;
cout<<b<<a<<endl;

Его вывод в Turbo C ++ будет

48 13

В то время как в современном компиляторе он будет выводиться (потому что они строго следуют правилам)

45 13
  • Примечание. Не рекомендуется многократное использование операторов увеличения / уменьшения для одной и той же переменной в одном выражении. Обработка / результаты таких
    выражений варьируются от компилятора к компилятору.
Сунил Диллон
источник
Дело не в том, что выражения, содержащие несколько операций инкремента / декремента, «различаются от компилятора к компилятору», а скорее хуже: такие множественные модификации между точками последовательности имеют неопределенное поведение и отравляют программу.
underscore_d
0

Понимание синтаксиса языка важно при рассмотрении ясности кода. Рассмотрите возможность копирования символьной строки, например, с постинкрементом:

char a[256] = "Hello world!";
char b[256];
int i = 0;
do {
  b[i] = a[i];
} while (a[i++]);

Мы хотим, чтобы цикл выполнялся, встречая нулевой символ (который проверяет ложь) в конце строки. Это требует проверки предварительного приращения значения, а также увеличения индекса. Но не обязательно в таком порядке - способ закодировать это с предварительным приращением:

int i = -1;
do {
  ++i;
  b[i] = a[i];
} while (a[i]);

Это вопрос вкуса, который яснее, и если на машине есть несколько регистров, оба должны иметь одинаковое время выполнения, даже если a [i] - функция, которая является дорогой или имеет побочные эффекты. Существенная разница может заключаться в выходном значении индекса.

шкейсер
источник