Staram się przekonać mój zespół, aby zezwalał na stosowanie wyjątków w C ++ zamiast zwracania wartości bool isSuccessful
lub 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();
};
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.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ę?
źródło
Either
/Result
monad, aby zwrócić błąd w sposób bezpieczny dla typuOdpowiedzi:
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:
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
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.
źródło
IOException
s . Sprawdzony podział jest arbitralny i bezużyteczny.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.
źródło
std::exception
zostaną 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.catch
opiera 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.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.
źródło
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ątkuC()
. Nie ma czegoś takiego jak całkowicie bezpieczne oprogramowanie wyjątkowe, a na pewno nie tam, gdzie nigdy nie oczekiwano wyjątków.C()
to wielki błąd. Domyślnie w C ++ jest to, że każda funkcja może rzucać - wymaga jawnegonoexcept(true)
wskazania kompilatorowi inaczej. W przypadku braku tej obietnicy programista musi założyć, że funkcja może rzucać.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.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.
źródło