Постфиксные инкремент и декремент
Операции «++» и «--» широко используются в языках с С-подобным синтаксисом.
Перед обсуждением этих операций необходимо сразу же сделать оговорку.
Здесь анализируется не принцип записи обозначения операции после операнда,
а свойство таких операций выполнять действие над операндом по окончании всех остальных операций.
В чём аномальность этих операций?
В справочных руководствах по С/С++ унарные операции «++» и «--» как в префиксном,
так и в постфиксном варианте имеют приоритет, равный 1 (всего приоритетов 16, от 0 до 15; 0 — наивысший приоритет).
Для префиксных операций это справедливо безо всяких замечаний.
Постфиксные «++» и «--» же при синтаксическом анализе так же имеют приоритет, равный 1.
А при выполнении появляется «фокус»: они выполняются в последнюю очередь, после выполнения всего выражения.
Получается, что эти операции помимо обычного существования имеют «параллельную реальность».
В алголоподобных языках операции и функции могут иметь операнды в количестве от нуля и более.
Но один операнд может принадлежать только одной операции или функции.
Но в С/С++ это не так.
«Отклонением от нормы» как раз являются «++» и «--».
Возьмём пример:
f(a++);
Аргумент «а» сначала является операндом функции «f()»,
а затем , после её выполнения, операнд «а» является операндом операции «++».
Операнд «а» один, а операций применяется к нему две!
В примере
f(++a);
такого не возникает: сначала выполняется «++», а затем результат этой операции является аргументом функции «f()».
Можно было бы предположить, что операция «++» просто имеет самый низкий приоритет.
И по этой причине она выполняется в последнюю очередь.
Опровергаем это, используя скобки для повышения приоритета:
a = (b++);
«a» будет присвоено значение «b», а не «b+1»!
Какой вывод из всего этого можно сделать?
1) Свойство постфиксных операций «++» и «--» выполняться после выполнения всего выражения противоречит общепринятому,
что операция или функция может иметь несколько операндов, но операнд относится только к одной операции или функции.
2) Эти противоречивые операции усложняют язык и компилятор соответственно;
это повод исключить подобные операции из языка, тем более что теряем не многое.
3) Удаление этих операций из языка приведёт к небольшому удлинению программ.
В примере на C вместо
do { *dst++ = *src++;} while (*src);
придётся писать:
do { *dst = * src; ++dst; ++src;) while (*src);
Это не очень дорогая расплата.
Зато отсутствуют «фокусы» и «трюки».
Есть ещё одна причина «не любить» эти операции.
Рассмотрим переопределение операции «++» в C++:
BigInt& operator++(BigInt& op1, BigInt op2) { ... }; // Это постфиксный инкремент
BigInt& operator++(BigInt& op1) { ... }; // А это префиксный инкремент
Чем отличаются эти определения?
Постфиксный инкремент имеет дополнительный операнд, единственное назначение которого —
сигнализировать о том, что инкремент постфиксный, а не префиксный.
Но средствами языка невозможно повторить такой трюк для, допустим, префиксного унарного минуса.
И для любой другой операции.
Это «зашито» в язык «шаманским» образом.
Но в языке не должно быть «магии», всё должно быть логичным и подчиняться общим правилам!
Меньше шаманства — меньше танцев с бубнами.
И третья причина «не любить» постфиксные «++» и «--».
Вы обратили внимание, что синтаксис этих операций при объявлении и употреблении разный?
Постфиксные «++» и «--» — это унарные операции, но объявляются как бинарные.
Опять нелогичность!
Операции и функции в языке должны и объявляться, и употребляться одним и тем же способом!
Но последнее замечание относится к реализации этих операций в C-подобных языках.
Вновь разрабатываемые языки могут избежать подобного недостатка.
Опубликовано: 2012.09.25, последняя правка: 2014.12.23 13:37
Отзывы
2014/04/26 11:45, Utkin #
Как сторонник Паскаля, вообще не вижу смысла в подобных операциях. Есть вполне себе работающее а=а+1. Для чего городить огород. Сам факт существования Паскаля и его использование для написания вполне рабочих программ говорит о том, что данные операции не обязательны вообще. Можно рассматривать как частный случай синтаксического сахара.
2014/05/01 15:05, Автор сайта #
Это краткая и удобная запись, позволяющая записывать имя объекта только единожды. Поэтому она и популярная, несмотря на свою «антинаучность». В Хаскеле нет таких операторов, но для императивных языков они удобны.
2014/06/17 04:44, 87.241.204.6 #
Это краткая и удобная запись, позволяющая записывать имя объекта только единожды И источник путаницы. На современном этапе основная проблема не в написании программы (при желании можно все очень сильно автоматизировать), а в её отладке, тестировании и дальнейшем сопровождении. Если говорить об одиночных разработках, то на данный факт можно закрыть глаза. Если говорить о промышленном программировании, то это источник бед. Я крайне негативно отношусь к смешиванию различных видов записи подобных операций. Есть удобное "человеческое" представление, большой практический опыт показал, что эксперименты на программистах с/с++ закончились неудачей. Программирование не должно коробить мыслительные процессы (итак алгоритм переводится на язык программирования, так ещё в самом языке для понимания кода требуются дополнительные преобразования). Все эти переводы из одной системы представления в другую источник ошибок. Если же форма записи привлекательна, то должна быть единственной во всей программе и никак иначе.
2014/06/17 16:16, Автор сайта #
А какую запись Вы считаете человеческой? А := А + 1 — это лучше, чем ++ А? И с какой точки зрения запись «А := А + 1» лучше — с точки зрения школьника? Когда я был школьником, то вообще не представлял, что делает операция «&». Но вуз помог потом. Теперь понимаю, что делают «&» и «++». Они по своей сложности не очень отличаются.
2014/06/19 10:02, utkin #
Нет, я о том, что должна быть единая форма записи без смесей. И желательно как можно ближе к естественной форме записи. А:=А+1 совершенно не обязательно. Можно и А=А+1.
2014/06/19 17:47, Автор сайта #
В функциональном программировании вообще предпочитают не модифицировать значения имеющихся объектов, а создавать новые объекты на основании старых:В = А + 1 Так легче раскрутить цепочку вычислений на предмет её доказательной правильности.
Лично для меня инкремент столь же естественен, как и А + 1, ещё со времён Clipper и Forth. Си был для меня третьим языком, где я увидел инкремент :)
2014/06/23 09:25, utkin #
Ну вот опять Ваши личные предпочтения. Вы же не один будете программировать на новом языке, или один? Ориентироваться на Вас (как и на любого другого) заведомо провальное дело. Ориентироваться нужно на большинство. А большинство вполне естественно воспринимает А=А+1, а вот насчет а++ будет путаница. Именно из-за сочетания обоих вариантов. Также как такой инкремент порождает ошибки в C/C++ где есть и ++а и а++ и а=а+1. В комбинации со скобками это создает вырвиглазные выражения. И если один выпендрился, то остальные путаются. Для промышленных масштабов это абсолютно не приемлемо. Код должен быть ясен большинству участников проекта, а не только его создателю.
2014/06/23 13:48, Автор сайта #
Большинство таки приемлет инкремент и декремент. Это личные предпочтения всех, кто программирует на Си-подобных языках: C, C++, C#, Objective C, D, Java, Cyclone и т.д. Если заглянуть в рейтинг Tiobe, то суммарный рейтинг этих языков — 2/3, т.е. «конституционное большинство». Каков процент тех, кому нравится — трудно сказать, но они его приемлют — это точно. Иначе бы они не пользовались этими языками. Вам, как поклоннику Паскаля, это не нравится. Но что поделаешь, когда речь идёт о большинстве.
2014/06/30 12:23, utkin #
Это личные предпочтения всех, кто программирует на Си-подобных языках: C, C++, C#, Objective C, D, Java, Cyclone и т.д. Я не согласен с Вами, так как следует разделять программирование «для себя» и промышленное программирование. В последнем случае язык навязывает работодатель — не попрётесь же с Паскалем там где пишут на C/C++. Поэтому тут тонкости насчет «личных предпочтений».
2014/06/30 17:28, Автор сайта #
Похоже, Вам трудно представить, что инкремент и декремент могут употреблять добровольно. Был такой «всесоюзный староста» Михаил Иванович Калинин, который пить не любил, но ему приходилось это делать, чтобы не быть белой вороной в когорте сталиных, молотовых, кагановичей. Он недоумевал по поводу водки: «Как её могут беспартийные пить?». В его голове не укладывалось, что водку можно пить в удовольствие, а не по долгу службы. Между прочим, в C++ ничто не препятствует отказу от инкремента и декремента. Возьмём такой код:int array[10]; int* ptr = array; ++ ptr; *ptr = 99; // array[1] теперь равен 99 ptr = ptr + 1; // это по-паскалевски! *ptr = 98; // array[2] теперь равен 98 Т.е. программированию без инкремента нет препятствий. Но программистам они нравится, они предпочитают более короткий код. Поэтому пользуют его и в хвост, и в гриву.
Кстати, о большинстве. Где-то читал примерно такой совет: «Если хотите достичь своей цели, никогда не ориентируйтесь на большинство. Иначе бы мы до сих пор жили на земле, которая покоится на трёх китах, не открыли бы Америки и не летали бы в атмосфере и космосе — ведь это невозможно».
2015/02/11 17:19, Автор комментария #
Т.е. я так понял из комментариев, что никто не знает операцию inc в Паскале?
2015/02/11 20:40, Автор сайта #
Автор Паскаля Никлаус Вирт выступал против Си-шных операций инкремента/декремента.
2015/09/03 14:49, Александр #
Отличный урок How to по работе инкремента и декремента в C# — https://www.youtube.com/watch?v=FTuLlHBRZxs
2016/04/29 04:29, Олег #
Utkin:Сам факт существования Паскаля и его использование для написания вполне рабочих программ говорит о том, что данные операции не обязательны вообще. Можно рассматривать как частный случай синтаксического сахара. Ты серьезно?! Сам факт СУЩЕСТВОВАНИЯ Паскаля?! Что-то Вас занесло, уважаемый ))
2016/04/29 11:30, Автор сайта #
А ещё сам факт существования Brainfuck говорит о том, что в языке программирования можно обойтись без букв.
2016/05/05 14:18, utkin #
Ты серьезно?! Сам факт СУЩЕСТВОВАНИЯ Паскаля?! Что-то Вас занесло, уважаемый )) Конечно. Он существует: а) давно, б) используется для обучения, в) используется в реальных проектах, г) имеет также и всяческие ответвления навроде языка Дельфи.
Все это говорит о том, что операции вида а++ НЕОБЯЗАТЕЛЬНЫ для использования. То есть синтаксический сахар. Никакого преимущества они не дают. Вопрос в эстетике — кому-то нравится или легче воспринимается, а для кого-то это противоестественная запись.
2016/05/05 14:19, utkin #
А ещё сам факт существования Brainfuck говорит о том, что в языке программирования можно обойтись без букв. А он используется в реальной работе?
2016/05/05 14:36, Автор сайта #
Просто я хотел сказать, что сам факт существования в данном случае ничего не значит.
2016/08/06 00:11, Александр #
1. Постфиксный инкремент/декремент имеют приоритет 1, префиксный инкремент/декремент имеют приоритет 2. 2. Приоритетов 18. 3. Не каждая функция примет аргумент (a++), в связи с тем, что постфиксные инкремент/декремент порождают временную переменную не LVALUE. Таким образом, следующая функция вызовет ошибку при попытке передать ей a++void f(int& val){/* */}; 4. Попробуйте скопировать простую строку "тест\0" предложенным Вами кодомdo { *dst = * src; ++dst; ++src;) while (*src); И последнее. Возможно, у постфиксных инкремента/декремента должны быть подобные свойства?
2016/08/23 03:59, xDDGx #
Почему бы тогда не оставить постфиксные ин/декремент, но дать им минимальный (разумный) приоритет? Никакой магии, а возможность написатьdo { *dst++ = *src++;} while (*src); останется.
Почему-то никто не вспомнил ещё про такую конструкцию: a+=1. Длиннее, чем ++a, но короче a=a+1, да и функциональнее первого (шаг может быть и 2, и 3, и нецелым, и выбор операций больше — даже сдвиги — а не только +/-).
Александр, если вы имеете в виду, что не копируется \0, то исходным do { *dst++ = *src++;} while (*src); достигается то же самое, их функционал эквивалентен. В конце концов, это всего лишь пример...
2016/10/31 08:43, rst256 #
Теперь понимаю, что делают «&» и «++». Они по своей сложности не очень отличаются. Может, вы знаете что они делают «&» и «++» в указанном ниже коде? void iseq(int a, int b){ assert(a==b); } void ismt(int a, int b){ assert(a+1==b); }
int main(int argc, char **argv){
{ int j=100, *jp; jp=&j; iseq(j++, j); assert(j==101); } //100, 100 { int j=100, *jp; iseq(j++, j); jp=&j; assert(j==101); } //100, 100 { int j=100, *jp; iseq(j++, j); assert(&j && j==101); } //100, 100 { int j=100, *jp; ismt(j++, j); assert(j==101); } //100, 100
{ int j=100, *jp; jp=&j; printf("\n%d, %d\n", j++, j); assert(j==101); } //100, 100 { int j=100, *jp; printf("%d, %d\n", j++, j); jp=&j; assert(j==101); } //100, 100 { int j=100, *jp; printf("%d, %d\n", j++, j); assert(&j && j==101); } //100, 100 { int j=100; printf("%d, %d\n", j++, j); assert(j==101); } //100, 101
{ int j=100, *jp; jp=&j; printf("\n%d, %d\n", ++j, j); assert(j==101); } //101, 100 { int j=100, *jp; printf("%d, %d\n", ++j, j); jp=&j; assert(j==101); } //101, 100 { int j=100; printf("%d, %d\n", ++j, j); assert(j==101); } //101, 101 return 0; }
2017/08/03 12:30, Алексей #
Написано:Аргумент «а» сначала является операндом функции «f()», а затем , после её выполнения, операнд «а» является операндом операции «++». Проблема в словах «после её выполнения». Это следует исправить на:Аргумент «а» копируется в операнд функции «f()», затем, до выполнения тела функции, над «а» производится «++», и затем выполняется тело функции «f()» с исходным операндом. Иначе говоря, на момент исполнения кода «f()», над «а» уже произведен постфиксный инкремент, хотя операнд она получит не инкрементированный.
Именно таково поведение языка C++ по стандарту: C++ standard, Program execution 1.9.15:, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2798.pdf
А также языка C. Это также проверяется поведением реальных компиляторов. Причём описанная ситуация становится сложнее. И, вообще говоря, поведение не определено в случае f(a++, a++); Здесь запятая «,» не является оператором запятой.
2022/04/29 15:34, stein47 #
Использование инкремента и декремента очень удобно. Но чревато "фокусами" и ошибками. Причем источником ошибок является программист, а не компилятор. Компилятор разберет и префиксный и постфиксный вариант без проблем. Нам нужно, сохранив удобство, снизить вероятность ошибок. Предложен вариант отказаться от постфиксного инкремента/декремента. Лично я почти всегда пользуюсь постфиксной записью. И даже не потому, что мне нужен "магический" отложенный эффект, а просто такая запись более эргономична. a++; ++b; Первая запись банально удобнее при наборе, а в данном контексте смысл равнозначен. Подвожу свою мысль к завершению: можно оставить только постфиксную запись, но смысл приравнять к префиксной. Правда, это уже разрыв "сишного наследия". И это "наследие" снова станет потенциальным источником ошибок...
2022/04/29 15:47, Gudleifr #
Но чревато "фокусами" и ошибками Все "нормальные языки" содержат в себе два практически непересекающихся языка: операторов и выражений. И для языка выражений до сих пор действует правило скриптов — любой возможной лексеме нужно приписать полезное значение, дабы вместить в одну строку как можно больше финтов. По идее, языковое творчество должно бы идти как раз по пути выделения языка операторов и обеспечения возможности переключения с одного языка выражений на другой на лету.
2022/04/29 16:18, Автор сайта #
не потому, что мне нужен "магический" отложенный эффект, а просто такая запись более эргономична. можно оставить только постфиксную запись, но смысл приравнять к префиксной. Правда, это уже разрыв "сишного наследия". Совершенно с Вами согласен. Правда, эти операторы с точки зрения функционального программирования изменяют состояние, а надо бы стремиться к иммутабельности.
2024/03/27 00:00, alextretyak #
И всё-таки, иногда постфиксные операции значительно удобнее. Вот у меня есть такой метод в классе: uint8_t read_byte() { return buffer[buffer_pos++]; } Если отказаться от постфиксного инкремента, тогда придётся этот метод переписать так: uint8_t read_byte() { uint8_t result = buffer[buffer_pos]; ++buffer_pos; return result; } Мало того, что строчек кода в теле метода стало в три раза больше, так ещё и пришлось вводить лишнюю сущность и давать ей имя (временная переменная result)...., а свойство таких операций выполнять действие над операндом по окончании всех остальных операций. А если лишить эти операции такого свойства? Что, если постфиксные инкремент и декремент всегда будут определены таким образом:template <typename T> T operator++(T& a, int) { T temp = a; // для типа `T` должен быть определён конструктор копирования ++a; // для типа `T` должен быть определён префиксный оператор `++` return temp; } // и аналогично для `--` В этом случае появляется сразу несколько плюсов:- поведение постфиксных операций становится полностью детерминированным: выражение a++ − a++ всегда возвращает -1 и при этом увеличивает a на 2; правда при условии, что компилятор при расчёте разности вычисляет уменьшаемое (левый операнд) всегда перед вычитаемым (правый операнд);
- исчезает различие в поведении постфиксных операций для встроенных в язык типов (int, float и др. — для них «++» и «--» выполняются после выполнения всего выражения) и для пользовательских типов (для них применяются переопределённые постфиксные «++» и «--»);
- пропадает «магия» в виде дополнительного неиспользуемого аргумента при переопределении операций «++» и «--», т.к. программист вообще не может переопределить постфиксные «++» и «--», а только префиксные (постфиксные всегда генерируются компилятором автоматически);
- это упрощает реализацию, т.к. запись a++ компилятор может просто заменять на вызов служебной функции postfix_increment(a), которая реализована аналогично:
template <typename T> T postfix_increment(T& a) { T temp = a; ++a; return temp; } Ну и в качестве оптимизации, считаю, что операция a++ должна автоматически заменяться компилятором на ++a в том случае, когда результат операции не используется. Всё-таки постфиксный инкремент выглядит красивее префиксного. Неспроста же Бьёрн назвал язык C++, а не ++C. :)(:
2024/03/27 08:01, kiv #
ИМХО обсуждение Си и Си++ как одного языка в корне не корректно! Если при написании кода на Си Вы не представляете в голове хотя бы приблизительно, какой машинный код будет сгенерирован, или как минимум никогда не интересовались для общего понимания ассемблерным листингом компиляции, то постарайтесь исключить Си из своих инструментов и перестаньте обсуждать его достоинства и недостатки.
2024/03/27 16:08, Автор сайта #
постарайтесь исключить Си из своих инструментов и перестаньте обсуждать его достоинства и недостатки. Знаете, Вас никто не называл земляным червяком, поэтому можно обсуждать идеи, ничего не рекомендуя (как учил Жванецкий) участникам дискуссии. Ведь высказывание было по теме. Поэтому надо критиковать смысл сказанного, а не людей.
Если сделать одинаковой семантику префиксных и постфиксных инкрементов и декрементов, то одного из видов можно избавиться. Выгоднее избавиться от постфиксных, чтобы избавить язык от всех постфиксных операций, от этого упрощается синтаксис языка. Хотя постфиксные инкременты и декременты можно оставить как синтаксический сахар к префиксным.
Хотя, конечно, жаль, что больше не будет гениальной по лаконичности записи do { *dst++ = *src++;} while (*src); Есть ещё одна вещь, которая против шерсти не соответствует традициям Си. Выскажу крамольную мысль, что изменение какого-либо значения в одном флаконе с его использованием в качестве аргумента не есть хорошо. Процитирую Википедию, статью «Синтаксис и семантика языка Си»:В следующем примере переменная изменяется трижды между точками следования, что приводит к неопределённому результату: int i = 1; // Описатель - первая точка следования, полное выражение - вторая i += ++i + 1; // Полное выражение - третья точка следования printf("%d\n", i); // Может быть выведено как 4, так и 5 Другие простые примеры неопределённого поведения, которого необходимо избегать: i = i++ + 1; // неопределённое поведение i = ++i + 1; // тоже неопределённое поведение
printf("%d, %d\n", --i, ++i); // неопределённое поведение printf("%d, %d\n", ++i, ++i); // тоже неопределённое поведение
printf("%d, %d\n", i = 0, i = 1); // неопределённое поведение printf("%d, %d\n", i = 0, i = 0); // тоже неопределённое поведение
a[i] = i++; // неопределённое поведение a[i++] = i; // тоже неопределённое поведение
2024/03/27 19:12, MihalNik #
избавить язык от всех постфиксных операций, от этого упрощается синтаксис языка Постфиксные операции не усложнят синтаксис и семантику, если не будут возвращать значений.изменение какого-либо значения в одном флаконе с его использованием в качестве аргумента не есть хорошо Вот и я про тоже. Да, краткость хороша, но в какой-то мере, иначе код придется дольше разглядывать и разгадывать. Перл тому отличный пример.Если отказаться от постфиксного инкремента, тогда придётся этот метод переписать так Есть ещё один "крамольный" вариант — разрешить операторы после "return", тогда строки будет две, а не три, без лишних переменных. А return можно переименовать в result. Добавить свой отзыв
Написать автору можно на электронную почту mail(аt)compiler.su
|