Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Новости и прочее

Постфиксные инкремент и декремент

Операции «++» и «--» широко используются в языках с С-подобным синтаксисом. Перед обсуждением этих операций необходимо сразу же сделать оговорку. Здесь анализируется не принцип записи обозначения операции после операнда, а свойство таких операций выполнять действие над операндом по окончании всех остальных операций.
постфиксные инкремент и декремент


         В чём аномальность этих операций? В справочных руководствах по С/С++ унарные операции «++» и «--» как в префиксном, так и в постфиксном варианте имеют приоритет, равный 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

ОценитеОценки посетителей
   ████████████████████████████ 34 (66.6%)
   ███████ 8 (15.6%)
   ███ 3 (5.88%)
   █████ 6 (11.7%)

Отзывы

     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.

     2024/04/01 14:10, veector          # 

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

    uint8_t read_byte()
{
return buffer[buffer_pos++];
}
Когда я вижу такой код на ревью (проверке), то сразу выдаю минус в карму.

     2024/04/01 14:27, veector          # 

Есть ещё один "крамольный" вариант — разрешить операторы после "return", тогда строки будет две, а не три, без лишних переменных. А return можно переименовать в result.

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

     2024/04/02 00:11, Автор сайта          # 

buffer_pos++

разрешить операторы после "return", тогда строки будет две, а не три, без лишних переменных.

В приведённых выше примерах есть неясность. Если buffer_pos локальная, то вообще непонятно, зачем её увеличивать перед выходом из функции, ведь её значение не возвращается оператором return. Если она глобальная, то смысл увеличения есть, но есть резонный вопрос — зачем она глобальная?!

Есть ещё вариант, когда buffer_pos — волатильная, тогда изменять её могут для произведения побочного эффекта. Но этот вариант вообще ни в какие ворота не лезет.

     2024/04/04 00:00, alextretyak          # 

MihalNik

Есть ещё один "крамольный" вариант — разрешить операторы после "return", тогда строки будет две, а не три, без лишних переменных. А return можно переименовать в result.

Хороший вариант, согласен. Жаль только, что не так много языков программирования, которые поддерживают специальную переменную result.

Автор сайта

Если buffer_pos локальная

Как же она может быть локальной, когда в теле метода она не объявляется? Да, приведённая строка кода является полным телом метода read_byte().

Если она глобальная, то смысл увеличения есть, но есть резонный вопрос — зачем она глобальная?!

Из моих слов «такой метод в классе» можно было догадаться, что в коде речь идёт о переменных-членах класса. И buffer, и buffer_pos являются переменными-членами.

veector

то сразу выдаю минус в карму.

Просто выдаёте и всё? А конкретные советы/рекомендации (о том, какой код был бы лучше в данном случае) Вы не даёте из принципа, я так полагаю. 🤣

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

Почему Вы так не любите конкретику? Что мешало сразу просто указать название этого языка программирования, как будто это прям какая-то коммерческая тайна.

Из более-менее популярных языков программирования, которые поддерживают специальную переменную для возврата значения, я могу назвать только BASIC, Pascal/Delphi и Nim (причём в первых двух эта переменная является именем функции, а в последнем используется специальная переменная result). Но в C++ такой возможности нет, а потому вопрос «а как более правильно реализовать return buffer[buffer_pos++];» остаётся открытым.

     2024/04/05 12:36, veector          # 

alextretyak, не, я добрый, всегда, всем, все разъясняю.

     2024/04/07 00:00, alextretyak          # 

всегда, всем, все разъясняю.

Ну, в таком случае, хотелось бы услышать (хотя бы краткое) разъяснение, что именно вас не устраивает в процитированном коде и какой код был бы лучше в данном случае?

     2024/04/08 17:52, veector          # 

alextretyak, ну тут всё же просто: Вы же считываете значение из массива, 1 байт, не проверяя номер элемента.

     2024/04/08 23:43, Автор сайта          # 

alextretyak, поделитесь тайным знанием, почему ваши сообщения в последнее время всегда делаются в 00:00 🙃

     2024/04/10 00:00, alextretyak          # 

veector

Вы же считываете значение из массива, 1 байт, не проверяя номер элемента.

Ах, вот оно в чём дело. Просто название обсуждаемой статьи — «Постфиксные инкремент и декремент», поэтому я решил немного сократить тело метода read_byte() для наглядности. Тем более, что в C++ такой код вполне может быть допустим в том случае, когда buffer — это не просто указатель или массив в стиле Си, а объект-экземпляр класса массива с контролем выхода за границы. Тогда внутри перегруженного operator[] будет та самая «проверка номера элемента», о которой вы говорите и которую в языке Си пришлось бы вставлять в код явно.

Вообще, полный код метода read_byte(), который используется в реальном проекте (https://github.com/alextretyak /file_for_humans/blob /master/IFile.hpp), выглядит так:
    uint8_t read_byte()
{
if (at_eof())
throw UnexpectedEOF();
return buffer[buffer_pos++];
}
Метод at_eof(), несмотря на название, не только проверяет на конец файла, а очень много чего делает: аллоцирует buffer, если он ещё не был проаллоцирован, читает из файла данные в buffer, если buffer_pos указывает на конец буфера, и при этом сбрасывает buffer_pos в 0 и обновляет позицию начала буфера в файле для корректной работы метода tell(), а если прочитать файл не удалось, то порождает исключение (таким образом, к моменту выполнения кода buffer[buffer_pos++] содержимое buffer уже подготовлено и buffer_pos гарантированно находится в допустимых пределах). Но даже такое тело метода вполне можно впихнуть в один return:
    uint8_t read_byte()
{
return !at_eof() ? buffer[buffer_pos++] : throw UnexpectedEOF();
}
Просто я не люблю использовать throw в выражениях, поэтому и не стал так писать в реальном коде. Но возвращаясь к теме статьи: против использования постфиксного инкремента в теле метода read_byte() вы ничего не имеете?

Автор сайта

поделитесь тайным знанием, почему ваши сообщения в последнее время всегда делаются в 00:00

Ну, не сказать, чтобы в «последнее». Уже более 4-х с половиной лет, начиная с этого сообщения. 😁

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

Почему я выбрал время отправки сообщений именно 00:00 по Москве? Ну, с одной стороны, в этом есть что-то по-программистски красивое. А с другой, это оказалось ещё и очень удобное для меня время: во Владивостоке это 7:00 утра, и я успеваю на свежую голову ещё разок хорошенько обдумать сообщение перед отправкой.

Если интересует техническая сторона вопроса, то никакими скриптами/ботами я не пользуюсь. Просто сверяю системное время с https://time.is и нажимаю кнопку отправки сообщения примерно в 7:00:30. Вероятность того, что время на сервере, куда я отправляю сообщение, расходится с time.is более чем на 30 секунд очень мала, поэтому пока что получалось отправлять сообщения без ошибок точно в 00:00.

     2024/04/10 17:55, veector          # 

alextretyak, я не занимаюсь разработкой языков и компиляторов, хотя потенциально умею это делать. Т.е. я как бы являюсь пользователем языков программирования и компиляторов, поэтому, мои ответы стоит воспринимать как "просто скромное мнение одного из пользователей".

К конструкциям языка вида "var++" и "--var" у меня очень простое отношение, как к удобному инструменту (типа "синтаксического сахара"), а не как к смысловой части языка (и/или компилятора). Соответственно, как любой инструмент, его можно приметь в дело и не в дело.

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

Вот так я НЕ делаю:
 do { *dst++ = *src++;} while (*src);
А вот так делаю:
 do { *dst = *src; dst++; src++; ) while (*src);

Но возвращаясь к теме статьи: против использования постфиксного инкремента в теле метода read_byte() вы ничего не имеете?

Я не против постфискного и префиксного инкрементов, но считаю, что в теле метода read_byte() в виде buffer[buffer_pos++] он неуместен, а программа, которой вместо простой проверки границ приходится отлавливать исключения — плохо спроектирована (не обижайтесь, но это мое мнение). Как бы ни было принято большинством в мире, наличие в тексте программы исключений и ассертов рантайма, для меня это признаки плохо спроектированной программы.

Вместо чтения read_byte() и буфера я использую другие методы и понятия: поток и извлечение информации из потока.
// Простите, но я люблю Си, поэтому, будет чистый Си, а в C++ вы уж сами переведете.
bool stream_get_byte(stream_t *stream, uint8_t *byte_ptr);
int stream_get_byte(stream_t *stream);
// + #define STREAM_EMPTY (-1), результат вне кодировки байта.

// К буферам это всё тоже применимо и тоже использую:
bool buffer_get_byte(buffer_t *buffer, uint8_t *byte_ptr);
int buffer_get_byte(buffer_t *buffer);
// + #define BUFFER_EMPTY (-1), результат вне кодировки байта.
Причем, слово get означает извлечение байта и это, на мой скромный взгляд, очень правильная по смыслу и достаточно частая операция с потоками и буферами.

Просто я не люблю использовать throw в выражениях, поэтому и не стал так писать в реальном коде.

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

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

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

     2024/04/11 00:08, Автор сайта          # 

Ах, вот оно в чём дело.

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

Уже более 4-х с половиной лет

Только недавно обратил внимание.

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

Без сомнения. Но тогда голова занята ответом. А ответы не всегда хочется давать, потому что они неоднократно давались, а одни и те же вопросы всё равно задаются и поднимаются. А на это уходит драгоценное время, которое могло бы быть потречено с большей пользой. Но Ваше мнение всё равно приветствуется. 🤣

наличие в тексте программы исключений и ассертов рантайма, для меня это признаки плохо спроектированной программы.

Джоэл Спольски справедливо указывает, что исключения значительно хуже, чем «goto». Впрочем, можно обойтись без них, решения есть.

     2024/04/21 00:00, alextretyak          # 

veector

Так исторически сложилось, что я больше пишу на Си с применением парадигмы ООП и мне ни разу не потребовалось применять исключения ни в одном крупном проекте.

Мне, откровенно говоря, тоже. Самый крупный мной разработанный проект содержал порядка 30 тыс. строк кода на C++ — графический движок (3D-рендеринг, а также 2D GUI) для одной не слишком известной компьютерной игры. В коде как движка, так и самой игры (не считая серверную часть) не использовались ни исключения, ни динамическая идентификация типов (RTTI), ни STL и практически не использовалась даже crt. И весь код (не считая серверную часть) был написан на C++03, т.к. в 2009 году C++11 ещё не было.

Но, тем не менее, я бы не взял на себя смелость утверждать, что использование исключений, RTTI и STL — это признаки плохо спроектированной программы.

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

После того, как я познакомился с Python, моё мнение об исключениях значительно изменилось. Если в C++ неотловленное в рантайме исключение выдавало совершенно невнятную ошибку, непонятную ни пользователю, ни программисту, то в Python сразу было понятно, где и в чём проблема. В простой консольной программе на Python можно вообще не проверять ошибки открытия файлов, т.к. если функция open() не смогла открыть файл и вызвала необработанное исключение, то в консоли отобразится не только понятный тип ошибки (FileNotFoundError) и полный стек вызовов (call stack) с указанием строк кода, но и само имя файла, который пытались открыть.

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

И хотя реализация идеи о том, что на компилируемых языках можно писать надёжный и эффективный код, в котором бы не нужно было вставлять проверки на каждый чих (например, на каждый вызов функции чтения блока данных из файла), ещё требует доработки, но это не значит, что в этом направлении вообще не нужно двигаться. Собственно, представленный мной код метода read_byte(), входящий в состав ffh — библиотеки для удобного, безопасного и эффективного чтения файлов, и является моей попыткой двигаться в этом направлении.

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

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

Зачем нужна буферизация, особенно при побайтовом чтении? Дело в том, что каждый вызов ReadFile() (если говорить про Windows) или read() (в POSIX) осуществляет переход в ядро и обратно (порядка 3000 тактов) и вызывать ReadFile() для чтения из файла маленькими блоками будет крайне неэффективно. По моим замерам каждый вызов ReadFile() для чтения всего одного байта обходится более чем в 8000 тактов даже при условии, что читаемый файл уже содержится в кэше операционной системы. Что мешает читать файл большими блоками или вообще сразу целиком в память? Первое не всегда удобно, а второе — не всегда приемлимо.

Допустим, у нас есть многогигабайтный текстовый файл и мы хотим не загружая его в память целиком подсчитать в нём количество строк в стиле Windows, т.е. сколько раз в файле встречается пара символов "\r\n". Если бы мы считали просто количество одиночных символов (количество '\n', например), тогда можно было бы читать файл большими блоками и просто подсчитывать количество байт с кодом символа '\n' внутри каждого прочитанного блока. Но в случае "\r\n" так просто уже не получится, т.к. возможны ситуации, когда прочитанный блок оканчивается на символ '\r', а для того, чтобы узнать следующий символ, необходимо прочитать следующий блок из файла. При этом, необходимо запомнить, что предыдущий блок оканчивался на символ '\r'.
Данный алгоритм можно выразить в следующем коде.
    int lines_count = 0;
int fd = open("input.txt", O_RDONLY | O_BINARY);
bool cr = false;
static char buffer[32*1024];
while (true) {
int n = read(fd, buffer, sizeof(buffer));
if (n <= 0) break;

if (cr && buffer[0] == '\n')
lines_count++;

for (int i = 1; i < n; i++)
if (buffer[i] == '\n' && buffer[i-1] == '\r')
lines_count++;

cr = buffer[n-1] == '\r';
}
std::cout << lines_count << '\n'; // или printf("%i\n", lines_count);
При использовании же метода read_byte() данная задача решается так:
    int lines_count = 0;
IFile f("input.txt"); // FILE *f = fopen("input.txt", "rb");
char prevc = 0; // char prevc = 0, c;
while (!f.at_eof()) { // while ((c = fgetc(f)) != EOF) {
char c = (char)f.read_byte(); // (в этой строке будет пусто)
if (c == '\n' && prevc == '\r')
lines_count++;
prevc = c;
}
std::cout << lines_count << '\n';
Разумеется, можно решить эту задачу аналогично с использованием стандартных файловых потоков Си (код я привёл в блоке комментариев справа). Но суть в том, что такой код значительно проще (тело цикла получилось в два раза короче) при сопоставимой производительности за счёт буферизации чтения.

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

считаю, что в теле метода read_byte() в виде buffer[buffer_pos++] он [постфиксный инкремент] неуместен

Но как тогда, по-вашему, следовало бы реализовать этот метод? Или даже, давайте я напишу код в вашем стиле:
int buffered_stream_get_byte(buffered_stream_t *stream)
{
if (buffered_stream_is_empty(stream))
return STREAM_EMPTY;
return stream->buffer[stream->buffer_pos++];
}
Здесь вы также считаете неуместным использование постфиксного инкремента? Тогда какой код был бы лучше в данном случае?

     2024/07/08 16:15, veector          # 

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

Здесь вы также считаете неуместным использование постфиксного инкремента?

Да.

Тогда какой код был бы лучше в данном случае?

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

Я модифицировал вашу краткую функцию вот таким образом:
int buffered_stream_get_byte(buffered_stream_t *stream, int default_value)
{
int result = default_value;

if (!buffered_stream_is_empty(stream))
{
result = stream->buffer[stream->buffer_pos];
stream->buffer_pos++;
};

return result;
}
Оптимальный код писать конечно же надо, это однозначно. Но оптимизация это все-таки не про сокращение числа строчек в программе методом return stream->buffer[stream->buffer_pos++], а про ООП, информационные модели, интересные приемы и архитектурные решения. Код программы — это же не константа, он постепенно изменяется. И уж лучше пусть программист подумает над тем, что неплохо бы было, чёрт возьми, не слепо следовать исходным условиям задачи, а предусмотреть параметр типа default_value для того, чтобы буфер подходил в будущем для ещё большего числа задач. Но и не переборщить и не гнаться за "универсальным на свете" решением.

Обращение за пределы памяти переменной это достаточно частая ошибка, а конструкции вида stream->buffer[stream->buffer_pos++] провоцируют программиста их совершать. Сейчас нам надо вернуть один байт, а условно "завтра" попросят вернуть int (явно попросят, ну согласитесь, не с int, так с чем-то еще) и у вас уже определенно не будет такой конструкции return stream->buffer[stream->buffer_pos++]; в коде программы. Так может и незачем за такие конструкции цепляться?

     2024/07/08 16:23, Gudleifr          # 

Но оптимизация это все-таки не про сокращение числа строчек в программе... а про ООП, информационные модели, интересные приемы и архитектурные решения

Вся эта мутота про "ООП и прочее" это даже не про оптимизацию, а про обфускацию.

ошибка, а конструкции ... провоцируют программиста их совершать

Не ошибки, а возможности. Программист должен управлять программой, а не программа — программистом.

     2024/07/08 16:33, veector          # 

Я модифицировал вашу краткую функцию вот таким образом

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

     2024/07/08 16:36, veector          # 

Gudleifr, если не секрет, сколько вам примерно лет (мне чуть больше сорока)? Вы давно работаете разработчиком софта на нативных языках?

     2024/07/08 16:50, Gudleifr          # 

если не секрет

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

     2024/07/08 16:56, veector          # 

И после этого вы ещё смеете пытаться ограничивать программистов в их возможностях? Нигде не ёкает?

Добавить свой отзыв

Написать автору можно на электронную почту
mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Содержание

Каким должен быть язык программирования?

Анализ и критика

●  Устарел ли текст как форма представления программы

●  Русский язык и программирование

●  Многоязыковое программирование

Синтаксис языков программирования

Синтаксический сахар

●  Некоторые «вкусности» Алгол-68

●  «Двухмерный» синтаксис Python

●  Почему языки с синтаксисом Си популярнее языков с синтаксисом Паскаля?

●  Должна ли программа быть удобочитаемой?

●  Стиль языка программирования

●  Тексто-графическое представление программы

●●  Разделители

●●  Строки программы

●●  Слева направо или справа налево?

●  Комментарии

●●  Длинные комментарии

●●  Короткие комментарии

●●  Комментарии автоматической генерации документации

●●  Нерабочий код

●●  Помеченные комментарии

●  Нужны ли беззнаковые целые?

●  Шестнадцатиричные и двоичные константы

●  Условные операторы

●  Переключатель

●  Циклы

●●  Продолжение цикла и выход из него

●  Некошерный «goto»

●  Изменение приоритетов операций

●  Операции присвоения и проверки на равенство. Возможно ли одинаковое обозначение?

●  Так ли нужны операции «&&», «||» и «^^»?

●  Постфиксные инкремент и декремент

●  Почему в PHP для конкатенации строк используется «.»?

●  Указатели и ссылки в C++

●●  О неправомерном доступе к памяти через указатели

●  Обработка ошибок

●  Функциональное программирование

●●  Нечистые действия в чистых функциях

●●  О чистоте и нечистоте функций и языков

●●  Макросы — это чистые функции, исполняемые во время компиляции

●●  Хаскелл, детище британских учёных

●●  Измеряем замедление при вызове функций высших порядков

●●  C vs Haskell: сравнение скорости на простом примере

●●  Уникальность имён функций: за и против

●●  Каррирование: для чего и как

●●  О тестах, доказывающих отсутствие ошибок

●  Надёжные программы из ненадёжных компонентов

●●  О многократном резервировании функций

●  Использование памяти

●  Почему динамическое распределение памяти — это плохо

●  Как обеспечить возврат функциями объектов переменной длины?

●●  Типы переменного размера (dynamically sized types, DST) в языке Rust

●●  Массивы переменной длины в C/C++

●●  Размещение объектов в стеке, традиционный подход

●●  Размещение объектов переменной длины с использованием множества стеков

●●  Размещение объектов переменной длины с использованием двух стеков

●●  Реализация двухстековой модели размещения данных

●●  Двухстековая модель: тесты на скорость

●●  Изменение длины объекта в стеке во время исполнения

●●  Размещение объектов переменной длины с использованием одного стека

●  Можно ли забыть о «куче», если объекты переменной длины хранить в стеке

●  Безопасность и размещение объектов переменной длины в стеке

●  Массивы, структуры, типы, классы переменной длины

●  О хранении данных в стеке, вместо заключения

●  Реализация параметрического полиморфизма

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Новости и прочее




Последние отзывы

2024/07/26 20:41 ••• Gudleifr
Программирование исчезнет. Будет дрессировка нейронных сетей

2024/07/08 16:56 ••• veector
Постфиксные инкремент и декремент

2024/07/01 21:58 ••• Прохожий
ЕС ЭВМ — это измена, трусость и обман?

2024/06/21 00:20 ••• Gudleifr
О превращении кибернетики в шаманство

2024/06/12 11:27 ••• Автор сайта
Все языки эквивалентны. Но некоторые из них эквивалентнее других

2024/05/31 12:31 ••• Прохожий
Идеальный транслятор

2024/05/28 15:16 ••• Прохожий
Русской операционной системой должна стать ReactOS

2024/05/25 17:18 ••• Прохожий
Избранные компьютерные анекдоты

2024/05/11 16:33 ••• Автор сайта
Энтузиасты-разработчики компиляторов и их проекты

2024/04/28 15:58 ••• Автор сайта
Обработка ошибок

2024/04/23 00:00 ••• alextretyak
Признаки устаревшего языка

2024/04/20 21:28 ••• Бурановский дедушка
Русский язык и программирование

2024/04/01 23:39 ••• Бурановский дедушка
Новости и прочее