W różnych dyscyplinach inżynierii oprogramowania istnieje wiele filozofii dotyczących tego, jak biblioteki powinny radzić sobie z błędami lub innymi wyjątkowymi warunkami. Kilka z tych, które widziałem:
- Zwraca kod błędu z wynikiem zwróconym przez argument wskaźnika. To właśnie robi PETSc.
- Zwraca błędy według wartości wartownika. Na przykład malloc zwraca NULL, jeśli nie może przydzielić pamięci,
sqrt
zwróci NaN, jeśli podasz liczbę ujemną itp. Takie podejście jest stosowane w wielu funkcjach libc. - Rzuć wyjątki. Używane w deal.II, Trilinos itp.
- Zwraca typ wariantu; na przykład funkcja C ++, która zwraca obiekt typu,
Result
jeśli działa poprawnie i używa typuError
do opisania, w jaki sposób nie powróciłbystd::variant<Error, Result>
. - Użyj assert i crash. Używany w p4est i niektórych częściach igraph.
Problemy z każdym podejściem:
- Sprawdzanie każdego błędu wprowadza wiele dodatkowego kodu. Wartości, w których wynik zostanie zapisany, muszą zawsze zostać zadeklarowane jako pierwsze, wprowadzając wiele zmiennych tymczasowych, których można użyć tylko raz. To podejście wyjaśnia, jaki błąd wystąpił, ale może być trudno ustalić, dlaczego lub, w przypadku głębokiego stosu wywołań, gdzie.
- Przypadek błędu można łatwo zignorować. Co więcej, wiele funkcji nie może mieć nawet znaczącej wartości wartownika, jeśli cały zakres typów danych wyjściowych jest wiarygodnym wynikiem. Wiele takich samych problemów jak # 1.
- Możliwe tylko w C ++, Python itp., Nie w C ani Fortran. Może być naśladowany w C za pomocą setjmp / longjmp sorcery lub libunwind .
- Możliwe tylko w C ++, Rust, OCaml itp., Nie w C ani Fortran. Można naśladować w C za pomocą makro-magii.
- Prawdopodobnie najbardziej informacyjny. Ale jeśli zastosujesz to podejście, powiedzmy, dla biblioteki C, dla której następnie napiszesz opakowanie Pythona, głupi błąd, taki jak przekazanie tablicy spoza zakresu do tablicy, spowoduje awarię interpretera Pythona.
Wiele porad w Internecie na temat obsługi błędów napisano z punktu widzenia systemów operacyjnych, programowania wbudowanego lub aplikacji internetowych. Awarie są niedopuszczalne i musisz martwić się o bezpieczeństwo. Aplikacje naukowe nie mają tych problemów w prawie takim samym stopniu, jeśli w ogóle.
Inną kwestią jest to, jakie rodzaje błędów można naprawić, czy nie. Awarii malloc nie można odzyskać, aw każdym razie zabójca braku pamięci w systemie operacyjnym dostanie się do niego, zanim to zrobisz. Indeks poza zakresem dla rozmiaru tablicy również nie jest możliwy do odzyskania. Dla mnie, jako użytkownika, najprzyjemniejszą rzeczą, jaką może zrobić biblioteka, jest awaria z informacyjnym komunikatem o błędzie. Z drugiej strony, niepowodzenie, powiedzmy, iteracyjnego liniowego solwera w zbieżności można odzyskać, stosując bezpośredni solwery faktoryzacji.
W jaki sposób biblioteki naukowe powinny zgłaszać błędy i oczekiwać ich usunięcia? Zdaję sobie oczywiście sprawę, że zależy to od języka, w którym biblioteka jest zaimplementowana. Ale o ile wiem, w przypadku każdej wystarczająco użytecznej biblioteki ludzie będą chcieli nazywać ją z innego języka niż ten, w którym jest zaimplementowana.
Nawiasem mówiąc, myślę, że podejście nr 5 można znacznie ulepszyć dla biblioteki C, jeśli zdefiniuje wskaźnik funkcji globalnego modułu obsługi asercji jako część publicznego API. Moduł obsługi asercji domyślnie raportuje numer pliku / linii i ulega awarii. Powiązania C ++ dla tej biblioteki zdefiniowałyby nowy moduł obsługi asercji, który zamiast tego zgłasza wyjątek C ++. Podobnie powiązania w języku Python definiują moduł obsługi asercji, który korzysta z interfejsu API CPython w celu zgłoszenia wyjątku w języku Python. Ale nie znam żadnych przykładów, które przyjmują takie podejście.
Odpowiedzi:
Dam ci moją perspektywę, która jest zakodowana w projekcie deal.II, do którego się odwołujesz.
Po pierwsze, istnieją dwa rodzaje warunków błędów: błędy, które można odzyskać i błędy, których nie można odzyskać.
Pierwsze dotyczy na przykład sytuacji, gdy pliku wejściowego nie można odczytać - na przykład gdy czytasz informacje z takiego pliku,
$HOME/.dealii
który może istnieć lub nie. Funkcja czytania powinna po prostu wrócić do funkcji wywoływania, aby ta ostatnia mogła dowiedzieć się, co zrobić. Może się również zdarzyć, że zasób nie jest w tej chwili dostępny, ale może być ponownie za minutę (zdalnie zamontowany system plików).To drugie, na przykład, jeśli próbujesz dodać wektor o rozmiarze 10 do wektora o rozmiarze 20: Spróbuj, jak możesz, nic nie można na to poradzić - w kodzie jest błąd punkt, w którym próbowaliśmy dodać.
Te dwa warunki należy traktować inaczej, niezależnie od używanego języka programowania:
W drugim przypadku, ponieważ nie ma możliwości odwołania, zakończ program. Możesz to zrobić, zgłaszając wyjątek lub zwracając kod błędu wskazujący dzwoniącemu, że nic nie można zrobić, ale równie dobrze możesz przerwać program od razu, ponieważ znacznie ułatwia to programistom debugowanie problemu.
W pierwszym przypadku powstała wyjątkowa sytuacja, z którą można sobie poradzić. Mimo że C i Fortran nie mieli możliwości tego wyrazić, wszystkie rozsądne języki, które pojawiły się później, włączyły sposoby do standardu językowego, aby poradzić sobie z takimi „wyjątkowymi” zwrotami, zapewniając, no cóż, „wyjątki”. Użyj ich - po to są; są również zaprojektowane w taki sposób, że nie można ich zapomnieć o ich zignorowaniu (jeśli tak, wyjątek rozprzestrzenia się tylko o jeden poziom wyżej).
Innymi słowy, zalecam tutaj (i to, co robi deal.II) to mieszanka twoich strategii 3 i 5, w zależności od kontekstu. To prawda, że 3 nie działa w językach takich jak C lub Fortran - w takim przypadku można argumentować, że jest to dobry powód, aby po prostu nie używać języków, które utrudniają wyrażenie tego, co chcesz zrobić.
Assert
wyjątku zamiast dzwonieniaabort()
.)źródło
std::exception
i można je wychwycić przez odniesienie bez znajomości typu pochodnego.