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. Вот его очень не хватает для повышения читабельности.
Edit: Наврал я! Предлагается макрос try, предназначенный для проверки failed и проброса исключения.
А вообще, в Си очень не хватает стандартизованного thread-local стека кодов ошибок в связке с указателями на функции помимо errno в качестве достаточного минимума. Отладка упростилась бы.
А в реализации defer'ов вообще не вижу преград. Компилятор может просто добавлять перед любым return список всех встреченных до данной строки defer'ов в рамках функции в обратном порядке. Либо формировать таблицу очистки с переходами через jmp. Удивительно, что этого до сих пор не ввели.
Грубый пример:
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;
А еще это будет совместимо с Rust
С одной стороны хочется чтобы все явно. С другой — писать для каждой функции какие исключения она может выбросить, будет сильно утомительно. Спецификатор try перед вызовом — с одной стороны хорошо что явно, с другой плохо что писанина. И еще груз совместимости со старыми исключениями… Просто не знаю:)
А если бы проектировать язык с нуля, с учетом ошибок дизайна предыдущих языков программирования, то какой бы вы видели идеальную систему исключений?
Мне нравится, как Intellij Idea подсказывает, на какой строке вызывается корутина в Kotlin:
Ничего лишнего не нужно, но всё понятно. Со старыми исключениями такого сделать нельзя, потому что большинство функций может их выбросить. С новыми статическими исключениями 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_*
методы. Если разработчики компиляторов в этот раз таки сделают так, что этими функциями реально можно будет гарантировать бесперебойную работу… может быть чего и выйдет.Спрашивается, зачем вообще что-то менять, если никому не станет лучше? Дело в том, что как часть плана ухода от динамических исключений, в будущем предлагается сделать максимальную часть стандартной библиотеки noexcept. (Я неточно написал: noexcept предлагается сделать все существующие функции, *которые бросают только из-за памяти*, конечно же.) Чем больше функций noexcept, тем меньше оверхед на поддержку исключений.
P.S. Пишу «уход от динамических исключений», но понятно, что пока это дело необозримого будущего. На вскидку, готово всё будет к C++26, потом ещё будет процесс внедрения новшеств в std, всякие deprecate-obsolete…
Так что лично я считаю, что здесь есть две ортогональные проблемы: написание корректно обрабатывающей недостаток памяти программы, и настройка окружения. Методы с различным подходом к обработке ошибок выделения памяти значительно усложняют решение первой проблемы.
А решение второй проблемы — это отдельный вопрос, не касающийся 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 не мешает, правда?
Например, если выше в стеке вызовов явно были 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)
Во второй раз — только по-русски, кому интересно уже погуглили:
[...] где Герб Сеттер предлагает [...]
Для людей с китайскими именами можно приводить и иероглифы, и пиньинь.
Писать код в стиле RAII не всегда удобно.
Но вот согласно текущему предложению, динамическое исключение, проходящее через throws-функцию, завернётся в std::error, и чтобы достать его обратно, нужно или дождаться, пока оно окажется в не-throws функции, или танцевать с бубном. Плюс, при обработке std::error надо будет помнить, что там может оказаться завёрнутое динамическое исключение, которое нельзя игнорировать с той же лёгкостью, что и коды ошибок.
По-моему, было бы логичнее сделать отдельные варианты throws(E) и throws(E) noexcept. Первый будет пропускать и статические, и динамические исключения. Второй будет пропускать только статические. Причём заворачивать динамическое исключение в std::error стоило бы только явным образом. Не уверен, что это лучшее решение, но что-то мне подсказывает, что для динамических исключений нужна «выделенная полоса».
Я вот пока не понимаю как детерменированные исключения будут стыковаться с кодом, который на них не рассчитан…
Обработка ошибок станет если не явной, то по крайней мере, предсказуемой. В статических исключениях будет аннотация throws
для функций, которая будет означать "да, я кину исключение, обработай это, пожалуйста". Далее IDE сможет подсвечивать вызовы таких функций, см. пример выше. Сейчас так делать невозможно, потому что расставлять noexcept
разработчики "ленятся". Кто-то считает, что нельзя отдавать такие вещи на откуп IDE, и нужно ещё и ключевое слово try
при всех вызовах бросающих функций. Мне кажется, что это уже лишнее.
функция возвращает std::expected, однако вместо отдельного дискриминатора типа bool, который вместе с выравниванием будет занимать до 8 байт на стеке, этот бит информации передаётся каким-то более быстрым способом, например, в Carry Flag
Не подскажите, как вызываемая функция может установить Carry Flag, а вызывающая его проанализировать, если компилятор генерирует код не непосредственно, а через LLVM? Что должен выдать компилятор на вход LLVM, чтобы LLVM работал с этим флагом?
Есть большие сомнения, что такая низкоуровневая сущность, как флаг, станет доступной в достаточно высокоуровневой LLVM. То же касается стека, который хотя и есть во всех современных архитектурах, но в LLVM напрямую им не управляют, что накладывает некоторые ограничения.
Детерминированные исключения и обработка ошибок в «C++ будущего»