4.5. Операторы инкремента и декремента

Операторы инкремента (++) и декремента (--) позволяют в краткой и удобной форме добавить или вычесть единицу из объекта. Эта форма записи обеспечивает не только удобство, она весьма популярна при работе с итераторами, поскольку большинство итераторов не поддерживает арифметических действий.

Эти операторы существуют в двух формах: префиксной и постфиксной. До сих пор использовался только префиксный оператор инкремента (prefix increment). Он осуществляет инкремент (или декремент) своего операнда и возвращает измененный объект как результат. Постфиксный оператор инкремента (postfix increment) (или декремента) возвращает копию первоначального операнда неизменной, а затем изменяет значение операнда.

int i = 0, j;

j = ++i; // j = 1, i = 1: префикс возвращает увеличенное значение

j = i++; // j = 1, i = 2: постфикс возвращает исходное значение

Операндами этих операторов должны быть l-значения. Префиксные операторы возвращают сам объект как l-значение. Постфиксные операторы возвращают копию исходного значения объекта как r-значение.

Совет. Используйте постфиксные операторы только по мере необходимости

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

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

Объединение операторов обращения к значению и инкремента в одном выражении

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

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

auto pbeg = v.begin();

// отображать элементы до первого отрицательного значения

while (pbeg != v.end() && *beg >= 0)

 cout << *pbeg++ << endl; // отобразить текущее значение и

                          // переместить указатель pbeg

Выражение *pbeg++ обычно малопонятно новичкам в языках С++ и С. Но поскольку эта схема весьма распространена, программисты С++ должны понимать такие выражения.

Приоритет постфиксного оператора инкремента выше, чем оператора обращения к значению, поэтому код *pbeg++ эквивалентен коду *(pbeg++). Часть pbeg++ осуществляет инкремент указателя pbeg и возвращает как результат копию предыдущего значения указателя pbeg. Таким образом, операндом оператора * будет неувеличенное значение указателя pbeg. Следовательно, оператор выводит элемент, на который первоначально указывал указатель pbeg, а затем осуществляет его инкремент.

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

Совет. Краткость может быть достоинством

Такие выражения, как *iter++, могут быть не очевидны, однако они весьма популярны. Следующая форма записи проще и менее подвержена ошибкам:

cout << *iter++ << endl;

чем ее более подробный эквивалент:

cout << *iter << endl;

++iter;

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

Помните, что операнды могут быть обработаны в любом порядке

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

Для иллюстрации проблемы перепишем цикл из раздела 3.4.1, который преобразует в верхний регистр символы первого введенного слова:

for (auto it = s.begin(); it != s.end() && !isspace(*it) ; ++it)

 it = toupper(*it); // преобразовать в верхний регистр

Этот пример использует цикл for, позволяющий отделить оператор обращения к значению beg от оператора его приращения. Замена цикла for, казалось бы, эквивалентным циклом while дает неопределенные результаты:

// поведение следующего цикла неопределенно!

while (beg != s.end() && !isspace(*beg))

 beg = toupper(*beg++); // ошибка: это присвоение неопределенно

Проблема пересмотренной версии в том, что левый и правый операнды оператора = используют значение, на которое указывает beg, и правый его изменяет. Поэтому присвоение неопределенно. Компилятор мог бы обработать это выражение так:

*beg = toupper(*beg);       // сначала обрабатывается левая сторона

*(beg + 1) = toupper(*beg); // сначала обрабатывается правая сторона

Или любым другим способом.

Упражнения раздела 4.5

Упражнение 4.17. Объясните различие между префиксным и постфиксным инкрементом.

Упражнение 4.18. Что будет, если цикл while из последнего пункта этого раздела, используемый для отображения элементов вектора, задействует префиксный оператор инкремента?

Упражнение 4.19. С учетом того, что ptr указывает на тип int, vec — вектор vector<int>, a ival имеет тип int, объясните поведение каждого из следующих выражений. Есть ли среди них неправильные? Почему? Как их исправить?

(a) ptr != 0 && *ptr++     (b) ival++ && ival

(с) vec[ival++] <= vec[ival]