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


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

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++];
}
Здесь вы также считаете неуместным использование постфиксного инкремента? Тогда какой код был бы лучше в данном случае?