Pull to refresh

Comments 50

Комментарий ноунейма.
С одной стороны, мне дико нравится пропозал, он гораздо лучше чем std::expected который имхо имеет нерешенные проблемы в том виде, в котором его предложил саттер (а именно, выброс значения, хранимого в error, при доступе к значению — ведь базовая идея чтобы возвращать вместе с типом что-то не очень тяжелое, скажем, код ошибки; и получается, что мы кидаем энум или там int или строку… в общем, что-то, что не наследует std::exception; с одной стороны, ловить int язык позволяет, с другой — говнокод).
С другой, вспоминается картинка про 14 конкурирующих стандартов.
Ведь проблема не столько в легаси коде; какой-нибудь Гугл или фейсбук с монорепозиторием могут потратить месяц на поправку сигнатуры функции и исправление ошибок компиляции (хотя часто есть задачи понасущнее)…
А вот захочу я модернизировать какой-нибудь int QString::toInt(bool *ok=nullptr) const;


Во что-то типа
std::optional QString::toInt() const;
Понятное дело, у меня не получится-ок, я сделаю свободную функцию (давно пора):
std::optional Qt::toInt(QStringView);
А старую объявлю deprecated. А ещё через 5 лет мне придётся менять сигнатуру на
int Qt::toInt2(QStringView) throws(ParseError);
А пользователям придётся каждый раз исправлять код… так и приходится жить с богомерзким возвратом эррор кода через ссылку/указатель, ведь надо чуть-чуть подождать и можно будет использовать новый true-way

Вот как поменять поведение std::vector::at без введения std2? Я не очень представляю, если честно.

Очень хорошие новости по поводу Си.

Сейчас обработка ошибок сводится к сильному загромождению кода. Немного спасают лишь макросы. Из-за всего этого страдает читабельность. Блок finally будет хоть каким-то выходом в ряде ситуаций из множества goto на блок освобождения ресурсов или вызовов pthread_cleanup_push()/pthread_cleanup_pop(). Хотя в остальных ситуациях лучше подошёл бы defer из Golang. Вот его очень не хватает для повышения читабельности.
Вот уменьшение boilerplate для Си как раз не собираются завозить. В комитете сказали, что если в Си когда-нибудь будут исключения, то явные. В статье приведён пример, как это будет выглядеть. Никакого автоматического проброса, явная проверка `result.failed`. А `finally` можно было бы рассматривать для C++, но там преобладает идеология RAII, поэтому этого тоже не будет.

Edit: Наврал я! Предлагается макрос try, предназначенный для проверки failed и проброса исключения.
Ну, по крайней мере лёд тронулся с места. С одной стороны в системном языке явные проверки выгодны, а с другой — во многих компаниях они просто будут отсутствовать в силу давления менеджеров по срокам.

А вообще, в Си очень не хватает стандартизованного thread-local стека кодов ошибок в связке с указателями на функции помимо errno в качестве достаточного минимума. Отладка упростилась бы.

А в реализации defer'ов вообще не вижу преград. Компилятор может просто добавлять перед любым return список всех встреченных до данной строки defer'ов в рамках функции в обратном порядке. Либо формировать таблицу очистки с переходами через jmp. Удивительно, что этого до сих пор не ввели.
defer это не про ошибки. Смотрите предложения по второй версии Go
В Си по каждой ошибке требуется освобождать выделенные ресурсы в обратной порядке. Поэтому либо обработка ошибок делает goto в секцию освобождения ресурсов, либо освобождение дублируется по каждому return. Поэтому defer в данном случае очень упростил бы обработку ошибок. Достаточно будет делать обычный return.

Грубый пример:
static sem_t sem;
static int some_func(void) // or not void
{
    int r;
    do {
        r = sem_wait(&sem);
    } while ((r == -1) && (errno == EINTR));
    if (r == -1) {
        return -1;
    }

    int saved_errno;

    char *buf = malloc(buffer_size);
    if (!buf) {
        saved_errno = errno;
        goto aborting_sem;
    }
    int x;
// ....
    sem_post(&sem);
    return x; // x>=0

// ....
aborting_sem:
    sem_post(&sem);
    errno = saved_errno;
    return -1;
}


В данном случае в defer попал бы sem_post. А если будет ошибка после выделения буфера, то и free попадёт. Но для defer'а потребовалось бы указывать, должен ли он вызываться всегда, или же только по ошибке. А, соответственно, вместо return могло бы использоваться ключевое слово, означающее возврат ошибки. Например:
return error -1;
return_error -1;
[[error]] return -1;
Даже не знаю. У меня неоднозначное отношение к исключениям. С одной стороны, это удобный механизм обработки ошибок. С другой — мне дико не нравится, что любая функция может выбросить исключение, это такой современный goto. Особенно по всякой фигне типа невозможности преобразовать строку в число потому что в строке попалась буква.

С одной стороны хочется чтобы все явно. С другой — писать для каждой функции какие исключения она может выбросить, будет сильно утомительно. Спецификатор try перед вызовом — с одной стороны хорошо что явно, с другой плохо что писанина. И еще груз совместимости со старыми исключениями… Просто не знаю:)

А если бы проектировать язык с нуля, с учетом ошибок дизайна предыдущих языков программирования, то какой бы вы видели идеальную систему исключений?
Ответ предназначен NeoCode, промахнулся кнопкой.

Мне нравится, как Intellij Idea подсказывает, на какой строке вызывается корутина в Kotlin:

image

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

Идеальная система исключений — сложно сказать, я не эксперт в этом ;) Хотя в разделе «Когда что использовать» написаны гайдлайны, эквивалентные тем, что сейчас используются в Swift. То есть для преобразования строки в число рекомендуется не пользоваться исключениями, потому что «в строке нет числа» — не ошибка. А вообще, в современных языках программирования наблюдается переход от динамических исключений к статическим. И `throws` в контексте C++ всё же нужен как подсказка программисту.

Что меня расстраивает — в текущем Proposal не предусмотрены cast-ы статических исключений. Закастить можно, но только один раз, к `std::error`, при этом от исключения остаётся один message, остальное теряется. В идеале, конечно, статические исключения должны быть такими же мощными, как динамические, но с бонусом, что можно очень быстро бросить код ошибки.
Все существующие функции стандартной библиотеки станут noexcept и будут крашить программу при std::bad_alloc. В то же время, будут добавлены новые API вроде vector::try_push_back, которые допускают ошибки выделения памяти.

Плохо. Если я решу написать программу, в которой мне хочется/надо обрабатывать нехватку памяти, то и библиотеки мне необходимо искать такие, которые используют новые try_* методы.
Всё же механизм должен быть единым.
Это, увы, лучшее, что можно сделать сегодня.

Цепочка событий такая:
1. Всё началось ещё в прошлом веке, когда умные люди выяснили, что программы часто «заказывают» куда больше памяти, чем им реально нужно. Решение «проблемы» — всем известно.
2. Так как этот костыль прописался во всех современных OS, то простого использования стандартной библиотеки и аккуратной обработки std::bad_allocнедостаточно! Чтобы программа работала корректно нужен свой аллокатор, а разработчики компиляторов его не добавляют, предположительно исходя из идеи: «он будет серьёзно замедлять код и, соответственно, пользователи будут его отключать».
3. Обнаружив, что написать программу, корректно отрабатывающую нехватку памяти, практически нереально (по меньшей мере если не использовать специальные, нестандартные, библиотеки для аллокации памяти) 99.9% разработчиков забивают на обработку этой ситуации большой и толстый болт.
4. В результате если вам хочется/нужно как-то обрабатывать-таки нехватку памяти — то вам нужно как-то по косвенным признакам отловить/найти среди тысяч библиотек, не обрабатывающих эту ситуацию корректно те редкие жемчужины, которые, всё-таки, делают это правильно. А сделать это сложно, так как какие-то ошмётки обработки нехватки памяти растыканы везде — только вот они нифига не текстируются и не работают.
5. Комитет по стандартизации пытется облегчить вам эту работу и вводит новые try_* методы. Если разработчики компиляторов в этот раз таки сделают так, что этими функциями реально можно будет гарантировать бесперебойную работу… может быть чего и выйдет.
P132 предлагает добавить nothrow версии аллокаторов и nothrow версии std:: функций, использующих аллокаторы. То есть, если всё это когда-нибудь примут, то всё равно нужно будет подсовывать везде свой «кошерный» аллокатор, плюс использовать новые версии методов.
То есть в стандарте нормального аллокатора, аллоцирующего память с MAP_POPULATE (и аналогов в других OS) по-прежнему не будет? Тогда это — очередной мёртворождённый высер…
Речь в существующих Proposal-ах о том, чтобы разделить API аллокаторов на noexcept и не noexcept, а также разделить функции, работающие с аллокаторами, на noexcept и не noexcept. Обрабатывать overcommit в стандарте никто не предлагает. Если вы хотите делать дополнительные проверки, то их надо делать в вашем аллокаторе, плюс использовать новые методы, иначе исключение, выброшенное вашим аллокатором, там же и останется (краш).

Спрашивается, зачем вообще что-то менять, если никому не станет лучше? Дело в том, что как часть плана ухода от динамических исключений, в будущем предлагается сделать максимальную часть стандартной библиотеки noexcept. (Я неточно написал: noexcept предлагается сделать все существующие функции, *которые бросают только из-за памяти*, конечно же.) Чем больше функций noexcept, тем меньше оверхед на поддержку исключений.

P.S. Пишу «уход от динамических исключений», но понятно, что пока это дело необозримого будущего. На вскидку, готово всё будет к C++26, потом ещё будет процесс внедрения новшеств в std, всякие deprecate-obsolete…
Во-первых, overcommit можно и отключить. Во-вторых, есть всякие лимиты типа cgroups. В-третьих, может быть свой менеджер памяти прямо на уровне new/delete/malloc/free.
Так что лично я считаю, что здесь есть две ортогональные проблемы: написание корректно обрабатывающей недостаток памяти программы, и настройка окружения. Методы с различным подходом к обработке ошибок выделения памяти значительно усложняют решение первой проблемы.
А решение второй проблемы — это отдельный вопрос, не касающийся C++.
Так что лично я считаю, что здесь есть две ортогональные проблемы
Это было бы так в идеальном мире с розовыми понями и единорогами, какающими радугой. А реальном мире — никто не будет заморачиваться с написанием и поддержкой кода, который можно использовать только в экзотическом, нестандартном окружении — у большинства разработчиков хватает реальных ошибок, которые проявляются на реальных системах.

Во-вторых, есть всякие лимиты типа cgroups.
Это ничего не меняет: вы по-прежнему можете «получить» память от mmap'а, а потом упасть при попытке записать туда один байт.

В-третьих, может быть свой менеджер памяти прямо на уровне new/delete/malloc/free.
Чтобы этим кто-то озаботился — оно должно быть «из коробки» по умолчанию. А до версии 2.6.23 даже «ручек» не было, чтобы такой менеджер написать… Если реальные реализации C++20 не будут такого менеджера предоставлять «из коробки» — ничего не изменится.
А реальном мире — никто не будет заморачиваться с написанием и поддержкой кода, который можно использовать только в экзотическом, нестандартном окружении — у большинства разработчиков хватает реальных ошибок, которые проявляются на реальных системах.

Ну, раз они не обрабатывают ошибки выделения памяти, то это им и не нужно. Ибо в реальности это действительно мало кому нужно. Я просто ратую за единый механизм, а не разделение уже на уровне исходного кода.
Это ничего не меняет: вы по-прежнему можете «получить» память от mmap'а, а потом упасть при попытке записать туда один байт.

Можем. Но это помогает администрировать систему и получить нужное окружение.
Чтобы этим кто-то озаботился — оно должно быть «из коробки» по умолчанию. А до версии 2.6.23 даже «ручек» не было, чтобы такой менеджер написать… Если реальные реализации C++20 не будут такого менеджера предоставлять «из коробки» — ничего не изменится.

Если вы про MAP_POPULATE, то это не выход, ибо тормозит выделение памяти. Настраивайте ОС так, как вам необходимо, и будет счастье.
Я просто ратую за единый механизм, а не разделение уже на уровне исходного кода.
Проблема в том, что «единый» механизм добавляет в программы кучу неработающего кода. Зачем?

Можем. Но это помогает администрировать систему и получить нужное окружение.
Ну и причём тут обработка нехватки памяти, тогда?

Настраивайте ОС так, как вам необходимо, и будет счастье.
Какое вдруг счастье? Подавляющее большинство программ как не обрабатывало нехватку памяти, так и не будет. Только ещё и убивать их будут раньше. Если мы что-то и хотим получить — так это отдельные процессы (какие-нибудь «супердемоны»), которые не умирают в случае нехватки памяти.
Проблема в том, что «единый» механизм добавляет в программы кучу неработающего кода.

Код работает, если есть соответствующее окружение. Overcommit — это вопрос ОС, а не языка.
Ну и причём тут обработка нехватки памяти, тогда?

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

Если программисты написали говнокод, при чём тут язык? Пишите нормальный код, соответствующий требованиям Стандарта, выключите overcommit и наслаждайтесь.
Код работает, если есть соответствующее окружение.
Код не работает просто потому что он есть. Его нужно тестировать и отлаживать, чтобы он работал.

Overcommit — это вопрос ОС, а не языка.
Только если код тестируется и используется на ОС как с Overcommit, так и без. Огромный процент существующих библиотек на системах без overcommit не используется и не тестируется.

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

Если программисты написали говнокод, при чём тут язык?
Притом что любой язык нужен лишь потому что его могут использовать реальные программисты для решения реальных задач. Если это невозможно — язык не будет использоваться, вот и всё.
Огромный процент существующих библиотек на системах без overcommit не используется и не тестируется

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

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

Хорошо, буду ждать смерти C++. Ведь в других языках можно обработать ошибку нехватки памяти, и overcommit не мешает, правда?
UFO just landed and posted this here
Еще мысль. Я как-то думал о том, какими должны быть исключения… и пришла в голову такая мысль (возможно странная): было бы хорошо, если бы функции были написаны некоторым универсальным образом, таким, что один и тот же оператор мог бы и генерировать исключение, и возвращать код возврата — в зависимости от того, как устроен вызывающий код. Т.е. объединить throw и return в специальный общий оператор.
Например, если выше в стеке вызовов явно были try{} catch{} — то генерируем исключение; если контекст вызовов такой, что исключений этого типа быть не должно (или например какой-нибудь блок noexcept{} ) — возвращаем код возврата. Это не исключает возможности всегда явно генерировать исключения или всегда явно возвращать код возврата.
Как такое сделать? Например, какой-то глобальной таблицей/стеком активных блоков try, т.е. при входе в блок try в этот глобальный стек заносится какой-то объект, при выходе — извлекается. При необходимости бросить исключение — проверяется, а нет ли в этом стеке объекта соответствующего типа. При нормальной (успешной) работе функций это вроде бы никаких накладных расходов не создает.
Зато это дает возможность использовать одни и те же функции и в коде без исключений, и в коде с исключениями, и никаких сюрпризов типа необработанных исключений не будет.
Добавлю, что такое наверное можно даже специальной библиотекой реализовать… Только нужно в точке «try {» знать какие будут «catch», можно сделать макрос и явно прописывать, но компилятор с этим справился бы лучше.
Т.е. разматывать стек при попытке бросить исключение, и, если нигде не ловится, возвращать код ошибки?
Ну не разматывать, а просматривать.

Не совсем понятно, как будет ваш сценарий работать для случая "кода возврата". Или давать доступ к обоим состояниям, с возможностью отстрелить себе ногу по самые гланды, или по сути классический try-catch.

Например, если выше в стеке вызовов явно были try{} catch{} — то генерируем исключение; если контекст вызовов такой, что исключений этого типа быть не должно (или например какой-нибудь блок noexcept{} ) — возвращаем код возврата.

Чем-то напоминает контекст использования в перле. Скажем, в скалярном контексте массив дает число элементов в массиве, в нескалярном — собственно массив. И сиди, думай, что в каждом конкретном случае программист сказать хотел. :)
Только вот в перле контекст обычно понятен из непосредственного окружения, а у вас получается, что по коду фиг поймешь, будет исключение или нет. И вообще… Это ж сколько глюков будет: программист забыл try/catch написать, а ему вместо исключения — код ошибки, который он, естественно, не проверяет.
Да, наверное вы правы, так еще больше неявности…
Тогда еще вариант: если функция может генерировать исключение, то компилятор должен убедиться, что оно где-то обрабатывается, иначе — ошибка компиляции. Можно ввести в прототип функции информацию об исключениях, которые она может сгенерировать.
Конечно, программист может влепить глобальный catch куда нибудь в main, и тогда никаких ошибок компиляции не будет… но я например ни в коем случае не буду так делать, а напротив — постараюсь перехватить исключения как можно раньше.
если функция может генерировать исключение, то компилятор должен убедиться, что оно где-то обрабатывается

Осталось отменить принцип раздельной компиляции. :)
Скорее ввести новый формат объектных файлов, ориентированных на модули и содержащих достаточно метаинформации. Заодно и реализовать компиляцию шаблонов в промежуточное двоичное представление.
Как такое сделать? Например, какой-то глобальной таблицей/стеком активных блоков try, т.е. при входе в блок try в этот глобальный стек заносится какой-то объект, при выходе — извлекается. При необходимости бросить исключение — проверяется, а нет ли в этом стеке объекта соответствующего типа. При нормальной (успешной) работе функций это вроде бы никаких накладных расходов не создает.


Вообще-то, именно так и реализованы блоки try-catch в с++ =)
Например, если обработчика исключения нет, то throw смотрит в таблицу текущих обработчиков, видит, что там пусто и вызывает terminate не разматывая стек до main().

Herb Sutter — Герб Саттер.
Niall Douglas — Найл Дуглас.
Если бы кто-то из них был китайцем, имя было бы записано иероглифами? Что за идиотская манера не переводить имена?


boilerplate — избыточность.

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

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

Есть более многословный вариант, который можно считать хорошим тоном: писать имена на русском и при первом упоминании в скобках оригинал. Для не латинских имён — еще и латинское. Только вот мало кто с этим заморачивается.

В первый раз — транслитерация на русский и оригинал в скобках для гугления:


[...] первоначальный документ от Герба Саттера (Herb Sutter)

Во второй раз — только по-русски, кому интересно уже погуглили:


[...] где Герб Сеттер предлагает [...]

Для людей с китайскими именами можно приводить и иероглифы, и пиньинь.

UFO just landed and posted this here
Мне больше интересно, когда же в C++ завезут блок finally?
Писать код в стиле RAII не всегда удобно.
Никто в C++20 не добавлял expected, не вводите людей в заблуждение. Это все еще proposal.
Спасибо, поправил. Мне казалось, что уже решили добавить, ан нет — в последнее время про P0323 никаких вестей.
Кажется странным решение, что функция может бросать или только статические исключения, или только динамические. Сравним с Java, где есть checked и unchecked исключения. Обычные динамические исключения хорошо выполняют роль unchecked исключений, когда непонятно, как исправить проблему, но где-то на верхнем уровне имеет смысл всё-таки поймать исключение и обработать. Или сравните с паниками Rust.

Но вот согласно текущему предложению, динамическое исключение, проходящее через throws-функцию, завернётся в std::error, и чтобы достать его обратно, нужно или дождаться, пока оно окажется в не-throws функции, или танцевать с бубном. Плюс, при обработке std::error надо будет помнить, что там может оказаться завёрнутое динамическое исключение, которое нельзя игнорировать с той же лёгкостью, что и коды ошибок.

По-моему, было бы логичнее сделать отдельные варианты throws(E) и throws(E) noexcept. Первый будет пропускать и статические, и динамические исключения. Второй будет пропускать только статические. Причём заворачивать динамическое исключение в std::error стоило бы только явным образом. Не уверен, что это лучшее решение, но что-то мне подсказывает, что для динамических исключений нужна «выделенная полоса».
Главная мысль запрета исключений в больших компаниях (например, Google) не в том, что они медленные на стадии проброса, а потому что при чтении кода вообще непонятно, бросает ли функция исключение или нет. Обработка ошибок должна быть явной, статические исключения не решают эту проблему, а потому Google так и продолжит банить исключения.
Вообще-то там совсем другое написано: Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch.

Я вот пока не понимаю как детерменированные исключения будут стыковаться с кодом, который на них не рассчитан…

Обработка ошибок станет если не явной, то по крайней мере, предсказуемой. В статических исключениях будет аннотация throws для функций, которая будет означать "да, я кину исключение, обработай это, пожалуйста". Далее IDE сможет подсвечивать вызовы таких функций, см. пример выше. Сейчас так делать невозможно, потому что расставлять noexcept разработчики "ленятся". Кто-то считает, что нельзя отдавать такие вещи на откуп IDE, и нужно ещё и ключевое слово try при всех вызовах бросающих функций. Мне кажется, что это уже лишнее.

Они просто на паскале не писали, вот и ленятся)

функция возвращает std::expected, однако вместо отдельного дискриминатора типа bool, который вместе с выравниванием будет занимать до 8 байт на стеке, этот бит информации передаётся каким-то более быстрым способом, например, в Carry Flag

Не подскажите, как вызываемая функция может установить Carry Flag, а вызывающая его проанализировать, если компилятор генерирует код не непосредственно, а через LLVM? Что должен выдать компилятор на вход LLVM, чтобы LLVM работал с этим флагом?

Очевидно, сначала надо добавить поддержку этого режима в LLVM

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

Sign up to leave a comment.

Articles