Bloki Try and catch działają wystarczająco dobrze w większości przypadków, ale jest jeden szczególny przypadek, w którym nie są wystarczające. Rozważmy następujący przykład:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
if (x <= 0)
throw 1; // Exception thrown here
}
};
class B : public A
{
public:
B(int x) : A{x} // A initialized in member initializer list of B
{
// What happens if creation of A fails and we want to handle it here?
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}W powyższym przykładzie klasa pochodna B wywołuje konstruktor klasy bazowej A, który może zgłosić wyjątek. Ponieważ utworzenie obiektu b zostało umieszczone wewnątrz bloku try (w funkcji main()), jeśli A zgłosi wyjątek, blok try main go przechwyci. W rezultacie ten program wypisuje:
Oops
Ale co, jeśli chcemy przechwycić wyjątek wewnątrz B? Wywołanie konstruktora bazowego A następuje poprzez listę inicjatorów składowych, przed wywołaniem treści konstruktora B. Nie ma możliwości owinięcia wokół niego standardowego bloku try.
W tej sytuacji musimy użyć nieco zmodyfikowanego bloku try zwanego blokiem try funkcji.
Bloki prób funkcji
Bloki funkcji try zostały zaprojektowane tak, aby umożliwić ustanowienie procedury obsługi wyjątków wokół treści całej funkcji, a nie wokół bloku kodu.
Składnia bloków try funkcji jest trochę trudna do opisania, więc pokażemy to przykład:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
if (x <= 0)
throw 1; // Exception thrown here
}
};
class B : public A
{
public:
B(int x) try : A{x} // note addition of try keyword here
{
}
catch (...) // note this is at same level of indentation as the function itself
{
// Exceptions from member initializer list or
// from constructor body are caught here
std::cerr << "Exception caught\n";
throw; // rethrow the existing exception
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}Gdy ten program jest uruchomiony, generuje wynik:
Exception caught Oops
Przyjrzyjmy się temu programowi bardziej szczegółowo.
Najpierw zwróć uwagę na dodanie słowa kluczowego try przed listą inicjatorów składowych. Oznacza to, że wszystko po tym momencie (aż do końca funkcji) powinno być uwzględnione w bloku try.
Po drugie, należy zauważyć, że powiązany blok catch znajduje się na tym samym poziomie wcięcia co cała funkcja. Każdy wyjątek zgłoszony pomiędzy słowem kluczowym try a końcem treści funkcji będzie mógł zostać wychwycony w tym miejscu.
Gdy powyższy program zostanie uruchomiony, zmienna b rozpoczyna konstrukcję, która wywołuje konstruktor B (który wykorzystuje funkcję try). Konstruktor B wywołuje konstruktor A, który następnie zgłasza wyjątek. Ponieważ konstruktor A nie obsługuje tego wyjątku, wyjątek propaguje się w górę stosu do konstruktora B, gdzie zostaje przechwycony przez catch na poziomie funkcji konstruktora B. Blok catch wypisuje „Złapany wyjątek”, a następnie ponownie zgłasza bieżący wyjątek w górę stosu, który jest przechwytywany przez blok catch w main(), który wypisuje „Oops”.
Najlepsza praktyka
Użyj bloków funkcji try, gdy potrzebujesz konstruktora do obsługi wyjątku zgłoszonego na liście inicjatorów składowych.
Ograniczenia bloków funkcyjnych catch
Za pomocą zwykłego bloku catch (wewnątrz a funkcja), mamy trzy możliwości: możemy zgłosić nowy wyjątek, ponownie zgłosić bieżący wyjątek lub rozwiązać wyjątek (poprzez instrukcję return lub pozwalając kontroli dotrzeć do końca bloku catch).
Blok catch na poziomie funkcji dla konstruktora musi albo zgłosić nowy wyjątek, albo ponownie zgłosić istniejący wyjątek - nie mogą one rozwiązywać wyjątków! Instrukcje return również nie są dozwolone, a dotarcie do końca bloku catch spowoduje niejawne ponowne zgłoszenie.
Blok catch na poziomie funkcji dla destruktora może zgłosić, ponownie zgłosić lub rozwiązać bieżący wyjątek za pomocą instrukcji return. Dotarcie do końca bloku catch spowoduje niejawne ponowne zgłoszenie.
Blok catch na poziomie funkcji dla innych funkcji może zgłosić, ponownie zgłosić lub rozwiązać bieżący wyjątek za pomocą instrukcji return. Dotarcie do końca bloku catch domyślnie rozwiąże wyjątek dla funkcji zwracających wartości inne niż (void) i spowoduje niezdefiniowane zachowanie funkcji zwracających wartość!
Poniższa tabela podsumowuje ograniczenia i zachowanie bloków catch na poziomie funkcji:
| Typ funkcji | Można rozwiązać wyjątki za pomocą instrukcji return | Zachowanie na końcu blok catch |
|---|---|---|
| Konstruktor | Nie, należy rzucić lub ponownie rzucić | Niejawny ponowny rzut |
| Destruktor | Tak | Niejawny ponowny rzut |
| Funkcja nie zwracająca wartości | Tak | Rozwiąż wyjątek |
| Funkcja zwracająca wartość | Tak | Niezdefiniowane zachowanie |
Ponieważ takie zachowanie na końcu bloku catch różni się znacznie w zależności od typu funkcji (i obejmuje niezdefiniowane zachowanie w przypadku funkcji zwracających wartość), zalecamy, aby nigdy nie pozwalać kontroli na dotarcie do końca bloku catch i zawsze jawnie rzucać, ponowne rzucanie lub powracanie.
Najlepsza praktyka
Unikaj dotarcia kontroli do końca bloku catch na poziomie funkcji. Zamiast tego jawnie rzuć, powtórz lub zwróć.
W powyższym programie, gdybyśmy nie zgłosili jawnie ponownie wyjątku w bloku catch konstruktora na poziomie funkcji, sterowanie osiągnęłoby koniec catch na poziomie funkcji, a ponieważ był to konstruktor, zamiast tego nastąpiłby niejawny ponowny rzut. Wynik byłby taki sam.
Chociaż bloków try na poziomie funkcji można używać także z funkcjami niebędącymi składowymi, zazwyczaj tak nie jest, ponieważ rzadko zdarza się, aby było to potrzebne. Są używane prawie wyłącznie z konstruktorami!
Bloki funkcji try mogą przechwytywać wyjątki zarówno podstawowej, jak i bieżącej klasy.
W powyższym przykładzie, jeśli konstruktor A lub B zgłosi wyjątek, zostanie on przechwycony przez blok try wokół konstruktora B.
Możemy to zobaczyć w poniższym przykładzie, w którym zgłaszamy wyjątek od klasy B zamiast klasy O:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
}
};
class B : public A
{
public:
B(int x) try : A{x} // note addition of try keyword here
{
if (x <= 0) // moved this from A to B
throw 1; // and this too
}
catch (...)
{
std::cerr << "Exception caught\n";
// If an exception isn't explicitly thrown here,
// the current exception will be implicitly rethrown
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}Otrzymujemy ten sam wynik:
Exception caught Oops
Nie używaj funkcji, spróbuj wyczyścić zasoby
Gdy konstrukcja obiektu się nie powiedzie, destruktor klasy nie jest wywoływany. W związku z tym możesz ulec pokusie użycia bloku try funkcji w celu oczyszczenia klasy, która przed niepowodzeniem miała częściowo przydzielone zasoby. Jednak odwoływanie się do elementów uszkodzonego obiektu jest uważane za zachowanie niezdefiniowane, ponieważ obiekt jest „martwy” przed wykonaniem bloku catch. Oznacza to, że nie możesz użyć funkcji spróbuj posprzątać po zajęciach. Jeśli chcesz posprzątać po zajęciach, postępuj zgodnie ze standardowymi zasadami czyszczenia klas, które zgłaszają wyjątki (zobacz podsekcję lekcji „Kiedy konstruktory zawiodą” 27.5 — Wyjątki, klasy i dziedziczenie).
Funkcja try jest przydatna przede wszystkim w przypadku niepowodzeń rejestrowania przed przekazaniem wyjątku na stos lub do zmiany typu zgłoszonego wyjątku.

