Większość ludzi twierdzi, że nigdy nie wyrzucaj wyjątków z destruktora - powoduje to nieokreślone zachowanie. Stroustrup podkreśla, że „wektor destruktora jawnie przywołuje destruktor dla każdego elementu. Oznacza to, że jeśli rzuca element destruktor, zniszczenie wektora nie powiedzie się ... Naprawdę nie ma dobrego sposobu na ochronę przed wyjątkami rzuconymi przez destruktory, więc biblioteka nie daje żadnych gwarancji, jeśli wyrzuci element niszczyciel ”(z Załącznika E3.2) .
Ten artykuł wydaje się mówić inaczej - że rzucanie niszczycieli jest mniej więcej w porządku.
Moje pytanie brzmi zatem: jeśli wyrzucenie z destruktora powoduje niezdefiniowane zachowanie, jak radzisz sobie z błędami występującymi podczas niszczenia?
Jeśli podczas operacji czyszczenia wystąpi błąd, czy po prostu go zignorujesz? Jeśli jest to błąd, który potencjalnie można obsłużyć na stosie, ale nie jest on bezpośrednio w destruktorze, czy nie ma sensu wyrzucać wyjątku z destruktora?
Oczywiście tego rodzaju błędy są rzadkie, ale możliwe.
źródło
xyz()
i utrzymuj destruktor w czystości od logiki innej niż RAII.commit()
zostanie wywołana metoda.Odpowiedzi:
Wyrzucenie wyjątku z destruktora jest niebezpieczne.
Jeśli propaguje się inny wyjątek, aplikacja zostanie zakończona.
Zasadniczo sprowadza się to do:
Wszystko, co niebezpieczne (tj. Które mogłoby spowodować wyjątek), powinno odbywać się publicznie (niekoniecznie bezpośrednio). Użytkownik twojej klasy może następnie potencjalnie poradzić sobie z tymi sytuacjami, używając publicznych metod i wychwytując wszelkie potencjalne wyjątki.
Destruktor wykończy obiekt, wywołując te metody (jeśli użytkownik nie zrobił tego jawnie), ale wszelkie wyjątki rzucania są wychwytywane i usuwane (po próbie rozwiązania problemu).
W rezultacie przekazujesz odpowiedzialność na użytkownika. Jeśli użytkownik jest w stanie poprawić wyjątki, ręcznie wywoła odpowiednie funkcje i przetworzy wszelkie błędy. Jeśli użytkownik obiektu nie martwi się (ponieważ obiekt zostanie zniszczony), niszczyciel pozostawi się do załatwienia.
Przykład:
std :: fstream
Metoda close () może potencjalnie wygenerować wyjątek. Destruktor wywołuje close (), jeśli plik został otwarty, ale upewnia się, że wszelkie wyjątki nie rozprzestrzeniają się poza destruktorem.
Jeśli więc użytkownik obiektu pliku chce wykonać specjalną obsługę problemów związanych z zamykaniem pliku, ręcznie wywoła metodę close () i obsłuży wszelkie wyjątki. Z drugiej strony, jeśli ich to nie obchodzi, niszczyciel pozostawi resztę sytuacji.
Scott Myers ma doskonały artykuł na ten temat w swojej książce „Effective C ++”
Edytować:
Najwyraźniej także w „Efektywniejszym C ++”
pozycja 11: Zapobiegaj wyjątkom od opuszczania destruktorów
źródło
Wyrzucenie z destruktora może spowodować awarię, ponieważ ten destruktor może zostać wywołany jako część „Odwijania stosu”. Odwijanie stosu to procedura, która ma miejsce po zgłoszeniu wyjątku. W tej procedurze wszystkie obiekty, które zostały wepchnięte do stosu od momentu „wypróbowania” i do momentu zgłoszenia wyjątku, zostaną zakończone -> zostaną wywołane ich destruktory. Podczas tej procedury kolejny wyjątek nie jest dozwolony, ponieważ nie jest możliwe jednoczesne obsłużenie dwóch wyjątków, co spowoduje wywołanie abort (), program się zawiesi, a sterowanie powróci do systemu operacyjnego.
źródło
throw
ale jeszcze nie znalazłcatch
dla niego bloku) w takim przypadkustd::terminate
(nieabort
) jest wywoływane zamiast zgłaszania (nowego) wyjątku (lub kontynuowania rozwijania stosu).W tym przypadku musimy rozróżnić , zamiast ślepo stosować się do ogólnych porad dotyczących konkretnych przypadków.
Zwróć uwagę, że poniższe ignoruje kwestię pojemników z obiektami i co robić w obliczu wielu d'torów obiektów wewnątrz pojemników. (I można to częściowo zignorować, ponieważ niektóre obiekty po prostu nie nadają się do umieszczenia w pojemniku).
Cały problem staje się łatwiejszy do myślenia, kiedy dzielimy klasy na dwa typy. Dtor klasy może mieć dwa różne obowiązki:
Jeśli spojrzymy na to pytanie w ten sposób, myślę, że można argumentować, że semantyka (R) nigdy nie powinna powodować wyjątku od dtor, ponieważ nie ma: a) nic na to nie poradzimy ib) wiele operacji wolnych zasobów przewidują nawet kontrolę błędów, np .
void
free(void* p);
Przedmioty z (c) semantyki, jak obiekt pliku, który musi skutecznie przepłukać go za dane lub ( „zakres strzeżonego”) połączenia z bazą danych, która ma commit w dtor są różnego rodzaju: My może zrobić coś o błędzie (na poziom aplikacji) i naprawdę nie powinniśmy kontynuować, jakby nic się nie wydarzyło.
Jeśli podążymy trasą RAII i pozwolimy na obiekty, które mają semantykę (C) w swoich d'torach, myślę, że wtedy musimy również uwzględnić dziwny przypadek, w którym takie d'tory mogą rzucać. Wynika z tego, że nie powinieneś umieszczać takich obiektów w kontenerach, a także wynika z tego, że program może nadal
terminate()
zgłaszać zatwierdzenie, gdy aktywny jest inny wyjątek.Jeśli chodzi o obsługę błędów (semantyka zatwierdzania / wycofywania zmian) i wyjątki, dobrze przemawia jeden z Andrei Alexandrescu : Obsługa błędów w C ++ / deklaratywnym przepływie kontroli (odbędzie się w NDC 2014 )
W szczegółach wyjaśnia, w jaki sposób biblioteka Folly implementuje narzędzie
UncaughtExceptionCounter
do swoichScopeGuard
narzędzi.(Powinienem zauważyć, że inni też mieli podobne pomysły).
Chociaż rozmowa nie koncentruje się na rzucaniu z d'tora, pokazuje narzędzie, którego można dziś użyć, aby pozbyć się problemów z tym, kiedy rzucić z d'tora.
W
przyszłości, niemożebyć standardowym elementem do tego,patrz N3614 ,a dyskusja o tym .Aktualizacja '17: funkcja C ++ 17 std do tego jest
std::uncaught_exceptions
afaikt. Szybko zacytuję artykuł CPpref:źródło
finally
.finally
jest dtor . Zawsze się nazywa, bez względu na wszystko. Aby uzyskać przybliżenie składniowe wreszcie, zobacz różne implementacje scope_guard. W dzisiejszych czasach, gdy maszyna jest na miejscu (nawet w standardzie, czy to C ++ 14?), Aby wykryć, czy dtor może rzucać, można nawet całkowicie go zabezpieczyć.finally
natury narzędziem dla (C). Jeśli nie rozumiesz, dlaczego: zastanów się, dlaczego uzasadnione jest umieszczanie wyjątków jeden na drugim wfinally
blokach i dlaczego to samo nie dotyczy niszczycieli. (W pewnym sensie jest to kwestia kontroli danych i danych . Niszczyciele służą do uwalniania danych, służą do zwalnianiafinally
kontroli. Są różne; szkoda, że C ++ je łączy).Prawdziwe pytanie, jakie należy sobie zadać na temat rzucania z destruktora, brzmi: „Co może zrobić osoba dzwoniąca?” Czy rzeczywiście jest coś przydatnego, co można zrobić z wyjątkiem, który zrównoważyłby zagrożenia związane z rzuceniem z destruktora?
Jeśli zniszczę
Foo
obiekt, aFoo
destruktor wyrzuci wyjątek, co mogę z tym zrobić? Mogę to zalogować lub zignorować. To wszystko. Nie mogę go „naprawić”, ponieważFoo
obiekt już zniknął. Najlepszy przypadek, rejestruję wyjątek i kontynuuję tak, jakby nic się nie wydarzyło (lub nie zakończyłem programu). Czy naprawdę warto potencjalnie powodować niezdefiniowane zachowanie, rzucając z destruktora?źródło
std::ofstream
destruktor opróżnia się, a następnie zamyka plik. Podczas opróżniania może wystąpić błąd zapełnienia dysku, z którym można absolutnie zrobić coś pożytecznego: pokazać użytkownikowi okno dialogowe błędu informujące, że na dysku brakuje wolnego miejsca.Jest niebezpieczny, ale nie ma też sensu z punktu widzenia czytelności / zrozumiałości kodu.
W tej sytuacji musisz zapytać
Co powinno złapać wyjątek? Czy osoba dzwoniąca foo? A może foo sobie z tym poradzi? Dlaczego osoba wywołująca foo powinna dbać o jakiś obiekt wewnętrzny dla foo? Język może definiować to w sposób sensowny, ale będzie nieczytelny i trudny do zrozumienia.
Co ważniejsze, gdzie idzie pamięć dla Object? Gdzie idzie pamięć posiadanego obiektu? Czy nadal jest przydzielany (rzekomo z powodu awarii destruktora)? Weź pod uwagę również, że obiekt znajdował się w przestrzeni stosu , więc oczywiście nie ma go.
Rozważ ten przypadek
Kiedy usuwanie obj3 kończy się niepowodzeniem, jak mogę faktycznie usunąć w sposób gwarantujący, że nie zakończy się niepowodzeniem? Cholera, mam pamięć!
Rozważmy teraz w pierwszym kodzie fragment obiektu Obiekt odchodzi automatycznie, ponieważ znajduje się na stosie, podczas gdy Obiekt3 znajduje się na stercie. Ponieważ wskaźnik do Object3 zniknął, jesteś jakby SOL. Masz wyciek pamięci.
Teraz jednym z bezpiecznych sposobów na zrobienie tego jest:
Zobacz także FAQ
źródło
int foo()
możesz użyć bloku try-try do zawinięcia całej funkcji foo w blok try-catch, w tym łapanie destruktorów, jeśli masz na to ochotę. Nadal nie jest to preferowane podejście, ale tak jest.Z projektu ISO dla C ++ (ISO / IEC JTC 1 / SC 22 N 4411)
Więc destruktory powinny generalnie wychwytywać wyjątki i nie pozwolić im rozmnażać się poza destruktorem.
źródło
Twój destruktor może działać w łańcuchu innych niszczycieli. Zgłoszenie wyjątku, który nie zostanie złapany przez twojego bezpośredniego rozmówcę, może pozostawić wiele obiektów w niespójnym stanie, powodując tym samym jeszcze więcej problemów niż ignorowanie błędu w operacji czyszczenia.
źródło
Należę do grupy, która uważa, że wzór „osłony ochronnej” rzucany w destruktor jest użyteczny w wielu sytuacjach - szczególnie w testach jednostkowych. Należy jednak pamiętać, że w C ++ 11 rzucenie niszczyciela powoduje wywołanie,
std::terminate
ponieważ niszczyciele są domyślnie opatrzone adnotacjaminoexcept
.Andrzej Krzemieński ma świetny post na temat niszczycieli, które rzucają:
Wskazuje, że C ++ 11 ma mechanizm zastępujący domyślny
noexcept
dla destruktorów:Wreszcie, jeśli zdecydujesz się wrzucić destruktor, powinieneś zawsze zdawać sobie sprawę z ryzyka podwójnego wyjątku (rzucanie, gdy stos jest odwijany z powodu wyjątku). To spowodowałoby połączenie
std::terminate
i rzadko jest to, czego chcesz. Aby uniknąć tego zachowania, możesz po prostu sprawdzić, czy istnieje już wyjątek przed rzuceniem nowego za pomocąstd::uncaught_exception()
.źródło
Wszyscy inni wyjaśnili, dlaczego rzucanie niszczycielami jest straszne ... co możesz z tym zrobić? Jeśli wykonujesz operację, która może się nie powieść, utwórz oddzielną metodę publiczną, która wykonuje czyszczenie i może generować dowolne wyjątki. W większości przypadków użytkownicy to zignorują. Jeśli użytkownicy chcą monitorować powodzenie / niepowodzenie czyszczenia, mogą po prostu wywołać jawną procedurę czyszczenia.
Na przykład:
źródło
Jako uzupełnienie głównych odpowiedzi, które są dobre, wyczerpujące i dokładne, chciałbym skomentować artykuł, do którego się odwołujesz - ten, który mówi, że „rzucanie wyjątków w destruktorach nie jest takie złe”.
Artykuł brzmi „jakie są alternatywy dla zgłaszania wyjątków” i wymienia niektóre problemy z każdą z nich. Po dokonaniu tego stwierdza, że ponieważ nie możemy znaleźć bezproblemowej alternatywy, powinniśmy nadal zgłaszać wyjątki.
Problem polega na tym, że żaden z wymienionych przez niego problemów z alternatywami nie jest tak zły jak zachowanie wyjątkowe, które, pamiętajmy, jest „niezdefiniowanym zachowaniem twojego programu”. Niektóre zarzuty autora obejmują „brzydkość estetyczną” i „zachęcanie do złego stylu”. Teraz, co wolisz? Program o złym stylu, czy taki, który wykazywał niezdefiniowane zachowanie?
źródło
Odp .: Istnieje kilka opcji:
Niech wyjątki wypłyną z twojego destruktora, niezależnie od tego, co dzieje się gdzie indziej. Robiąc to, bądź świadomy (lub nawet przerażony), że może nastąpić std :: terminate.
Nigdy nie pozwól, aby wyjątek wypłynął z twojego destruktora. Może być napisany do dziennika, jakiś duży czerwony zły tekst, jeśli możesz.
moja ulubiona : Jeśli
std::uncaught_exception
zwróci fałsz, pozwól, aby wypłynęły wyjątki. Jeśli zwróci wartość true, wróć do metody rejestrowania.Ale czy warto wrzucać d'tory?
Zgadzam się z większością powyższych stwierdzeń, że rzucania najlepiej unikać w destruktorze, tam gdzie to możliwe. Ale czasem najlepiej jest zaakceptować, że to się może zdarzyć i dobrze sobie z tym poradzić. Wybrałbym 3 powyżej.
Istnieje kilka dziwnych przypadków, w których naprawdę dobrym pomysłem jest rzucenie z destruktora. Jak kod błędu „trzeba sprawdzić”. Jest to typ wartości zwracany z funkcji. Jeśli osoba dzwoniąca odczyta / sprawdzi zawarty kod błędu, zwrócona wartość zostanie zniszczona po cichu. Ale jeśli zwrócony kod błędu nie zostanie odczytany do czasu, gdy zwracane wartości wykroczą poza zakres, spowoduje to wyjątek od swojego destruktora .
źródło
Obecnie postępuję zgodnie z polityką (o której mówi wielu), że klasy nie powinny aktywnie rzucać wyjątków od swoich destruktorów, ale zamiast tego powinny udostępnić publiczną „bliską” metodę wykonania operacji, która może się nie powieść ...
... ale wierzę, że destruktory dla klas typu kontenerowego, takich jak wektor, nie powinny maskować wyjątków wyrzucanych z klas, które zawierają. W tym przypadku faktycznie używam metody „free / close”, która wywołuje się rekurencyjnie. Tak, powiedziałem rekurencyjnie. Istnieje metoda na to szaleństwo. Propagacja wyjątków opiera się na stosie: jeśli wystąpi pojedynczy wyjątek, wówczas oba pozostałe destruktory będą nadal działać, a oczekujący wyjątek będzie propagowany, gdy procedura powróci, co jest świetne. Jeśli wystąpi wiele wyjątków, wówczas (w zależności od kompilatora) albo pierwszy wyjątek zostanie rozpropagowany, albo program zakończy się, co jest w porządku. Jeśli zdarzy się tak wiele wyjątków, że rekursja przepełnia stos, wówczas coś jest naprawdę nie tak i ktoś się o tym dowie, co również jest w porządku. Osobiście,
Chodzi o to, że kontener pozostaje neutralny i to od zawartych w nim klas zależy, czy zachowają się, czy zachowają się niewłaściwie, jeśli chodzi o rzucanie wyjątków od swoich destruktorów.
źródło
W przeciwieństwie do konstruktorów, w których zgłaszanie wyjątków może być użytecznym sposobem na wskazanie, że tworzenie obiektu się powiodło, wyjątków nie należy rzucać w destruktory.
Problem występuje, gdy podczas niszczenia stosu zostanie wyrzucony wyjątek z destruktora. Jeśli tak się stanie, kompilator znajdzie się w sytuacji, w której nie będzie wiedział, czy kontynuować proces rozwijania stosu, czy obsłużyć nowy wyjątek. W rezultacie twój program zostanie natychmiast zakończony.
Dlatego najlepszym rozwiązaniem jest powstrzymanie się od stosowania wyjątków w destruktorach. Zamiast tego napisz wiadomość do pliku dziennika.
źródło
Martin Ba (powyżej) jest na dobrej drodze - inaczej budujesz architekturę dla logiki RELEASE i COMMIT.
Do wypuszczenia:
Powinieneś jeść wszelkie błędy. Zwalniasz pamięć, zamykasz połączenia itp. Nikt inny w systemie nigdy nie powinien ZOBACZYĆ tych rzeczy i przekazujesz zasoby systemowi operacyjnemu. Jeśli wygląda na to, że potrzebujesz tutaj prawdziwej obsługi błędów, jest to prawdopodobnie konsekwencja wad projektowych w twoim modelu obiektowym.
Do zatwierdzenia:
W tym miejscu chcesz tego samego rodzaju obiektów otoki RAII, które takie jak std :: lock_guard zapewniają muteksy. Z tymi nie umieszczasz logiki zatwierdzania w Dtor AT ALL. Masz do tego dedykowany interfejs API, a następnie zawijaj obiekty, które RAII zatwierdzą go w swoich IRA i obsłużą tam występujące błędy. Pamiętaj, że możesz złapać wyjątki w destruktorze w porządku; wydaje je śmiertelnie niebezpieczne. Pozwala to również wdrożyć zasady i różne sposoby obsługi błędów po prostu przez zbudowanie innego opakowania (np. Std :: unique_lock vs. std :: lock_guard) i zapewnia, że nie zapomnisz wywołać logiki zatwierdzania - która jest jedyną połową przyzwoite uzasadnienie umieszczenia go w dtor na 1. miejscu.
źródło
Główny problem jest następujący: nie można zawieść . Co w końcu znaczy nie zawieść? Jeśli transakcja w bazie danych nie powiedzie się i nie powiedzie się (nie nastąpi przywrócenie), co stanie się z integralnością naszych danych?
Ponieważ destruktory są wywoływane zarówno dla normalnych, jak i wyjątkowych (nieudanych) ścieżek, one same nie mogą zawieść, w przeciwnym razie my „zawiedziemy”.
Jest to problem trudny koncepcyjnie, ale często rozwiązaniem jest po prostu znalezienie sposobu, aby upewnić się, że niepowodzenie nie może zawieść. Na przykład baza danych może zapisać zmiany przed zatwierdzeniem zewnętrznej struktury danych lub pliku. Jeśli transakcja się nie powiedzie, strukturę plików / danych można odrzucić. Wszystko, co musi wtedy zapewnić, to zatwierdzenie zmian z tej zewnętrznej struktury / pliku transakcji atomowej, która nie może zawieść.
Najodpowiedniejszym rozwiązaniem dla mnie jest zapisanie logiki nieoczyszczania w taki sposób, aby logika czyszczenia nie mogła zawieść. Na przykład, jeśli masz ochotę utworzyć nową strukturę danych w celu oczyszczenia istniejącej struktury danych, być może możesz spróbować utworzyć tę strukturę pomocniczą z wyprzedzeniem, abyśmy nie musieli już tworzyć jej wewnątrz destruktora.
Trzeba przyznać, że o wiele łatwiej to powiedzieć niż zrobić, ale jest to jedyny naprawdę właściwy sposób, aby to zrobić. Czasami myślę, że powinna istnieć możliwość napisania osobnej logiki destruktora dla normalnych ścieżek wykonania z dala od wyjątkowych, ponieważ czasami destruktory czują się trochę tak, jakby miały podwójną odpowiedzialność, próbując poradzić sobie z obiema (przykładem są osłony zakresów, które wymagają wyraźnego zwolnienia ; nie wymagaliby tego, gdyby mogli odróżnić ścieżki wyjątkowych zniszczeń od ścieżek innych niż wyjątkowe).
Nadal najważniejszym problemem jest to, że nie możemy zawieść, i jest to trudny problem z projektem koncepcyjnym, który idealnie rozwiązuje się we wszystkich przypadkach. Staje się łatwiejsze, jeśli nie zostaniesz zbyt pochłonięty złożonymi strukturami kontrolnymi z mnóstwem małych obiektów oddziałujących ze sobą, a zamiast tego modelujesz swoje projekty w nieco bardziej masywny sposób (przykład: układ cząstek z niszczycielem, aby zniszczyć całą cząsteczkę układ, a nie oddzielny nietrywialny destruktor na cząsteczkę). Kiedy modelujesz swoje projekty na tym grubszym poziomie, masz do czynienia z mniej trywialnymi niszczycielami, z którymi możesz sobie poradzić, i często możesz sobie pozwolić na wszystko, co jest wymagane narzut pamięci / przetwarzania, aby upewnić się, że niszczyciele nie zawiodą.
I to jest jedno z najłatwiejszych rozwiązań, oczywiście, rzadziej używać niszczycieli. W powyższym przykładzie cząstek, być może po zniszczeniu / usunięciu cząstki, należy zrobić pewne rzeczy, które mogą zawieść z jakiegokolwiek powodu. W takim przypadku zamiast wywoływać taką logikę przez dtor cząstki, która mogłaby zostać wykonana na wyjątkowej ścieżce, można zamiast tego zrobić to wszystko przez układ cząstek, gdy usuwa cząstkę. Usunięcie cząstki zawsze może odbywać się na nietypowej ścieżce. Jeśli system zostanie zniszczony, być może może po prostu oczyścić wszystkie cząstki i nie zawracać sobie głowy logiką usuwania pojedynczych cząstek, która może zawieść, podczas gdy logika, która może zawieść, jest wykonywana tylko podczas normalnego działania układu cząstek, gdy usuwa jedną lub więcej cząstek.
Często istnieją takie rozwiązania, które pojawiają się, jeśli unikniesz zajmowania się wieloma małymi obiektami z nietrywialnymi niszczycielami. Kiedy możesz zaplątać się w bałagan, w którym wydaje się prawie niemożliwe, aby być wyjątkowym, bezpieczeństwo jest wtedy, gdy zaplątasz się w wiele małych obiektów, z których wszystkie mają nietrywialne cechy.
Bardzo by to pomogło, gdyby nothrow / noexcept faktycznie zostało przetłumaczone na błąd kompilatora, jeśli cokolwiek, co go określa (w tym funkcje wirtualne, które powinny odziedziczyć specyfikację noexcept klasy bazowej), próbowałoby wywołać wszystko, co mogłoby wyrzucić. W ten sposób moglibyśmy uchwycić wszystkie te rzeczy w czasie kompilacji, gdybyśmy faktycznie przypadkowo napisali destruktor, który mógłby rzucić.
źródło
Ustaw zdarzenie alarmowe. Zazwyczaj zdarzenia alarmowe są lepszą formą powiadamiania o awarii podczas czyszczenia obiektów
źródło