Czy program C ++ powinien wychwytywać wszystkie wyjątki i zapobiegać przedostawaniu się wyjątków poza main ()?


29

Kiedyś mi doradzono, że program C ++ powinien ostatecznie wyłapać wszystkie wyjątki. Podane wówczas uzasadnienie było zasadniczo takie, że programy, które zezwalają na wyjątki, pojawiają się poza main()wejściem w dziwny stan zombie. Powiedziano mi to kilka lat temu i z perspektywy czasu uważam, że zaobserwowane zjawisko było spowodowane długim generowaniem wyjątkowo dużych zrzutów rdzenia z przedmiotowego projektu.

W tym czasie wydawało się to dziwne, ale przekonujące. To całkowicie nonsensowne, że C ++ powinien „ukarać” programistów za to, że nie wyłapali wszystkich wyjątków, ale dowody przed mną zdawały się to potwierdzać. W przypadku omawianego projektu programy, które wprowadziły nieprzechwycone wyjątki, wydawały się wchodzić w dziwny stan zombie - lub, jak podejrzewam, przyczyną była teraz, proces pośród niechcianego zrzutu rdzenia jest niezwykle trudny do zatrzymania.

(Dla każdego, kto zastanawia się, dlaczego nie było to wtedy bardziej oczywiste: projekt wygenerował dużą ilość danych wyjściowych w wielu plikach z wielu procesów, które skutecznie przesłaniały dowolny rodzaj aborted (core dumped)wiadomości, aw tym konkretnym przypadku pośmiertne badanie zrzutów rdzenia nie było jest to ważna technika debugowania, dlatego nie rzucano dużo na zrzuty rdzenia. Problemy z programem zwykle nie zależały od stanu nagromadzonego na podstawie wielu zdarzeń w czasie przez program długo działający, ale raczej początkowe dane wejściowe do programu krótkotrwałego (< 1 godzina), więc bardziej praktyczne było ponowne uruchomienie programu z tymi samymi danymi wejściowymi z kompilacji debugowania lub w debugerze, aby uzyskać więcej informacji.)

Obecnie nie jestem pewien, czy istnieje jakaś poważna zaleta lub wada wychwytywania wyjątków wyłącznie w celu zapobiegania wyjściu wyjątków main().

Niewielką zaletą, jaką mogę wymyślić, aby pozwolić na wyjątki, aby zniknęły w przeszłości, main()jest to, że powoduje std::exception::what()to wydrukowanie wyniku na terminalu (przynajmniej w przypadku programów skompilowanych gcc w systemie Linux). Z drugiej strony jest to trywialne do osiągnięcia przez wychwycenie wszystkich wyjątków wynikających z std::exceptioni wydrukowanie wyniku, std::exception::what()a jeśli pożądane jest wydrukowanie wiadomości z wyjątku, który z niej nie pochodzi std::exception, należy ją złapać przed wyjściem main()w celu wydrukowania wiadomość.

Skromną wadą, jaką mogę wymyślić, aby pozwolić na wyjątki, aby się wybić, main()jest to, że mogą powstawać niepożądane zrzuty rdzeni. W przypadku procesu używającego dużej ilości pamięci może to być dość uciążliwe, a kontrolowanie zachowania zrzutu pamięci z programu wymaga wywołań funkcji specyficznych dla systemu operacyjnego. Z drugiej strony, jeśli pożądany jest zrzut pamięci i wyjście, można to w dowolnym momencie osiągnąć przez wywołanie, std::abort()a wyjście bez zrzutu pamięci można w dowolnym momencie wywołać std::exit().

Anegdotycznie nie sądzę, żebym kiedykolwiek widział domyślną what(): ...wiadomość drukowaną przez szeroko rozpowszechniany program po awarii.

Jakie są, jeśli w ogóle, mocne argumenty przemawiające za lub przeciw dopuszczeniu wyjątków C ++ main()?

Edycja: Na tej stronie jest wiele ogólnych pytań dotyczących obsługi wyjątków. Moje pytanie dotyczy w szczególności wyjątków w C ++, których nie można obsłużyć i które doprowadziły do ​​końca main()- być może można wydrukować komunikat o błędzie, ale jest to natychmiastowy błąd zatrzymania.




@gnat Moje pytanie jest nieco bardziej szczegółowe. Chodzi o wyjątki C ++, których nie można obsłużyć, a nie obsługę wyjątków w dowolnym języku programowania w żadnych okolicznościach.
Praxeolitic

W przypadku programów wielowątkowych rzeczy mogą być bardziej złożone lub bardziej egzotyczne (wyjątki wrt)
Basile Starynkevitch 30.01.16

1
Specjaliści od oprogramowania dla Ariane 5 z pewnością żałują, że nie złapali wszystkich wyjątków podczas pierwszego uruchomienia ...
Eric Towers

Odpowiedzi:


25

Jednym z problemów z dopuszczeniem wyjątków do głównego jest to, że program zakończy się wywołaniem, do std::terminatektórego ma zostać wywołane zachowanie domyślne std::abort. Jest to implementacja zdefiniowana tylko wtedy, gdy rozwijanie stosu jest wykonywane przed wywołaniem, terminateaby Twój program mógł zakończyć się bez wywoływania jednego destruktora! Jeśli masz jakieś zasoby, które naprawdę trzeba było przywrócić przez wywołanie destruktora, jesteś w marynacie ...


12
Jeśli masz jakieś zasoby, które naprawdę trzeba było przywrócić przez wywołanie destruktora, jesteś w marynacie ... => Biorąc pod uwagę, że (niestety) C ++ jest dość podatny na awarie i nie wspominając o tym, że system operacyjny może zdecydować się zabić twój program w dowolnym momencie (na przykład OOM Killer w Uniksie), twój program (i jego środowisko) musi być dostosowany do obsługi awarii.
Matthieu M.

@MatthieuM. Czy mówisz, że destruktory nie są dobrym miejscem do czyszczenia zasobów, które powinny wystąpić nawet podczas awarii?
Praxeolitic

6
@Praxeolitic: Mówię, że nie wszystkie awarie rozwijają stos, na przykład std::abortnie, a tym samym nieudane twierdzenia też nie. Warto również zauważyć, że we współczesnych systemach operacyjnych sam system oczyści wiele zasobów (pamięć, uchwyty plików itp.), Które są powiązane z identyfikatorem procesu. Na koniec wspomniałem również o „Defense in Depth”: nie można oczekiwać, że wszystkie inne procesy będą odporne na błędy i zawsze zwrócą zdobyte zasoby (sesje, ładnie skończą się pisanie plików ...), które musisz zaplanować to ...
Matthieu M.

3
@Praxeolitic Nie, twoje oprogramowanie nie może uszkodzić danych podczas awarii zasilania. Jeśli możesz to zrobić, możesz także nie uszkodzić danych po nieobsługiwanym wyjątku.
user253751

1
@Praxeolitic: Właściwie to istnieje. Będziesz chciał posłuchać WM_POWERBROADCASTwiadomości. Działa to tylko wtedy, gdy komputer jest zasilany bateryjnie (jeśli korzystasz z laptopa lub zasilacza UPS).
Brian

28

Głównym powodem, dla którego nie można uniknąć wyjątków, mainjest to, że w przeciwnym razie tracisz możliwość kontrolowania sposobu zgłaszania problemu użytkownikom.

W przypadku programu, który nie jest przeznaczony do używania przez długi czas lub do szerokiej dystrybucji, można zaakceptować zgłaszanie nieoczekiwanych błędów niezależnie od sposobu, w jaki system operacyjny zdecyduje się to zrobić (na przykład wyświetlenie okna dialogowego błędu w systemie Windows ).

W przypadku programów, które sprzedajesz lub które są oferowane ogółowi społeczeństwa przez organizację, której reputację należy utrzymywać, ogólnie lepiej jest zgłosić w miły sposób, że napotkałeś nieoczekiwany problem i spróbować zaoszczędzić jak najwięcej dane użytkownika, jak to możliwe. Nietracenie pracy użytkownika przez pół dnia i nieoczekiwane zawieszenie się, ale częściowe zamknięcie jest ogólnie znacznie lepsze dla reputacji firmy niż alternatywa.


Czy na pewno domyślne zachowanie wyjątku C ++, który pozostawia, main() ma wiele wspólnego z systemem operacyjnym? System operacyjny może nic nie wiedzieć o C ++. Domyślam się, że jest to określone przez kod, który kompilator wstawia gdzieś do programu.
Praxeolitic

5
@Praxeolitic: System operacyjny ustawia bardziej konwencje, a kompilator generuje kod, aby spełnić te konwencje, gdy program niespodziewanie zakończy się. Najważniejsze jest to, że należy unikać nieoczekiwanego zakończenia programu, gdy tylko jest to możliwe.
Bart van Ingen Schenau

Tracisz kontrolę tylko w zakresie standardowego C ++. Powinien być dostępny specjalny sposób obsługi takich „awarii”. C ++ zwykle nie działa w próżni.
Martin Ba

To okno dialogowe błędu bezpośredniego można skonfigurować w rejestrze na włączanie / wyłączanie tego, co jest warte - ale aby to zrobić, potrzebna byłaby kontrola rejestru - a większość programów nie działa w trybie kiosku (po przejęciu system operacyjny).
PerryC,

11

TL; DR : Co mówi specyfikacja?


Objazd techniczny ...

Gdy zgłoszony zostanie wyjątek i żaden program obsługi nie jest na niego gotowy:

  • jest to implementacja zdefiniowana, czy stos jest rozwinięty, czy nie
  • std::terminate nazywa się, co domyślnie przerywa
  • w zależności od konfiguracji środowiska, przerwanie może, ale nie musi, pozostawić raport o awarii

To ostatnie może być przydatne w przypadku bardzo rzadkich błędów (ponieważ ich odtwarzanie jest procesem marnującym czas).


To, czy złapać wszystkie wyjątki, czy nie, jest ostatecznie kwestią specyfikacji:

  • czy określono sposób zgłaszania błędów użytkownika? (niewłaściwa wartość, ...)
  • czy określono, jak zgłaszać błędy funkcjonalne? (brak katalogu docelowego, ...)
  • czy określono sposób zgłaszania błędów technicznych? (rozpalanie asercji, ...)

W przypadku każdego programu produkcyjnego należy to określić i postępować zgodnie ze specyfikacją (i być może argumentować, że należy ją zmienić).

W przypadku szybkich zestawionych programów, z których mogą korzystać tylko osoby techniczne (ty, twoi koledzy z drużyny), jedno jest w porządku. Zalecam, aby zawiesił się i skonfigurował środowisko, aby uzyskać raport lub nie, w zależności od potrzeb.


1
To. Decydujące czynniki wrt. Zasadniczo są poza zakresem C ++.
Martin Ba

4

Wyłapany wyjątek daje możliwość wydrukowania ładnego komunikatu o błędzie lub nawet próby przywrócenia go po błędzie (być może po prostu poprzez ponowne uruchomienie aplikacji).

Jednak w C ++ wyjątek nie przechowuje informacji o stanie programu, gdy został zgłoszony. Jeśli go złapiesz, cały taki stan zostanie zapomniany, natomiast jeśli pozwolisz, aby program się zawiesił, zwykle jest on nadal obecny i można go odczytać ze zrzutu programu, co ułatwia debugowanie.

Więc to jest kompromis.


4

W momencie, gdy wiesz, że musisz przerwać, idź std::terminatejuż i zadzwoń, aby ograniczyć dalsze obrażenia.

Jeśli wiesz, że możesz bezpiecznie zakończyć, zrób to zamiast tego. Pamiętaj, że odwijanie stosu nie jest gwarantowane, gdy wyjątek nigdy nie zostanie złapany, więc łap i odrzuć.

Jeśli możesz bezpiecznie zgłosić / zarejestrować błąd lepiej niż system zrobi to sam, śmiało.
Ale naprawdę upewnij się, że nie przypadkowo pogorszysz sytuację.

Często jest już za późno, aby zapisać dane po wykryciu nieodwracalnego błędu, choć zależy to od konkretnego błędu.
W każdym razie, jeśli twój program jest napisany z myślą o szybkim odzyskaniu, zabicie go może być najlepszym sposobem na jego zakończenie, nawet jeśli jest to zwykłe zamknięcie.


1

Rozbijanie się z wdziękiem jest dobrą rzeczą przez większość czasu - ale są kompromisy. Czasami dobrze jest upaść. Powinienem wspomnieć, że głównie myślę o debugowaniu w bardzo dużych. W przypadku prostego programu - chociaż może być nadal przydatny, nie jest tak pomocny, jak w przypadku bardzo złożonego programu (lub kilku złożonych programów wchodzących w interakcje).

Nie chcesz upaść publicznie (chociaż jest to naprawdę nieuniknione - awarie programów, a o tym, o czym tu mówimy, nie ma mowy o naprawdę matematycznie weryfikowalnym programie, który nie ulega awarii). Pomyśl o Bill Gates BSODing w trakcie demo - źle, prawda? Niemniej jednak możemy spróbować dowiedzieć się, dlaczego rozbiliśmy się i nie rozbiliśmy ponownie w ten sam sposób.

Włączyłem funkcję raportowania błędów systemu Windows, która tworzy lokalne zrzuty awarii w przypadku nieobsługiwanych wyjątków. Działa cuda, jeśli masz pliki symboli powiązane z twoją (zawieszającą się) kompilacją. Co ciekawe, ponieważ skonfigurowałem to narzędzie, chcę więcej awarii - ponieważ uczę się od każdej awarii. Potem mogę naprawić błędy i mniej awarii.

Na razie chcę, aby moje programy rzuciły się na szczyt i uległy awarii. W przyszłości mógłbym z wdziękiem zjadać wszystkie moje wyjątki - ale nie jeśli sprawię, że będą dla mnie działać.

Możesz przeczytać więcej o lokalnych zrzutach awaryjnych tutaj, jeśli jesteś zainteresowany: Zbieranie zrzutów w trybie użytkownika


-6

Ostatecznie, jeśli wyjątek pojawi się poza main (), spowoduje to awarię aplikacji, a moim zdaniem aplikacja nigdy nie powinna ulec awarii. Jeśli można zawiesić aplikację w jednym miejscu, to dlaczego nigdzie nie? Po co w ogóle zawracać sobie głowę obsługą wyjątków? (Sarkazm, niezbyt sugerujący to ...)

Możesz mieć globalną próbę / catch, która drukuje elegancką wiadomość informującą użytkownika, że ​​wystąpił nieusuwalny błąd i że musi on wysłać e-maila do boba w IT na ten temat lub coś podobnego, ale zawieszanie się jest całkowicie nieprofesjonalne i niedopuszczalne. Jest to trywialne, aby zapobiec, a użytkownik może poinformować użytkownika o tym, co się właśnie wydarzyło i jak to naprawić, aby to się nie powtórzyło.

Crashing to ściśle amatorska godzina.


8
Więc lepiej udawać, że wszystko działa poprawnie i zamiast tego zastąpić całą pamięć stałą bełkotem?
Deduplicator

1
@Deduplicator: Nie: zawsze możesz złapać wyjątek i go obsłużyć.
Giorgio

5
Jeśli wiesz, jak sobie z tym poradzić, najprawdopodobniej poradzisz sobie z tym na długo, zanim wstaniesz main. Jeśli nie wiesz, jak sobie z tym poradzić, udawanie, że to robisz, jest oczywiście strasznym błędem.
Deduplicator

8
ale zawieszanie się jest całkowicie nieprofesjonalne i nieakceptowalne => spędzanie czasu na pracy nad bezużytecznymi funkcjami jest nieprofesjonalne: zawieszanie się ma znaczenie tylko wtedy, gdy jest ważne dla użytkownika końcowego, jeśli nie, to pozwalanie na awarię (i otrzymywanie raportu o awarii z zrzutem pamięci) jest bardziej ekonomiczny.
Matthieu M.

Matthieu M., jeśli naprawdę tyle wysiłku wymaga zalogowanie błędu, zapisanie otwartych plików i namalowanie przyjaznej wiadomości na ekranie, nie wiem, co powiedzieć. Jeśli uważasz, że nie ma sensu z wdziękiem ponieść porażki, prawdopodobnie powinieneś porozmawiać z kimkolwiek, kto robi wsparcie techniczne dla twojego kodu.
Roger Hill,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.