Постфиксные инкремент и декремент (ссылка на статью)


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

2024-03-27 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 в том случае, когда результат операции не используется. Всё-таки постфиксный инкремент выглядит красивее префиксного. Неспроста же Бьёрн назвал язык C++, а не ++C. :)(:

2024-03-27 MihalNik

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

2024-04-01 veector

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

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

2024-04-02 Автор сайта

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

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

2024-04-04 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 veector

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

2024-04-07 alextretyak

всегда, всем, все разъясняю.
Ну, в таком случае, хотелось бы услышать [хотя бы краткое] разъяснение, что именно вас не устраивает в процитированном коде [в сообщении от 2024-04-01] и какой код был бы лучше в данном случае?

2024-04-08 veector

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

2024-04-08 Автор сайта

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

2024-04-10 alextretyak

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

Вообще, полный код метода read_byte(), который используется в реальном проекте, выглядит так:
    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 секунд очень мала, поэтому пока что получалось отправлять сообщения без ошибок точно в 0x:00.]

2024-04-10 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 Автор сайта

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

Уже более 4-х с половиной лет
Только недавно обратил внимание.

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

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

2024-04-21 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 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-29 alextretyak

veector
Код программы — это же не константа, он постепенно изменяется.
У меня несколько иное мнение на этот счёт. Бóльшая часть кода — да, скорее всего, рано или поздно будет либо дописана/изменена, либо вообще выкинута и заменена на другой код. Но есть код, который, как я считаю, должен быть именно константой, либо стремиться к этому. Для того, чтобы можно было опираясь на него двигаться дальше. Это как фундамент для здания. Если постоянно переделывать фундамент, то постройка дома так никогда и не завершится.

Взять к примеру недавний случай со сбоем CrowdStrike. С моей точки зрения, причина произошедшего инцидента не в том, что разработчики ошиблись или что они недостаточно хорошо протестировали обновление драйвера, а в самой необходимости этого обновления. Если в системах, обслуживающих банки и аэропорты, необходимо постоянно обновлять системные драйверы — то явно тут что-то не так. Драйверы — это не прикладное ПО. К ним вполне применим принцип «работает — не трогай». Для их обновления должна быть веская причина. [Единственное исключение — графические драйверы для игрового компьютера, но к компьютерам в банках и аэропортах это не относится.]

Поэтому я категорически против идеологии что "код (любой код!) должен постоянно изменяться", т.к. считаю, что это неизбежно будет приводить к катастрофам.

Обсуждаемая функция read_byte(), конечно, не настолько критична, как системный драйвер. Но она является частью ядра стандартной библиотеки языка программирования. Ядра, которое написано с расчётом на то, что на его реализацию [а не просто внешний интерфейс/набор функционала] можно полагаться при написании приложений. Каждое изменение в ядре стандартной библиотеки я стараюсь отслеживать на регрессию. Так, оптимизация для чтений файла большими блоками потребовала добавления в код функции чтения единственной условной конструкции, но из-за неё компилятор перестал встраивать эту функцию — это обнаружилось после того, как я заметил, что чтение маленькими блоками замедлилось на 12% после коммита оптимизации чтения большими блоками. Пришлось добавить проверку времени компиляции, которая "отключает" эту условную конструкцию при чтении маленькими блоками. Вот соответствующий коммит.

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

Сейчас нам надо вернуть один байт, а условно "завтра" попросят вернуть int
Нет. Не попросят. Для чтения int уже есть отдельная функция — read_bytes<sizeof(int)>(&i). Так что имеющийся код реализации read_byte() — это окончательный вариант функции чтения одного байта из файла.
И по задумке этот код должен давать понять, что это очень простая и быстрая функция. И что её можно спокойно вызывать даже для многомегабайтных файлов (другими словами — вызывать сотни миллионов раз без опасения, что это будет замедлять процесс обработки файла, и без желания переписать код на поблочное чтение файла). Разумеется, одного исходного кода не достаточно, и хороший программист должен опираться прежде всего на тесты производительности, но и тут функция read_byte() показывает себя очень неплохо — замедление всего в 1.3 раза при чтении [уже находящегося в кэше операционной системы!] файла с разбивкой по строкам, без какой-либо обработки прочитанных данных. Т.е. фактически, это максимально возможное замедление.

Но, возвращаясь к постфиксному инкременту.
Я сейчас пишу статью {}, посвящённую сравнению моделей итераторов в различных языках программирования. И во многих случаях использование постфиксного инкремента также существенно упрощает код. А именно: в реализациях метода next(), осуществляющего переход к следующему элементу итерируемого объекта, в языках программирования Python, Rust и Java. Конкретно, речь о строке return *b++; в реализации итераторов массива и о строке return cur++; в реализации итераторов диапазона.
Без использования постфиксного инкремента соответствующую строку кода пришлось бы заменить на три строки (с добавлением переменной
result
), что заметно ухудшит читаемость, т.к. все эти методы состоят всего из пары строк кода.

Но если говорить про типичные функции в типичных программах, то особой необходимости в постфиксных инкрементах/декрементах конечно нет.