W poprzedniej lekcji na temat 27.2 — Podstawowa obsługa wyjątków, wyjaśniliśmy, jak rzutowanie, próba i złapanie współdziałają, aby włączyć wyjątek obsługa. W tej lekcji omówimy interakcję obsługi wyjątków z funkcjami.
Zgłaszanie wyjątków z wywoływanej funkcji
W poprzedniej lekcji zauważyliśmy, że „blok try wykrywa wszelkie wyjątki zgłaszane przez instrukcje w bloku try”. W odpowiednich przykładach nasze instrukcje rzutu zostały umieszczone w bloku try i przechwycone przez powiązany blok catch, a wszystko to w ramach tej samej funkcji. Konieczność zgłaszania i łapania wyjątków w ramach pojedynczej funkcji ma ograniczoną wartość.
Bardziej interesująca jest sytuacja, gdy instrukcja wewnątrz bloku try jest wywołaniem funkcji, a wywoływana funkcja zgłasza wyjątek. Czy blok try wykryje wyjątek zgłoszony przez funkcję wywoływaną z bloku try?
Odpowiedź na szczęście brzmi: tak!
Jedną z najbardziej przydatnych właściwości obsługi wyjątków jest to, że instrukcje rzutu NIE muszą być umieszczane bezpośrednio w bloku try. Zamiast tego wyjątki mogą być zgłaszane z dowolnego miejsca funkcji, a wyjątki te mogą zostać przechwycone przez blok try obiektu wywołującego (lub obiektu wywołującego itp.). Gdy wyjątek zostanie złapany w ten sposób, wykonanie przeskakuje z miejsca, w którym wyjątek został zgłoszony, do bloku catch obsługującego wyjątek.
Kluczowa informacja
Bloki Try wychwytują wyjątki nie tylko z instrukcji znajdujących się w bloku try, ale także z funkcji wywoływanych w bloku try.
Pozwala to nam na użycie obsługi wyjątków w znacznie bardziej modułowy sposób. Zademonstrujemy to, przepisując program pierwiastkowy z poprzedniej lekcji, aby użyć funkcji modułowej.
#include <cmath> // for sqrt() function
#include <iostream>
// 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;
try // Look for exceptions that occur within try block and route to attached catch block(s)
{
double d = mySqrt(x);
std::cout << "The sqrt of " << x << " is " << d << '\n';
}
catch (const char* exception) // catch exceptions of type const char*
{
std::cerr << "Error: " << exception << std::endl;
}
return 0;
}W tym programie wzięliśmy kod, który sprawdza wyjątek, oblicza pierwiastek kwadratowy i umieszczamy go w funkcji modułowej o nazwie mySqrt(). Następnie wywołaliśmy tę funkcję mySqrt() z poziomu bloku try. Sprawdźmy, czy nadal działa zgodnie z oczekiwaniami:
Enter a number: -4 Error: Can not take sqrt of negative number
Działa! Gdy w mySqrt() zostanie zgłoszony wyjątek, w mySqrt() nie ma procedury obsługi, która mogłaby obsłużyć wyjątek. Jednakże wywołanie funkcji mySqrt() (w funkcji main()) znajduje się w bloku try, z którym jest powiązany pasujący moduł obsługi wyjątków. Dlatego wykonanie przeskakuje z instrukcji rzutu w mySqrt() na początek bloku catch w funkcji main(), a następnie wznawia.
Najbardziej interesującą częścią powyższego programu jest to, że funkcja mySqrt() może zgłosić wyjątek, ale sama go nie obsługuje! Zasadniczo oznacza to, że mySqrt() jest skłonna powiedzieć: „Hej, jest problem!”, ale nie chce sama zająć się problemem. Zasadniczo jest to delegowanie odpowiedzialności za obsługę wyjątku na osobę wywołującą (odpowiednik tego, w jaki sposób użycie kodu powrotu przekazuje odpowiedzialność za obsługę błędu z powrotem na osobę wywołującą funkcję).
W tym momencie niektórzy z Was prawdopodobnie zastanawiają się, dlaczego dobrym pomysłem jest przekazywanie błędów z powrotem do osoby wywołującej. Dlaczego po prostu nie sprawić, by MySqrt() obsługiwała własny błąd? Problem polega na tym, że różne aplikacje mogą chcieć obsługiwać błędy na różne sposoby. Aplikacja konsolowa może chcieć wydrukować wiadomość tekstową. Aplikacja systemu Windows może chcieć wyświetlić okno dialogowe błędu. W jednej aplikacji może to być błąd krytyczny, a w innej aplikacji może tak nie być. Przekazując błąd z funkcji, każda aplikacja może obsłużyć błąd mySqrt() w sposób, który jest dla niej najbardziej odpowiedni w kontekście! Ostatecznie sprawia to, że funkcja mySqrt() jest możliwie modułowa, a obsługę błędów można umieścić w mniej modułowych częściach kodu.
Obsługa wyjątków i rozwijanie stosu
W tej sekcji przyjrzymy się, jak faktycznie działa obsługa wyjątków, gdy zaangażowanych jest wiele funkcji.
Powiązana treść
Przed kontynuowaniem zobacz lekcję 20.2 — Stos i sterta jeśli potrzebujesz odświeżenia stosów wywołań i rozwinięcia stosu.
Gdy zostanie zgłoszony wyjątek, program najpierw sprawdza, czy wyjątek można natychmiast obsłużyć wewnątrz bieżącą funkcję (co oznacza, że wyjątek został zgłoszony w bloku try wewnątrz bieżącej funkcji i powiązany jest z nim odpowiedni blok catch). Jeśli bieżąca funkcja może obsłużyć wyjątek, robi to.
Jeśli nie, program następnie sprawdza, czy wywołujący funkcję (następna funkcja na stosie wywołań) może obsłużyć wyjątek. Aby obiekt wywołujący funkcję mógł obsłużyć wyjątek, wywołanie bieżącej funkcji musi znajdować się wewnątrz bloku try i musi być powiązany pasujący blok catch. Jeśli nie zostanie znalezione dopasowanie, sprawdzany jest obiekt wywołujący wywołujący (dwie funkcje w górę stosu wywołań). Podobnie, aby obiekt wywołujący mógł obsłużyć wyjątek, wywołanie wywołującego musi znajdować się wewnątrz bloku try i musi być powiązany pasujący blok catch.
Proces sprawdzania każdej funkcji w górę stosu wywołań trwa do momentu znalezienia procedury obsługi lub sprawdzenia wszystkich funkcji na stosie wywołań i nie można znaleźć żadnej procedury obsługi.
Jeśli zostanie znaleziona pasująca procedura obsługi wyjątku, wykonanie przeskakuje od punktu, w którym wyjątek zostanie zgłoszony do górę pasującego bloku catch. Wymaga to rozwinięcia stosu (usunięcia bieżącej funkcji ze stosu wywołań) tyle razy, ile jest to konieczne, aby funkcja obsługująca wyjątek znalazła się na szczycie stosu wywołań.
Jeśli nie zostanie znaleziona pasująca procedura obsługi wyjątków, stos może zostać rozwinięty lub nie. Porozmawiamy więcej o tym przypadku na następnej lekcji (27.4 -- Nieprzechwycone wyjątki i procedury obsługi catch-all).
Gdy bieżąca funkcja zostanie usunięta ze stosu wywołań, wszystkie zmienne lokalne są jak zwykle niszczone, ale nie jest zwracana żadna wartość.
Kluczowa informacja
Odwijanie stosu niszczy zmienne lokalne w rozwijanych funkcjach (co jest dobre, ponieważ zapewnia ich destruktory wykonaj).
Kolejny przykład rozwijania stosu
Aby zilustrować powyższe, spójrzmy na bardziej złożony przykład z użyciem większego stosu. Mimo że program jest długi, jest całkiem prosty: main() wywołuje A(), A() wywołuje B(), B() wywołuje C(), C() wywołuje D(), a D() zgłasza wyjątek.
#include <iostream>
void D() // called by C()
{
std::cout << "Start D\n";
std::cout << "D throwing int exception\n";
throw - 1;
std::cout << "End D\n"; // skipped over
}
void C() // called by B()
{
std::cout << "Start C\n";
D();
std::cout << "End C\n";
}
void B() // called by A()
{
std::cout << "Start B\n";
try
{
C();
}
catch (double) // not caught: exception type mismatch
{
std::cerr << "B caught double exception\n";
}
try
{
}
catch (int) // not caught: exception not thrown within try
{
std::cerr << "B caught int exception\n";
}
std::cout << "End B\n";
}
void A() // called by main()
{
std::cout << "Start A\n";
try
{
B();
}
catch (int) // exception caught here and handled
{
std::cerr << "A caught int exception\n";
}
catch (double) // not called because exception was handled by prior catch block
{
std::cerr << "A caught double exception\n";
}
// execution continues here after the exception is handled
std::cout << "End A\n";
}
int main()
{
std::cout << "Start main\n";
try
{
A();
}
catch (int) // not called because exception was handled by A
{
std::cerr << "main caught int exception\n";
}
std::cout << "End main\n";
return 0;
}Przyjrzyj się temu programowi bardziej szczegółowo, i zobacz, czy możesz dowiedzieć się, co zostanie wydrukowane, a co nie, gdy zostanie uruchomiona. Odpowiedź jest następująca:
Start main Start A Start B Start C Start D D throwing int exception A caught int exception End A End main
Przeanalizujmy, co się stanie w tym przypadku. Wypisanie wszystkich instrukcji „Start” jest proste i nie wymaga dalszych wyjaśnień. Funkcja D() wypisuje „D zgłasza wyjątek int”, a następnie zaczyna robić się interesująco.
Ponieważ funkcja D() sama w sobie nie obsługuje wyjątku, jej funkcje wywołujące (funkcje na stosie wywołań) są sprawdzane, aby sprawdzić, czy któraś z nich może obsłużyć wyjątek. Funkcja C() nie obsługuje żadnych wyjątków, więc nie znaleziono w niej żadnego dopasowania.
Funkcja B() ma dwa oddzielne bloki try. Blok try zawierający wywołanie C() ma procedurę obsługi wyjątków typu double, ale nie pasuje ona do naszego wyjątku typu int (a wyjątki nie dokonują konwersji typów), zatem nie znaleziono żadnego dopasowania dla pustego bloku try procedura obsługi wyjątków dla wyjątków typu int, ale ten blok catch nie jest uważany za pasujący, ponieważ wywołanie C() nie znajduje się w powiązanym bloku try.
A() również ma blok try i zawiera wywołanie B(), więc program sprawdza, czy istnieje procedura obsługi wyjątków int. Jest! W rezultacie A() obsługuje wyjątek i wypisuje „Wychwycony wyjątek int”.
Ponieważ wyjątek został już obsłużony, sterowanie jest kontynuowane normalnie po blokach catch w A(). Oznacza to, że funkcja A() wypisuje „Koniec A”, a następnie kończy się normalnie.
Kontrola powraca do funkcji main(). Chociaż funkcja main() ma procedurę obsługi wyjątku dla int, nasz wyjątek został już obsłużony przez funkcję A(), więc blok catch w funkcji main() nie zostanie wykonany. main() po prostu wypisuje „End main” i następnie kończy się normalnie.
Istnieje kilka interesujących zasad zilustrowanych w tym programie:
Po pierwsze, osoba wywołująca funkcję, która zgłasza wyjątek, nie musi go obsługiwać, jeśli nie chce. W tym przypadku C() nie obsłużyło wyjątku zgłoszonego przez D(). Przekazał tę odpowiedzialność jednemu ze swoich wywołujących na stosie.
Po drugie, jeśli blok try nie ma procedury obsługi catch dla typu zgłaszanego wyjątku, odwijanie stosu odbywa się tak, jakby w ogóle nie było bloku try. W tym przypadku funkcja B() również nie obsłużyła wyjątku, ponieważ nie miała odpowiedniego rodzaju bloku catch.
Po trzecie, jeśli funkcja ma pasujący blok catch, ale wywołanie bieżącej funkcji nie nastąpiło w powiązanym bloku try, ten blok catch nie jest używany. Widzieliśmy to także w B().
Na koniec, po wykonaniu pasującego bloku catch, przepływ sterowania przebiega normalnie, zaczynając od pierwszej instrukcji po ostatnim bloku catch. Zostało to zademonstrowane przez obsługę błędu przez funkcję A(), następnie kontynuowanie operacji „End A” i powrót do osoby wywołującej. Zanim program wrócił do funkcji main(), wyjątek został już zgłoszony i obsłużony — funkcja main() nie miała pojęcia, że w ogóle istnieje wyjątek!
Jak widać, odwijanie stosu zapewnia nam bardzo przydatne zachowanie — jeśli funkcja nie chce obsłużyć wyjątku, nie musi. Wyjątek będzie propagowany w górę stosu, dopóki nie znajdzie kogoś, kto to zrobi! To pozwala nam zdecydować, gdzie na stosie wywołań jest najodpowiedniejsze miejsce do obsługi wszelkich błędów, które mogą wystąpić.
W następnej lekcji przyjrzymy się, co się stanie, jeśli nie przechwycisz wyjątku, oraz podamy metodę zapobiegania takiej sytuacji.

