Jak mogę tworzyć i egzekwować umowy dotyczące wyjątków?

33

Staram się przekonać mój zespół, aby zezwalał na stosowanie wyjątków w C ++ zamiast zwracania wartości bool isSuccessfullub wyliczenia z kodem błędu. Nie mogę jednak odeprzeć jego krytyki.

Rozważ tę bibliotekę:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. Rozważ programistę wywołującego tę B()funkcję. Sprawdza jego dokumentację i widzi, że nie zwraca żadnych wyjątków, więc nie próbuje niczego złapać. Ten kod może spowodować awarię programu podczas produkcji.

  2. Rozważ programistę wywołującego tę C()funkcję. Nie sprawdza dokumentacji, więc nie wychwytuje żadnych wyjątków. Wywołanie jest niebezpieczne i może spowodować awarię programu podczas produkcji.

Ale jeśli sprawdzimy pod kątem błędów w ten sposób:

void old_C(myenum &return_code);

Deweloper korzystający z tej funkcji zostanie ostrzeżony przez kompilator, jeśli nie poda tego argumentu, i powie „Aha, to zwraca kod błędu, który muszę sprawdzić”.

Jak mogę bezpiecznie korzystać z wyjątków, aby zawrzeć jakąś umowę?

DBedrenko
źródło
4
@ Sjoerd Główną zaletą jest to, że programista jest zmuszony przez kompilator do zapewnienia zmiennej funkcji do przechowywania kodu powrotu; uświadamia się mu, że może wystąpić błąd, i powinien sobie z tym poradzić. Jest to nieskończenie lepiej niż wyjątki, które nie mają żadnej kontroli czasu kompilacji.
DBedrenko
4
„powinien sobie z tym poradzić” - nie ma też możliwości sprawdzenia, czy tak się stanie.
Sjoerd,
4
@Sjoerd To nie jest konieczne. Jest wystarczająco dobry, aby powiadomić programistę, że może wystąpić błąd i że powinien go sprawdzić. Oceniający oceniają również, czy sprawdzili, czy nie zawierają błędu.
DBedrenko
5
Możesz mieć większą szansę na użycie Either/ Resultmonad, aby zwrócić błąd w sposób bezpieczny dla typu
Daenyth,
5
@gardenhead Ponieważ zbyt wielu programistów nie używa go poprawnie, kończy się brzydkim kodem i zamiast tego obwiniają tę funkcję. Sprawdzona funkcja wyjątku w Javie to piękna rzecz, ale dostaje zły rap, ponieważ niektórzy ludzie po prostu tego nie rozumieją.
AxiomaticNexus

Odpowiedzi:

47

Jest to uzasadniona krytyka wyjątków. Często są one mniej widoczne niż prosta obsługa błędów, taka jak zwracanie kodu. I nie ma łatwego sposobu na wyegzekwowanie „umowy”. Częściowo chodzi o to, aby umożliwić wychwytywanie wyjątków na wyższym poziomie (jeśli musisz wychwycić każdy wyjątek na każdym poziomie, czym różni się od zwracania kodu błędu?). A to oznacza, że ​​twój kod może zostać wywołany przez inny kod, który nie obsługuje go odpowiednio.

Wyjątki mają wady; musisz złożyć skargę na podstawie kosztów i korzyści.
Uznałem te dwa artykuły za pomocne: konieczność wyjątków i wszystko jest nie tak z wyjątkami. Ponadto ten post na blogu zawiera opinie wielu ekspertów na temat wyjątków, ze szczególnym uwzględnieniem C ++ . Choć opinia ekspertów wydaje się opowiadać się za wyjątkami, daleka jest od jednoznacznego konsensusu.

Jeśli chodzi o przekonanie lidera zespołu, może to nie być właściwa bitwa do wyboru. Zwłaszcza nie ze starszym kodem. Jak zauważono w drugim linku powyżej:

Wyjątków nie można propagować za pomocą kodu, który nie jest bezpieczny. Zastosowanie wyjątków oznacza zatem, że cały kod w projekcie musi być bezpieczny w wyjątkach.

Dodanie odrobiny kodu, który wykorzystuje wyjątki do projektu, który głównie tego nie robi, prawdopodobnie nie będzie poprawą. Nieużywanie wyjątków w dobrze napisanym kodzie nie jest katastrofalnym problemem; w zależności od aplikacji i pytanego eksperta może to wcale nie stanowić problemu. Musisz wybrać swoje bitwy.

Prawdopodobnie nie jest to argument, który poświęciłbym wysiłkowi - przynajmniej nie do momentu rozpoczęcia nowego projektu. A nawet jeśli masz nowy projekt, czy będzie on używał lub będzie używany przez dowolny starszy kod?


źródło
1
Dziękuję za radę i linki. To nowy projekt, a nie stary. Zastanawiam się, dlaczego wyjątki są tak powszechne, gdy mają tak dużą wadę, że ich stosowanie jest niebezpieczne. Jeśli złapiesz wyjątek n-poziomy wyżej, a następnie zmodyfikujesz kod i przeniesiesz go, nie ma statycznego sprawdzania, aby zagwarantować, że wyjątek jest nadal wychwytywany i obsługiwany (i to zbyt wiele, aby prosić recenzentów o sprawdzenie wszystkich funkcji n-poziomy głęboko ).
DBedrenko
3
@Zobacz powyższe linki, aby znaleźć argumenty na korzyść wyjątków (dodałem dodatkowy link z bardziej specjalistyczną opinią).
6
Ta odpowiedź jest na miejscu!
Doc Brown
1
Z doświadczenia mogę stwierdzić, że próba dodania wyjątków do istniejącej bazy kodu jest NAPRAWDĘ złym pomysłem. Pracowałem z taką bazą kodów i było NAPRAWDĘ, NAPRAWDĘ ciężko pracować, ponieważ nigdy nie wiedziałeś, co wybuchnie na twojej twarzy. Wyjątków należy używać TYLKO w projektach, w których planujesz je z góry.
Michael Kohne
26

Na ten temat napisano dosłownie całe książki, więc każda odpowiedź będzie co najwyżej streszczeniem. Oto niektóre z ważnych punktów, które moim zdaniem warto na podstawie twojego pytania. To nie jest wyczerpująca lista.


Wyjątki nie są przeznaczone do złapania w całym miejscu.

Tak długo, jak w głównej pętli znajduje się ogólna procedura obsługi wyjątków - w zależności od typu aplikacji (serwer WWW, usługa lokalna, narzędzie wiersza poleceń ...) - zwykle masz wszystkie potrzebne procedury obsługi wyjątków.

W moim kodzie jest tylko kilka instrukcji catch - jeśli w ogóle - poza główną pętlą. I wydaje się, że jest to powszechne podejście we współczesnym C ++.


Wyjątki i kody powrotu nie wykluczają się wzajemnie.

Nie powinieneś robić z tego podejścia „wszystko albo nic”. W wyjątkowych sytuacjach należy stosować wyjątki. Rzeczy takie jak „Nie znaleziono pliku konfiguracji”, „Dysk pełny” lub cokolwiek innego, co nie może być obsługiwane lokalnie.

Typowe awarie, takie jak sprawdzenie, czy nazwa pliku podana przez użytkownika jest poprawna, nie jest przypadkiem użycia wyjątków; Zamiast tego użyj wartości zwracanej w tych przypadkach.

Jak widać z powyższych przykładów, „nie znaleziono pliku” może być wyjątkiem lub kodem powrotu, w zależności od przypadku użycia: „jest częścią instalacji”, a „użytkownik może napisać literówkę”.

Więc nie ma absolutnej zasady. Zgrubna wskazówka jest następująca: jeśli może być obsługiwany lokalnie, należy ustawić wartość zwracaną; jeśli nie możesz poradzić sobie z tym lokalnie, zgłoś wyjątek.


Statyczne sprawdzanie wyjątków nie jest przydatne.

Ponieważ wyjątki i tak nie są obsługiwane lokalnie, zazwyczaj nie ma znaczenia, które wyjątki można zgłaszać. Jedyną przydatną informacją jest to, czy można zgłosić wyjątek.

Java ma sprawdzanie statyczne, ale zwykle jest uważane za nieudany eksperyment, a większość języków - zwłaszcza C # - nie ma tego rodzaju sprawdzania statycznego. To dobra lektura na temat powodów, dla których C # go nie ma.

Z tych powodów C ++ wycofało się throw(exceptionA, exceptionB)na korzyść noexcept(true). Domyślnie funkcja może rzucać, więc programiści powinni się tego spodziewać, chyba że dokumentacja wyraźnie obiecuje inaczej.


Pisanie bezpiecznego kodu wyjątku nie ma nic wspólnego z pisaniem programów obsługi wyjątków.

Wolę powiedzieć, że pisanie bezpiecznego kodu wyjątku polega na tym, jak unikać pisania programów obsługi wyjątków!

Większość najlepszych praktyk ma na celu zmniejszenie liczby procedur obsługi wyjątków. Jednokrotne napisanie kodu i automatyczne wywołanie go - np. Przez RAII - powoduje mniej błędów niż kopiowanie i wklejanie tego samego kodu w całym miejscu.

Sjoerd
źródło
3
Tak długo, jak istnieje ogólna procedura obsługi wyjątków ... zwykle jest już w porządku. ” Jest to sprzeczne z większością źródeł, które podkreślają, że napisanie bezpiecznego kodu wyjątku jest bardzo trudne.
12
@ dan1111 „Zapisywanie bezpiecznego kodu wyjątku” ma niewiele wspólnego z pisaniem programów obsługi wyjątków. Pisanie kodu bezpiecznego wyjątku dotyczy używania RAII do enkapsulacji zasobów, używania „najpierw pracuj lokalnie, a następnie zamiany / przenoszenia” i innych dobrych praktyk. Są trudne, gdy się do nich nie przyzwyczajasz. Ale „pisanie programów obsługi wyjątków” nie jest częścią tych najlepszych praktyk.
Sjoerd,
4
@AxiomaticNexus Na pewnym poziomie możesz wyjaśnić każde nieudane użycie funkcji jako „złych programistów”. Największe pomysły to te, które ograniczają niewłaściwe użytkowanie, ponieważ ułatwiają właściwe postępowanie. Statycznie sprawdzone wyjątki nie należą do tej kategorii. Tworzą pracę nieproporcjonalną do ich wartości, często w miejscach, w których pisany kod po prostu nawet nie musi o nich dbać. Użytkownicy mają pokusę, aby uciszyć je w niewłaściwy sposób, aby kompilator przestał ich niepokoić. Nawet dobrze wykonane, prowadzą do wielu bałaganu.
jpmc26,
4
@AxiomaticNexus Powodem, dla którego jest nieproporcjonalny, jest to, że można go łatwo propagować do prawie każdej funkcji w całej bazie kodu . Kiedy wszystkie funkcje w połowie moich klas uzyskują dostęp do bazy danych, nie każda z nich musi jawnie zadeklarować, że może zgłosić wyjątek SQLException; podobnie jak każda metoda kontrolera, która je wywołuje. Taki hałas jest w rzeczywistości wartością ujemną, niezależnie od tego, jak niewielki jest nakład pracy. Sjoerd uderza w sedno: większość kodu nie musi dotyczyć wyjątków, ponieważ i tak nic na to nie poradzi .
jpmc26
3
@AxiomaticNexus Tak, ponieważ najlepsi programiści są tak doskonali, ich kod zawsze doskonale poradzi sobie z każdym możliwym wejściem użytkownika i nigdy nie zobaczysz tych wyjątków w prod. A jeśli to zrobisz, oznacza to, że jesteś życiową porażką. Poważnie, nie dawaj mi tych śmieci. Nikt nie jest tak doskonały, nawet najlepszy. Twoja aplikacja powinna zachowywać się zdrowo nawet w obliczu tych błędów, nawet jeśli „zdrowo” to „zatrzymaj się i zwróć stronę / komunikat o połowie przyzwoitego błędu”. I zgadnij co: to samo robisz z 99% IOExceptions . Sprawdzony podział jest arbitralny i bezużyteczny.
jpmc26,
8

Programiści C ++ nie szukają specyfikacji wyjątków. Szukają gwarancji wyjątku.

Załóżmy, że fragment kodu zgłosił wyjątek. Jakie założenia może przyjąć programista, że ​​nadal będą aktualne? Co ze sposobu pisania kodu gwarantuje kod po wyjątku?

Czy też jest możliwe, że pewien fragment kodu może zagwarantować, że nigdy nie wyrzuci (tj. Nic poza zakończeniem procesu systemu operacyjnego)?

Słowo „wycofać” pojawia się często w dyskusjach na temat wyjątków. Możliwość przywrócenia do prawidłowego stanu (który jest wyraźnie udokumentowany) jest przykładem gwarancji wyjątku. Jeśli nie ma gwarancji wyjątku, program powinien zakończyć się na miejscu, ponieważ nie ma nawet gwarancji, że kod, który wykona później, będzie działał zgodnie z przeznaczeniem - powiedzmy, jeśli pamięć ulegnie uszkodzeniu, dalsze działanie jest technicznie niezdefiniowanym zachowaniem.

Różne techniki programowania w C ++ promują wyjątki. RAII (zarządzanie zasobami oparte na zakresie) zapewnia mechanizm wykonywania kodu czyszczenia i zapewnia, że ​​zasoby są zwalniane zarówno w normalnych, jak i wyjątkowych przypadkach. Wykonanie kopii danych przed wykonaniem modyfikacji na obiektach pozwala przywrócić stan tego obiektu, jeśli operacja się nie powiedzie. I tak dalej.

Odpowiedzi na to pytanie StackOverflow dają wgląd w to, jak długo programiści C ++ rozumieją wszystkie możliwe tryby awarii, które mogą się zdarzyć w ich kodzie, i starają się strzec poprawności stanu programu pomimo awarii. Analiza kodu C ++ wiersz po wierszu staje się nawykiem.

Podczas programowania w C ++ (do użytku produkcyjnego) nie można pozwolić sobie na połysk szczegółów. Ponadto binarny obiekt blob (nie open-source) jest zmorą programistów C ++. Jeśli będę musiał wywołać jakiś binarny obiekt blob, a ten obiekt zawiedzie, wówczas inżynieria odwrotna jest tym, co zrobiłby programista C ++.

Odniesienie: http://en.cppreference.com/w/cpp/language/exceptions#Exception_safety - patrz punkt Bezpieczeństwo wyjątków.

C ++ nieudana próba zaimplementowania specyfikacji wyjątków. Późniejsza analiza w innych językach wskazuje, że specyfikacje wyjątków po prostu nie są praktyczne.

Dlaczego jest to nieudana próba: aby ją ściśle egzekwować, musi być częścią systemu typów. Ale tak nie jest. Kompilator nie sprawdza specyfikacji wyjątku.

Dlaczego C ++ to wybrało i dlaczego doświadczenia z innych języków (Java) dowodzą, że specyfikacja wyjątków jest dyskusyjna: gdy ktoś modyfikuje implementację funkcji (na przykład musi wywołać inną funkcję, która może rzucić nowy rodzaj wyjątek), ścisłe wymuszanie specyfikacji wyjątków oznacza, że ​​musisz również zaktualizować tę specyfikację. To się rozprzestrzenia - może być konieczne zaktualizowanie specyfikacji wyjątków dla dziesiątek lub setek funkcji, co jest prostą zmianą. Gorzej jest z abstrakcyjnymi klasami podstawowymi (odpowiednik interfejsów C ++). Jeśli specyfikacja wyjątków zostanie wymuszona na interfejsach, implementacje interfejsów nie będą mogły wywoływać funkcji, które generują różne typy wyjątków.

Odniesienie: http://www.gotw.ca/publications/mill22.htm

Począwszy od wersji C ++ 17, [[nodiscard]]atrybutu można użyć w przypadku zwracanych wartości funkcji (patrz: https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value -nie można ignorować ).

Więc jeśli dokonam zmiany kodu i wprowadzę nowy rodzaj warunku awarii (tj. Nowy typ wyjątku), czy jest to zmiana przełomowa? Czy powinien był zmusić dzwoniącego do zaktualizowania kodu, czy przynajmniej zostać ostrzeżony o zmianie?

Jeśli zaakceptujesz argumenty, że programiści C ++ szukają gwarancji wyjątku zamiast specyfikacji wyjątku, wówczas odpowiedź jest taka, że ​​jeśli nowy rodzaj warunku niepowodzenia nie złamie żadnego z wyjątków gwarantujących poprzednio obiecany kod, nie będzie to zmiana przełomowa.

rwong
źródło
„Po zmodyfikowaniu implementacji funkcji (na przykład musi ona wywołać inną funkcję, która może wygenerować nowy rodzaj wyjątku), ścisłe wymuszanie specyfikacji wyjątków oznacza, że ​​musisz również zaktualizować tę specyfikację” - Dobra tak jak powinieneś. Jaka byłaby alternatywa? Ignorujesz nowo wprowadzony wyjątek?
AxiomaticNexus
@AxiomaticNexus Na to również udzielono gwarancji wyjątku. W rzeczywistości twierdzę, że specyfikacja nigdy nie będzie kompletna; gwarancja jest tym, czego szukamy. Często porównujemy typ wyjątku, próbując dowiedzieć się, która gwarancja została złamana - aby zgadnąć, która gwarancja była nadal ważna. W specyfikacji wyjątków wymieniono niektóre typy, które należy sprawdzić, ale pamiętaj, że w żadnym praktycznym systemie lista nie może być kompletna. W większości przypadków zmusza to ludzi do skorzystania ze skrótu „zjadania” typu wyjątku, owijając go w inny wyjątek, co utrudnia sprawdzanie typu wyjątku
rwong
@AxiomaticNexus Nie jestem ani za, ani za argumentem przeciwko zastosowaniu wyjątku. Typowe użycie wyjątków zachęca do posiadania podstawowej klasy wyjątków i niektórych pochodnych klas wyjątków. Tymczasem należy pamiętać, że możliwe są inne typy wyjątków, np. Wyrzucone z C ++ STL lub innych bibliotek. Można argumentować, że specyfikacja wyjątku mogłaby działać w tym przypadku: należy określić, że std::exceptionzostaną wygenerowane standardowe wyjątki ( ) i własny wyjątek podstawowy. Ale zastosowane do całego projektu oznacza po prostu, że każda funkcja będzie miała tę samą specyfikację: hałas.
rwong
Chciałbym zauważyć, że jeśli chodzi o wyjątki, C ++ i Java / C # są różnymi językami. Po pierwsze C ++ nie jest bezpieczny pod względem typu w sensie pozwalającym na wykonanie dowolnego kodu lub zastąpienie danych, po drugie C ++ nie drukuje ładnego śladu stosu. Różnice te zmusiły programistów C ++ do dokonania różnych wyborów dotyczących obsługi błędów i wyjątków. Oznacza to również, że niektóre projekty mogą słusznie twierdzić, że C ++ nie jest dla nich odpowiednim językiem. (Na przykład osobiście uważam, że dekodowanie obrazu TIFF nie będzie dozwolone w C lub C ++.)
rwong 28.10.16
1
@rwong: Dużym problemem związanym z obsługą wyjątków w językach zgodnych z modelem C ++ jest to, że catchopiera się na typie obiektu wyjątku, gdy w wielu przypadkach liczy się nie tyle bezpośrednia przyczyna wyjątku, co raczej implikacja stan systemu. Niestety, typ wyjątku ogólnie nie mówi nic o tym, czy spowodował on zakłócenie kodu przez fragment kodu w sposób, który pozostawił obiekt w częściowo zaktualizowanym (a tym samym niepoprawnym) stanie.
supercat
4

Rozważ programistę wywołującego funkcję C (). Nie sprawdza dokumentacji, więc nie wychwytuje żadnych wyjątków. Połączenie jest niebezpieczne

Jest całkowicie bezpieczny. Nie musi wychwytywać wyjątków w każdym miejscu, może po prostu rzucić próbę / złapać w miejscu, w którym może faktycznie zrobić coś pożytecznego. Jest to niebezpieczne tylko wtedy, gdy pozwala na wyciek z nici, ale zazwyczaj nie jest trudno temu zapobiec.

DeadMG
źródło
Jest to niebezpieczne, ponieważ nie oczekuje wyjątku C(), a tym samym nie chroni przed kodem po pominięciu wywołania. Jeśli ten kod jest wymagany do zagwarantowania, że ​​pewna niezmienność pozostaje prawdziwa, gwarancja ta wygasa przy pierwszym wyjściu wyjątku C(). Nie ma czegoś takiego jak całkowicie bezpieczne oprogramowanie wyjątkowe, a na pewno nie tam, gdzie nigdy nie oczekiwano wyjątków.
cmaster
1
@cmaster Nie spodziewanie się wyjątkuC() to wielki błąd. Domyślnie w C ++ jest to, że każda funkcja może rzucać - wymaga jawnego noexcept(true)wskazania kompilatorowi inaczej. W przypadku braku tej obietnicy programista musi założyć, że funkcja może rzucać.
Sjoerd
1
@cmaster To jest jego własna głupia wina i nie ma nic wspólnego z autorem C (). Wyjątkiem jest bezpieczeństwo, powinien się o tym dowiedzieć i postępować zgodnie z nim. Jeśli wywoła funkcję, o której nie wie, że nie jest wyjątkiem, powinien cholernie dobrze poradzić sobie z tym, co się stanie, jeśli ją wyrzuci. Jeśli nie potrzebuje wyjątku, powinien znaleźć implementację.
DeadMG,
@Sjoerd i DeadMG: Chociaż standard zezwala dowolnej funkcji na zgłaszanie wyjątku, nie oznacza to, że projekty muszą dopuszczać wyjątki w kodzie. Jeśli zakazujesz wyjątków od swojego kodu, tak jak robi to lider zespołu PO, nie musisz programować w sposób bezpieczny. A jeśli spróbujesz przekonać kierownika zespołu, aby zezwalał na wyjątki w bazie kodu, która wcześniej była wolna od wyjątków, pojawi się mnóstwo C()funkcji, które psują się w obecności wyjątków. Jest to naprawdę bardzo nierozsądna rzecz, jest skazana na wiele problemów.
cmaster
@cmaster Chodzi o nowy projekt - patrz pierwszy komentarz do zaakceptowanej odpowiedzi. Więc nie ma żadnych istniejących funkcji, które mogłyby się zepsuć, ponieważ nie ma żadnych istniejących funkcji.
Sjoerd,
3

Jeśli budujesz system krytyczny, rozważ stosowanie się do wskazówek lidera zespołu i nie używaj wyjątków. Jest to reguła AV 208 w standardach kodowania C ++ Lockheed Martin Joint Strike Fighter Air Vehicle C ++ . Z drugiej strony, wytyczne MISRA C ++ mają bardzo szczegółowe zasady dotyczące tego, kiedy wyjątki mogą i nie mogą być stosowane, jeśli budujesz system oprogramowania zgodny z MISRA.

Jeśli budujesz krytyczne systemy, prawdopodobnie używasz również narzędzi do analizy statycznej. Wiele narzędzi analizy statycznej ostrzega, jeśli nie sprawdzisz wartości zwracanej przez metodę, dzięki czemu przypadki braku obsługi błędów będą wyraźnie widoczne. Według mojej najlepszej wiedzy, podobne wsparcie narzędzi do wykrywania prawidłowej obsługi wyjątków nie jest tak silne.

Ostatecznie argumentowałbym, że projektowanie na podstawie umowy i programowanie obronne w połączeniu z analizą statyczną jest bezpieczniejsze dla krytycznych systemów oprogramowania niż wyjątki.

Thomas Owens
źródło