Как сообщать об ошибках в научных библиотеках?

11

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

  1. Вернуть код ошибки с результатом, возвращаемым аргументом указателя. Это то, что делает PETSc.
  2. Вернуть ошибки по часовой стрелке. Например, malloc возвращает NULL, если не удалось выделить память, sqrtвозвращает NaN, если вы передаете отрицательное число и т. Д. Этот подход используется во многих функциях libc.
  3. Брось исключения. Используется в сделке. II, Трилино и др.
  4. Вернуть тип варианта; например, функция C ++, которая возвращает объект типа, Resultесли он работает правильно, и использует тип Errorдля описания возврата std::variant<Error, Result>.
  5. Используйте assert и crash. Используется в p4est и некоторых частях igraph.

Проблемы с каждым подходом:

  1. Проверка на каждую ошибку вводит много дополнительного кода. Значения, в которые будет сохраняться результат, всегда должны быть объявлены первыми, вводя множество временных переменных, которые могут использоваться только один раз. Этот подход объясняет, какая ошибка произошла, но может быть трудно определить, почему или для глубокого стека вызовов, где.
  2. Случай ошибки легко игнорировать. Вдобавок ко всему, многие функции не могут даже иметь значимое значение для часового типа, если весь диапазон типов вывода является правдоподобным результатом. Многие из тех же проблем, что и # 1.
  3. Возможно только в C ++, Python и т. Д., Но не в C или Fortran. Может быть имитирован в C с помощью волшебства setjmp / longjmp или libunwind .
  4. Возможно только в C ++, Rust, OCaml и т. Д., Но не в C или Fortran. Может быть имитирован в C с помощью макро-магии.
  5. Возможно, самый информативный. Но если вы примете этот подход, скажем, для библиотеки C, для которой вы затем напишите обертку Python, глупая ошибка, такая как передача индекса за пределы массива, приведет к сбою интерпретатора Python.

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

Другое соображение - какие ошибки можно исправить или нет. Ошибка malloc не подлежит восстановлению, и в любом случае убийца нехватки памяти ОС доберется до нее раньше, чем вы. Индекс за пределами размера массива также не подлежит восстановлению. Для меня как для пользователя самое приятное, что может сделать библиотека, - это аварийно завершить работу с информативным сообщением об ошибке. С другой стороны, неспособность, скажем, итеративного линейного решателя сходиться может быть восстановлена ​​с помощью решателя прямой факторизации.

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

Кроме того, я думаю, что подход № 5 может быть существенно улучшен для библиотеки C, если он определяет глобальный указатель на функцию-обработчик утверждений как часть общедоступного API. Обработчик утверждений по умолчанию будет сообщать номер файла / строки и сбои. Привязки C ++ для этой библиотеки будут определять новый обработчик утверждений, который вместо этого генерирует исключение C ++. Аналогично, привязки Python будут определять обработчик утверждений, который использует API CPython для генерирования исключения Python. Но я не знаю ни одного примера такого подхода.

Даниэль Шаперо
источник
Еще одним соображением является последствия производительности. Как эти различные методы влияют на скорость программного обеспечения? Должны ли мы использовать различную обработку ошибок в «управляющих» частях кода (например, при обработке входных файлов) по сравнению с вычислительно дорогими «движками»?
LedHead
Обратите внимание, что лучший ответ будет отличаться в зависимости от языка.
Хрилис-на забастовке-

Ответы:

10

Я дам вам свою точку зрения, которая закодирована в проекте deal.II, на который вы ссылаетесь.

Во-первых, существует два вида состояний ошибок: ошибки, из которых можно восстановить, и ошибки, из которых невозможно восстановить.

  • Первый - это, например, если входной файл не может быть прочитан - например, если вы читаете информацию из файла, $HOME/.dealiiкоторый может существовать или не существовать. Функция чтения должна просто вернуться к вызывающей функции, чтобы последняя выяснила, что делать. Может также случиться так, что ресурс в данный момент недоступен, но может снова появиться через минуту (удаленно смонтированная файловая система).

  • Последнее, например, если вы пытаетесь добавить вектор размера 10 к вектору размера 20: попробуйте, как вы можете, с этим ничего не поделаешь - в коде есть ошибка, которая привела к точка, где мы попытались сделать сложение.

Эти два условия должны рассматриваться по-разному, независимо от используемого вами языка программирования:

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

  • В первом случае возникла исключительная ситуация, с которой можно справиться. Даже несмотря на то, что C и Fortran не имели возможности выразить это, все разумные языки, появившиеся позже, включили способы в языковой стандарт для решения таких «исключительных» возвратов путем предоставления, ну, в общем, «исключений». Используйте это - вот для чего они здесь; они также разработаны таким образом, что вы не можете забыть игнорировать их (если вы делаете, исключение распространяется только на один уровень выше).

Другими словами, то, что я защищаю здесь (и что делает. II), представляет собой смесь ваших стратегий 3 и 5, в зависимости от контекста. Это правда, что 3 не работает в таких языках, как C или Fortran - в этом случае можно утверждать, что это хорошая причина просто не использовать языки, которые затрудняют выражение того, что вы хотите сделать.

x), но так как оценщик должен вызываться повторно, он должен не просто аварийно завершить, а просто вызвать исключение. В таких случаях, даже если передача отрицательного значения не подлежит восстановлению, следует генерировать исключение, а не прерывать программу. Я не согласился с этой позицией пару лет назад, но передумал после того, как в руководящих принципах программного обеспечения сообщества xSDK было указано, что программы никогда не должны аварийно завершаться (или, по крайней мере, должен быть способ переключения с аварийного отказа на исключительный) - так что соглашайтесь. У II теперь есть возможность сделать Assertисключение вместо вызова abort().)

Вольфганг Бангерт
источник
Я бы просто порекомендовал обратное: генерировать исключение, когда ситуация не может быть обработана, и возвращать код ошибки, когда это можно обработать. Проблема в том, что работать с выбрасываемыми исключениями довольно сложно: прикладной программист должен знать тип всех возможных исключений, чтобы перехватывать и обрабатывать их, иначе программа просто рухнет. Сбои в порядке и даже приветствуются в ситуациях, которые не могут быть обработаны, потому что точка аварийного отказа сообщается с помощью python, например, но для ситуаций, которые могут быть обработаны, это (в основном) не приветствуется.
cdalitz
@cdalitz: это недостаток дизайна C ++, который вы можете создавать объекты любого типа. Но любое разумное программное обеспечение (исключая Trilinos) выдает только те исключения, которые являются производными std::exception, и они могут быть перехвачены по ссылке, не зная производного типа.
Вольфганг Бангерт
1
Но я категорически не согласен с возвратом кода ошибки по причинам, изложенным в первоначальном вопросе: (i) коды ошибок игнорируются слишком часто и, как следствие, ошибки вообще не обрабатываются; (ii) во многих случаях просто нет исключительного значения, которое может быть разумно возвращено, учитывая, что тип возвращаемого значения функции является фиксированным; (iii) функции имеют разные типы возвращаемых значений, и вам придется в каждом случае отдельно определять, каким будет «исключительное» значение, представляющее ошибку.
Вольфганг Бангерт
WB написал (извините, трюк с @ не работает по какой-то причине, а имя пользователя по какой-то причине удаляется StackExchage): «Коды ошибок игнорируются слишком часто». Это еще больше относится к отлову исключений: не многие разработчики программного обеспечения берут на себя задачу заключать в скобки каждый вызов функции в блоке try / catch. Но это в основном дело вкуса: до тех пор, пока в документации четко указано, какие исключения выдает функция, я могу с этим справиться. Но опять-таки можно сказать: обязанность писать документацию слишком часто игнорируется ;-)
cdalitz
Но дело в том, что если вы забудете поймать исключение, проблем с последующим потоком не будет: программа просто прерывается. Будет легко найти, где возникла проблема. Если вы забудете проверить код ошибки, ваша программа может потерпеть крах в какой-то более поздний момент из-за неопределенного внутреннего состояния - но где исходная проблема оставалась совершенно неясной. Найти такие ошибки очень сложно.
Вольфганг Бангерт