27.4 -- Nieprzechwycone wyjątki i procedury obsługi catch-all

Do tej pory powinieneś mieć rozsądne pojęcie o tym, jak działają wyjątki. W tej lekcji omówimy kilka bardziej interesujących przypadków wyjątków.

Nieprzechwycone wyjątki

Kiedy funkcja zgłasza wyjątek, którego sama nie obsługuje, zakłada, że ​​funkcja znajdująca się gdzieś niżej na stosie wywołań obsłuży wyjątek. W poniższym przykładzie funkcja mySqrt() zakłada, że ​​ktoś obsłuży zgłoszony przez nią wyjątek — ale co się stanie, jeśli tak naprawdę nikt tego nie zrobi?

Oto ponownie nasz program pierwiastkowy, bez bloku try w funkcji main():

#include <iostream>
#include <cmath> // for sqrt() function

// A modular square root function
double mySqrt(double x)
{
    // If the user entered a negative number, this is an error condition
    if (x < 0.0)
        throw "Can not take sqrt of negative number"; // throw exception of type const char*

    return std::sqrt(x);
}

int main()
{
    std::cout << "Enter a number: ";
    double x;
    std::cin >> x;

    // Look ma, no exception handler!
    std::cout << "The sqrt of " << x << " is " << mySqrt(x) << '\n';

    return 0;
}

Załóżmy teraz, że użytkownik wprowadzi -4, a mySqrt(-4) zgłosi wyjątek. Funkcja mySqrt() nie obsługuje wyjątku, więc program sprawdza, czy jakaś funkcja znajdująca się na dole stosu wywołań obsłuży wyjątek. main() również nie ma procedury obsługi tego wyjątku, więc nie można znaleźć żadnej procedury obsługi.

Gdy nie można znaleźć procedury obsługi wyjątku dla funkcji, wywoływana jest std::terminate() i aplikacja kończy się. W takich przypadkach stos wywołań może zostać rozwinięty lub nie! Jeśli stos nie zostanie odwinięty, zmienne lokalne nie zostaną zniszczone i nie nastąpi żadne czyszczenie oczekiwane po zniszczeniu tych zmiennych!

Ostrzeżenie

Stos wywołań może zostać odwinięty, jeśli wyjątek nie zostanie obsłużony.

Jeśli stos nie zostanie odwinięty, zmienne lokalne nie zostaną zniszczone, co może powodować problemy, jeśli zmienne te mają nietrywialne destruktory.

Na marginesie…

Chociaż mogłoby się to wydawać dziwne, że w takim przypadku nie rozwija się stosu, istnieje dobry powód, aby tego nie robić. Nieobsługiwany wyjątek jest zazwyczaj czymś, czego należy za wszelką cenę uniknąć. Jeśli stos zostałby rozwinięty, wszystkie informacje debugowania dotyczące stanu stosu, które doprowadziły do ​​zgłoszenia nieobsługiwanego wyjątku, zostałyby utracone! Nie rozwijając, zachowujemy te informacje, co ułatwia ustalenie, w jaki sposób został zgłoszony nieobsługiwany wyjątek, i naprawienie go.

Gdy wyjątek nie zostanie obsłużony, system operacyjny zazwyczaj powiadomi Cię, że wystąpił błąd nieobsłużonego wyjątku. Sposób, w jaki to robi, zależy od systemu operacyjnego, ale możliwe są następujące możliwości: wydrukowanie komunikatu o błędzie, wyświetlenie okna dialogowego o błędzie lub po prostu awaria. Niektóre systemy operacyjne są mniej eleganckie niż inne. Generalnie jest to coś, czego należy unikać!

Procedury obsługi catch-all

A teraz znaleźliśmy się w zagadce:

  • Funkcje mogą potencjalnie generować wyjątki dowolnego typu danych (w tym typów danych zdefiniowanych przez program), co oznacza, że istnieje nieskończona liczba możliwych typów wyjątków do przechwycenia.
  • Jeśli wyjątek nie zostanie przechwycony, program zakończy się natychmiast (a stos może nie zostać rozwinął się, więc Twój program może nawet nie posprzątać poprawnie).
  • Dodawanie jawnych procedur obsługi catch dla każdego możliwego typu jest uciążliwe, szczególnie w przypadku tych, które mają być osiągane tylko w wyjątkowych przypadkach!

Na szczęście C++ zapewnia nam również mechanizm wychwytywania wszystkich typów wyjątków. Nazywa się to procedurą obsługi catch-all. Procedura obsługi catch-all działa podobnie jak zwykły blok catch, z tą różnicą, że zamiast używać określonego typu do przechwytywania, używa operatora wielokropka (…) jako typu do przechwytywania. Z tego powodu procedura obsługi catch-all jest czasami nazywana „operacją obsługi przechwytywania wielokropka”

Jeśli pamiętasz z lekcji 20.5 -- Elipsa (i dlaczego jej unikać), elipsy były wcześniej używane do przekazywania argumentów dowolnego typu do funkcji. W tym kontekście reprezentują wyjątki dowolnego typu danych. Oto prosty przykład:

#include <iostream>

int main()
{
	try
	{
		throw 5; // throw an int exception
	}
	catch (double x)
	{
		std::cout << "We caught an exception of type double: " << x << '\n';
	}
	catch (...) // catch-all handler
	{
		std::cout << "We caught an exception of an undetermined type\n";
	}
}

Ponieważ nie ma określonej procedury obsługi wyjątku dla typu int, procedura obsługi catch-all przechwytuje ten wyjątek. Ten przykład daje następujący wynik:

We caught an exception of an undetermined type

Procedurę obsługi catch-all należy umieścić jako ostatnią w łańcuchu bloków catch. Ma to na celu zapewnienie, że wyjątki mogą zostać przechwycone przez procedury obsługi wyjątków dostosowane do określonych typów danych, jeśli takie procedury obsługi istnieją.

Często blok procedury obsługi catch-all pozostaje pusty:

catch(...) {} // ignore any unanticipated exceptions

To wyłapie wszelkie nieprzewidziane wyjątki, zapewniając, że odwinięcie stosu nastąpi do tego momentu i zapobiegnie zakończeniu programu, ale nie powoduje żadnej specyficznej obsługi błędów.

Używanie procedury obsługi catch-all do zawijania main()

Jednym z zastosowań procedury obsługi catch-all jest zawinięcie zawartości funkcji main():

#include <iostream>

struct GameSession
{
    // Game session data here
};

void runGame(GameSession&)
{
    throw 1;
}

void saveGame(GameSession&)
{
    // Zapisz user's game here
}

int main()
{
    GameSession session{};

    try
    {
        runGame(session);
    }
    catch(...)
    {
        std::cerr << "Abnormal termination\n";
    }

    saveGame(session); // save the user's game (even if catch-all handler was hit)

    return 0;
}

W tym przypadku, jeśli runGame() lub dowolna z wywoływanych przez nią funkcji zgłosi wyjątek, który nie jest obsługiwany, zostanie on przechwycony przez tę procedurę obsługi catch-all. Stos będzie rozwijany w uporządkowany sposób (zapewniający zniszczenie zmiennych lokalnych). Zapobiegnie to również natychmiastowemu zakończeniu programu, dając nam szansę wydrukowania wybranego przez nas błędu i zapisania stanu użytkownika przed wyjściem.

Wskazówka

Jeśli Twój program używa wyjątków, rozważ użycie procedury obsługi catch-all w głównej części, aby zapewnić uporządkowane zachowanie w przypadku wystąpienia nieobsłużonego wyjątku.

Jeśli procedura obsługi catch-all przechwyci wyjątek, powinieneś założyć, że program znajduje się teraz w jakimś nieokreślonym stanie, natychmiast wykonaj czyszczenie, a następnie zakończyć.

Debugowanie nieobsługiwanych wyjątków

Nieobsługiwane wyjątki wskazują, że wydarzyło się coś nieoczekiwanego i prawdopodobnie chcemy w pierwszej kolejności zdiagnozować, dlaczego został zgłoszony nieobsługiwany wyjątek. Wiele debugerów będzie (lub można je tak skonfigurować) przerywać w przypadku nieobsługiwanych wyjątków, co pozwala nam wyświetlić stos w miejscu, w którym został zgłoszony nieobsługiwany wyjątek. Jeśli jednak mamy procedurę obsługi catch-all, wówczas obsługiwane są wszystkie wyjątki i (ponieważ stos jest rozwijany) tracimy przydatne informacje diagnostyczne.

Dlatego w kompilacjach debugowania przydatne może być wyłączenie procedury obsługi catch-all. Możemy to zrobić za pomocą dyrektyw kompilacji warunkowej.

Oto jeden ze sposobów, aby to zrobić:

#include <iostream>

struct GameSession
{
    // Game session data here
};

void runGame(GameSession&)
{
    throw 1;
}

void saveGame(GameSession&)
{
    // Zapisz user's game here
}

class DummyException // a dummy class that can't be instantiated
{
    DummyException() = delete;
}; 

int main()
{
    GameSession session {}; 

    try
    {
        runGame(session);
    }
#ifndef NDEBUG // if we're in release node
    catch(...) // compile in the catch-all handler
    {
        std::cerr << "Abnormal termination\n";
    }
#else // in debug mode, compile in a catch that will never be hit (for syntactic reasons)
    catch(DummyException)
    {
    }
#endif

    saveGame(session); // save the user's game (even if catch-all handler was hit)

    return 0;
}

Składniowo, blok try wymaga co najmniej jednego powiązanego bloku catch. Jeśli więc procedura obsługi catch-all jest kompilowana warunkowo, musimy albo warunkowo skompilować blok try, albo musimy warunkowo skompilować w innym bloku catch. Bardziej przejrzyście jest zrobić to drugie.

W tym celu tworzymy klasę DummyException której instancji nie można utworzyć, ponieważ ma usunięty konstruktor domyślny i nie ma innych konstruktorów. Kiedy zdefiniowano NDEBUG , kompilujemy procedurę obsługi catch, aby przechwycić wyjątek typu DummyException. Ponieważ nie możemy utworzyć DummyException, ten moduł obsługi catch nigdy niczego nie złapie. Dlatego wszelkie wyjątki, które dotrą do tego punktu, nie będą obsługiwane.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
108 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze