В различных дисциплинах разработки программного обеспечения есть много философий о том, как библиотеки должны справляться с ошибками или другими исключительными условиями. Несколько из тех, что я видел:
- Вернуть код ошибки с результатом, возвращаемым аргументом указателя. Это то, что делает PETSc.
- Вернуть ошибки по часовой стрелке. Например, malloc возвращает NULL, если не удалось выделить память,
sqrt
возвращает NaN, если вы передаете отрицательное число и т. Д. Этот подход используется во многих функциях libc. - Брось исключения. Используется в сделке. II, Трилино и др.
- Вернуть тип варианта; например, функция C ++, которая возвращает объект типа,
Result
если он работает правильно, и использует типError
для описания возвратаstd::variant<Error, Result>
. - Используйте assert и crash. Используется в p4est и некоторых частях igraph.
Проблемы с каждым подходом:
- Проверка на каждую ошибку вводит много дополнительного кода. Значения, в которые будет сохраняться результат, всегда должны быть объявлены первыми, вводя множество временных переменных, которые могут использоваться только один раз. Этот подход объясняет, какая ошибка произошла, но может быть трудно определить, почему или для глубокого стека вызовов, где.
- Случай ошибки легко игнорировать. Вдобавок ко всему, многие функции не могут даже иметь значимое значение для часового типа, если весь диапазон типов вывода является правдоподобным результатом. Многие из тех же проблем, что и # 1.
- Возможно только в C ++, Python и т. Д., Но не в C или Fortran. Может быть имитирован в C с помощью волшебства setjmp / longjmp или libunwind .
- Возможно только в C ++, Rust, OCaml и т. Д., Но не в C или Fortran. Может быть имитирован в C с помощью макро-магии.
- Возможно, самый информативный. Но если вы примете этот подход, скажем, для библиотеки C, для которой вы затем напишите обертку Python, глупая ошибка, такая как передача индекса за пределы массива, приведет к сбою интерпретатора Python.
Большая часть рекомендаций в Интернете об обработке ошибок написана с точки зрения операционных систем, встроенных разработок или веб-приложений. Сбои недопустимы, и вам нужно беспокоиться о безопасности. Научные приложения не имеют этих проблем почти в той же степени, если вообще.
Другое соображение - какие ошибки можно исправить или нет. Ошибка malloc не подлежит восстановлению, и в любом случае убийца нехватки памяти ОС доберется до нее раньше, чем вы. Индекс за пределами размера массива также не подлежит восстановлению. Для меня как для пользователя самое приятное, что может сделать библиотека, - это аварийно завершить работу с информативным сообщением об ошибке. С другой стороны, неспособность, скажем, итеративного линейного решателя сходиться может быть восстановлена с помощью решателя прямой факторизации.
Как научные библиотеки должны сообщать об ошибках и ожидать, что они будут обработаны? Я, конечно, понимаю, что это зависит от того, на каком языке реализована библиотека. Но, насколько я могу судить, для любой достаточно полезной библиотеки люди захотят вызывать ее не с того языка, на котором она реализована.
Кроме того, я думаю, что подход № 5 может быть существенно улучшен для библиотеки C, если он определяет глобальный указатель на функцию-обработчик утверждений как часть общедоступного API. Обработчик утверждений по умолчанию будет сообщать номер файла / строки и сбои. Привязки C ++ для этой библиотеки будут определять новый обработчик утверждений, который вместо этого генерирует исключение C ++. Аналогично, привязки Python будут определять обработчик утверждений, который использует API CPython для генерирования исключения Python. Но я не знаю ни одного примера такого подхода.
Ответы:
Я дам вам свою точку зрения, которая закодирована в проекте deal.II, на который вы ссылаетесь.
Во-первых, существует два вида состояний ошибок: ошибки, из которых можно восстановить, и ошибки, из которых невозможно восстановить.
Первый - это, например, если входной файл не может быть прочитан - например, если вы читаете информацию из файла,
$HOME/.dealii
который может существовать или не существовать. Функция чтения должна просто вернуться к вызывающей функции, чтобы последняя выяснила, что делать. Может также случиться так, что ресурс в данный момент недоступен, но может снова появиться через минуту (удаленно смонтированная файловая система).Последнее, например, если вы пытаетесь добавить вектор размера 10 к вектору размера 20: попробуйте, как вы можете, с этим ничего не поделаешь - в коде есть ошибка, которая привела к точка, где мы попытались сделать сложение.
Эти два условия должны рассматриваться по-разному, независимо от используемого вами языка программирования:
Во втором случае, поскольку нет выхода из программы, завершите программу. Вы могли бы сделать это, выдав исключение или вернув код ошибки, который указывает вызывающей стороне, что ничего нельзя сделать, но вы могли бы также немедленно прервать программу, поскольку это значительно облегчает программисту отладку проблемы.
В первом случае возникла исключительная ситуация, с которой можно справиться. Даже несмотря на то, что C и Fortran не имели возможности выразить это, все разумные языки, появившиеся позже, включили способы в языковой стандарт для решения таких «исключительных» возвратов путем предоставления, ну, в общем, «исключений». Используйте это - вот для чего они здесь; они также разработаны таким образом, что вы не можете забыть игнорировать их (если вы делаете, исключение распространяется только на один уровень выше).
Другими словами, то, что я защищаю здесь (и что делает. II), представляет собой смесь ваших стратегий 3 и 5, в зависимости от контекста. Это правда, что 3 не работает в таких языках, как C или Fortran - в этом случае можно утверждать, что это хорошая причина просто не использовать языки, которые затрудняют выражение того, что вы хотите сделать.
Assert
исключение вместо вызоваabort()
.)источник
std::exception
, и они могут быть перехвачены по ссылке, не зная производного типа.