wyrzucanie wyjątków z destruktora

257

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.

Greg Rogers
źródło
36
„Dwa wyjątki naraz” to podstawowa odpowiedź, ale nie jest PRAWDZIWYM powodem. Prawdziwym powodem jest to, że wyjątek powinien zostać zgłoszony wtedy i tylko wtedy, gdy nie można spełnić warunków dodatkowych funkcji. Warunkiem niszczyciela jest to, że obiekt już nie istnieje. To nie może się zdarzyć. Każda podatna na awarie operacja wycofania z eksploatacji musi zatem zostać wywołana jako osobna metoda, zanim obiekt znajdzie się poza zasięgiem (rozsądne funkcje i tak zwykle mają tylko jedną ścieżkę sukcesu).
spraff
29
@spraff: Czy wiesz, że to, co powiedziałeś, oznacza „wyrzucić RAII”?
Kos,
16
@spraff: konieczność wywołania „oddzielnej metody, zanim obiekt wykracza poza zakres” (jak napisałeś) faktycznie wyrzuca RAII! Kod korzystający z takich obiektów będzie musiał zapewnić, że taka metoda zostanie wywołana przed wywołaniem destruktora. Wreszcie, ten pomysł w ogóle nie pomaga.
Frunsi,
8
@Frunsi nie, ponieważ ten problem wynika z faktu, że destruktor próbuje zrobić coś poza zwykłym uwolnieniem zasobów. Kuszące jest powiedzenie „zawsze chcę skończyć z XYZ” i myślenie, że jest to argument za umieszczeniem takiej logiki w destruktorze. Nie, nie bądź leniwy, pisz xyz()i utrzymuj destruktor w czystości od logiki innej niż RAII.
spraff
6
@Frunsi Na przykład, zobowiązując coś pliku nie jest koniecznie OK, aby zrobić w destruktora klasy reprezentujące transakcję. Jeśli zatwierdzenie nie powiodło się, jest za późno, aby go obsłużyć, gdy cały kod zaangażowany w transakcję wyszedł poza zakres. Destruktor powinien odrzucić transakcję, chyba że commit()zostanie wywołana metoda.
Nicholas Wilson,

Odpowiedzi:

197

Wyrzucenie wyjątku z destruktora jest niebezpieczne.
Jeśli propaguje się inny wyjątek, aplikacja zostanie zakończona.

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

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

Martin York
źródło
5
„O ile nie masz nic przeciwko potencjalnemu zamknięciu aplikacji, prawdopodobnie powinieneś połknąć błąd.” - prawdopodobnie powinien to być raczej wyjątek (wybacz kalambur) niż reguła - to znaczy, że szybko zawodzić.
Erik Forbes,
15
Nie zgadzam się. Zakończenie programu zatrzymuje odwijanie stosu. Nie będzie już wywoływany destruktor. Wszelkie otwarte zasoby pozostaną otwarte. Myślę, że połknięcie wyjątku byłoby preferowaną opcją.
Martin York,
20
System operacyjny może wyczyścić zasoby, których jest właścicielem. Pamięć, uchwyty plików itp. Co ze złożonymi zasobami: połączenia DB. To łącze do ISS, który otworzyłeś (czy automatycznie wysyła bliskie połączenia)? Jestem pewien, że NASA chce, abyś zamknął połączenie w sposób czysty!
Martin York,
7
Jeśli aplikacja „szybko zawiedzie” poprzez przerwanie, to nie powinna generować wyjątków. Jeśli zawiedzie, przekazując kontrolę z powrotem do stosu, nie powinien tego robić w sposób, który może spowodować przerwanie programu. Tak czy inaczej, nie wybieraj obu.
Tom
2
@LokiAstari Protokół transportu, którego używasz do komunikacji ze statkiem kosmicznym, nie może obsłużyć zerwanego połączenia? Ok ...
doug65536
54

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.

Gal Goldman
źródło
1
czy możesz wyjaśnić, w jaki sposób wywołano abort () w powyższej sytuacji? Oznacza to, że kontrola wykonania była nadal kompilatorem C ++
Krishna Oza
1
@Krishna_Oza: Całkiem proste: za każdym razem, gdy zgłaszany jest błąd, kod, który generuje błąd, sprawdza trochę, co wskazuje, że system wykonawczy jest w trakcie rozpakowywania stosu (tj. Obsługuje inny, throwale jeszcze nie znalazł catchdla niego bloku) w takim przypadku std::terminate(nie abort) jest wywoływane zamiast zgłaszania (nowego) wyjątku (lub kontynuowania rozwijania stosu).
Marc van Leeuwen,
53

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:

  • (R) uwolnij semantykę (aka zwolnij tę pamięć)
  • (C) zatwierdzanie semantyki (aka opróżniania pliku na dysk)

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 UncaughtExceptionCounterdo swoich ScopeGuardnarzę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 , nie może być standardowym elementem do tego, patrz N3614 , a dyskusja o tym .

Aktualizacja '17: funkcja C ++ 17 std do tego jest std::uncaught_exceptionsafaikt. Szybko zacytuję artykuł CPpref:

Notatki

Przykładem intużycia -rendering uncaught_exceptionsjest ... ... najpierw tworzy obiekt wartownika i zapisuje liczbę nieprzechwyconych wyjątków w swoim konstruktorze. Dane wyjściowe są wykonywane przez destruktor obiektu wartownika, chyba że wyrzuci foo () ( w takim przypadku liczba nieprzechwyconych wyjątków w destruktorze jest większa niż zaobserwowana przez konstruktora )

Martin Ba
źródło
6
Bardzo się zgadzam. I dodając jeszcze jedną semantykę wycofywania semantycznego (Ro). Używany powszechnie w osłonie zakresu. Podobnie jak w przypadku mojego projektu, w którym zdefiniowałem makro ON_SCOPE_EXIT. W przypadku semantyki cofania jest to, że tutaj może się zdarzyć coś znaczącego. Więc naprawdę nie powinniśmy ignorować niepowodzenia.
Weipeng L,
Wydaje mi się, że jedynym powodem, dla którego popełniamy semantykę w destruktorach, jest to, że C ++ nie obsługuje finally.
user541686,
@Mehrdad: 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ć.
Martin Ba
1
@MartinBa: Myślę, że nie trafiłeś w sedno mojego komentarza, co jest zaskakujące, ponieważ zgadzam się z twoją opinią, że (R) i (C) są różne. Próbowałem powiedzieć, że dtor jest z natury narzędziem dla (R) i z finallynatury narzędziem dla (C). Jeśli nie rozumiesz, dlaczego: zastanów się, dlaczego uzasadnione jest umieszczanie wyjątków jeden na drugim w finallyblokach 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 zwalniania finallykontroli. Są różne; szkoda, że ​​C ++ je łączy).
541686
1
@Mehrdad: Zbyt długo tutaj. Jeśli chcesz, możesz tutaj zbudować argumenty: programmers.stackexchange.com/questions/304067/… . Dzięki.
Martin Ba,
21

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ę Fooobiekt, a Foodestruktor wyrzuci wyjątek, co mogę z tym zrobić? Mogę to zalogować lub zignorować. To wszystko. Nie mogę go „naprawić”, ponieważ Fooobiekt 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?

Derek Park
źródło
11
Właśnie zauważyłem ... rzucanie z dtor nigdy nie jest niezdefiniowanym zachowaniem. Jasne, może wywoływać terminate (), ale jest to bardzo dobrze określone zachowanie.
Martin Ba,
4
std::ofstreamdestruktor 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.
Andy,
13

Jest niebezpieczny, ale nie ma też sensu z punktu widzenia czytelności / zrozumiałości kodu.

W tej sytuacji musisz zapytać

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

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

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

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:

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

Zobacz także FAQ

Doug T.
źródło
Wskrzeszając tę ​​odpowiedź, re: pierwszy przykład, 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.
tyree731
13

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.

3 Proces wywoływania destruktorów dla automatycznych obiektów zbudowanych na ścieżce od bloku try do wyrażenia rzut jest nazywany „odwijaniem stosu”. [Uwaga: Jeśli wywołanie destruktora podczas odwijania stosu kończy się z wyjątkiem, wywoływane jest std :: terminate (15.5.1). Więc destruktory powinny generalnie wychwytywać wyjątki i nie pozwolić im rozmnażać się poza destruktorem. - uwaga końcowa]

lothar
źródło
1
Nie odpowiedział na pytanie - PO już o tym wie.
Arafangion
2
@Arafangion Wątpię, aby był tego świadomy (wywołanie std :: terminate), ponieważ zaakceptowana odpowiedź zawiera dokładnie ten sam punkt.
lothar
@Arafangion, jak w niektórych odpowiedziach tutaj, niektórzy wspominali, że wywoływano abort (); A może po kolei std :: terminate wywołuje funkcję abort ().
Krishna Oza
7

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.

Franci Penov
źródło
7

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::terminateponieważ niszczyciele są domyślnie opatrzone adnotacjami noexcept.

Andrzej Krzemieński ma świetny post na temat niszczycieli, które rzucają:

Wskazuje, że C ++ 11 ma mechanizm zastępujący domyślny noexceptdla destruktorów:

W C ++ 11, destruktor jest domyślnie określony jako noexcept. Nawet jeśli nie dodasz specyfikacji i zdefiniujesz destruktor w ten sposób:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

Kompilator nadal niewidocznie doda specyfikację noexceptdo destruktora. A to oznacza, że ​​moment, w którym Twój destruktor zgłasza wyjątek, std::terminatezostanie wywołany, nawet jeśli nie wystąpi sytuacja podwójnego wyjątku. Jeśli jesteś naprawdę zdeterminowany, aby zezwolić swoim niszczycielom na rzucanie, będziesz musiał to wyraźnie określić; masz trzy opcje:

  • Jawnie określ swój destruktor jako noexcept(false),
  • Dziedzicz klasę od innej, która już określa swój destruktor jako noexcept(false).
  • Umieść w swojej klasie niestatyczny element danych, który już określa swój destruktor jako noexcept(false).

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::terminatei 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().

Gaspard
źródło
6

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:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};
Tomek
źródło
Szukam rozwiązania, ale starają się wyjaśnić, co się stało i dlaczego. Chcesz tylko wyjaśnić, czy funkcja destruktora jest wywoływana wewnątrz destruktora?
Jason Liu,
5

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?

DJClayworth
źródło
1
Nie niezdefiniowane zachowanie, ale raczej natychmiastowe zakończenie.
Marc van Leeuwen,
Norma mówi „niezdefiniowane zachowanie”. Takie zachowanie często kończy się, ale nie zawsze.
DJClayworth,
Nie, przeczytaj [oprócz. Termin] w rozdziale Obsługa wyjątków-> Funkcje specjalne (co w mojej kopii standardu to 15.5.1, ale jego numeracja jest prawdopodobnie nieaktualna).
Marc van Leeuwen,
2

P: Więc moje pytanie brzmi - jeśli wyrzucenie z destruktora skutkuje niezdefiniowanym zachowaniem, w jaki sposób radzisz sobie z błędami występującymi podczas niszczenia?

Odp .: Istnieje kilka opcji:

  1. 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.

  2. 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.

  3. moja ulubiona : Jeśli std::uncaught_exceptionzwró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 .

MartinP
źródło
4
Twoja ulubiona rzecz to coś, czego ostatnio próbowałem i okazuje się, że nie powinieneś tego robić. gotw.ca/gotw/047.htm
GManNickG
1

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.

Mateusz
źródło
1

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.

Devesh Agrawal
źródło
1
Zapisanie wiadomości do pliku dziennika może spowodować wyjątek.
Konard
1

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.

użytkownik3726672
źródło
1

Moje pytanie brzmi zatem: jeśli wyrzucenie z destruktora powoduje niezdefiniowane zachowanie, jak radzisz sobie z błędami występującymi podczas niszczenia?

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ść.

Pragmatycznym rozwiązaniem może być po prostu upewnienie się, że szanse niepowodzenia w przypadku awarii są astronomicznie nieprawdopodobne, ponieważ uniemożliwienie porażki może być w niektórych przypadkach prawie niemożliwe.

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ć.

Dragon Energy
źródło
1
Zniszczenie jest teraz porażką?
ciekawy,
Myślę, że ma na myśli, że podczas awarii wywoływane są destruktory, aby je usunąć. Jeśli więc wywoływany jest destruktor podczas aktywnego wyjątku, wówczas nie można go wyczyścić po poprzednim niepowodzeniu.
user2445507
0

Ustaw zdarzenie alarmowe. Zazwyczaj zdarzenia alarmowe są lepszą formą powiadamiania o awarii podczas czyszczenia obiektów

MRN
źródło