Zaawansowane CPP/Wykład 13: Wyjątki: Różnice pomiędzy wersjami
m (Zastępowanie tekstu - "<div class="thumb"><div style="width:(.*);"> <flash>file=(.*)\.swf\|width=(.*)\|height=(.*)<\/flash> <div\.thumbcaption>(.*)<\/div> <\/div><\/div>" na "$3x$4px|thumb|center|$5") |
|||
(Nie pokazano 66 wersji utworzonych przez 6 użytkowników) | |||
Linia 1: | Linia 1: | ||
==Wstęp== | ==Wstęp== | ||
Jesteśmy istotami omylnymi, więc niezależnie od naszych starań | Jesteśmy istotami omylnymi, więc niezależnie od naszych starań, pisane przez nas | ||
programy bedą zawierały usterki, dostarczane do nich dane nie zawsze | programy bedą zawierały usterki, dostarczane do nich dane nie zawsze | ||
będą poprawne, a i sprzęt nie | będą poprawne, a i sprzęt może nie działać tak jak trzeba. | ||
Nie oznacza to, że należy zaniechać dążenia do pisania | Nie oznacza to, że należy zaniechać dążenia do pisania bezbłędnego kodu, | ||
wprost przeciwnie, jakość kodu powinna być jednym z naszych | wprost przeciwnie, jakość kodu powinna być jednym z naszych | ||
priorytetów, ale należy się też pogodzić z faktem, że błędy będą | |||
występować i powinniśmy być na takie sytuacje przygotowani | występować i powinniśmy być na takie sytuacje przygotowani. Jak to | ||
mówią "najwyższą formą zaufania jest kontrola". | mówią "najwyższą formą zaufania jest kontrola". | ||
Na potrzeby tego wykładu zdefiniujemy bardzo luźno błąd jako | Na potrzeby tego wykładu zdefiniujemy bardzo luźno błąd jako | ||
wystąpienie sytuacji, która wystąpić nie powinna. Nie będziemy też | wystąpienie sytuacji, która wystąpić nie powinna. Nie będziemy też | ||
interesować się bardzo tym jak błedy wykrywać, ale raczej co zrobić | interesować się bardzo tym, jak błedy wykrywać, ale raczej co zrobić, | ||
kiedy takowy wykryjemy. W następnym podrozdziale omówię bardzo | kiedy takowy wykryjemy. W następnym podrozdziale omówię bardzo | ||
pobieżnie różne możliwości reakcji na wystąpienie | pobieżnie różne możliwości reakcji na wystąpienie błędu i wprowadzę | ||
pojęcie wyjątku. Reszta wykładu będzie poświęcona zagadnieniom związanym | pojęcie wyjątku. Reszta wykładu będzie poświęcona zagadnieniom związanym | ||
z pisaniem kodu używającego | z pisaniem kodu używającego wyjątków. | ||
==Wykrywanie | ==Wykrywanie błędów== | ||
Zanim przejdziemy do sytuacji, w której wiemy, że wystąpił bład, musimy poświęcić kilka akapitów na zastanowienie się czy w ogóle należy błedy wykrywać i obsługiwać. Nawet jeśli większość z Państwa krzyknie "oczywiście, że | |||
której wiemy że | tak" (choć podejrzewam, że większość tego sama nie robi: kto ostatnio sprawdzał wartość zwróconą przez funkcję <code>printf</code>?), to | ||
akapitów na zastanowienie się | i tak pozostaje pytanie, jakie błędy będziemy starali się wykrywać. | ||
obsługiwać. | |||
tak" (choć podejrzewam że większość tego sama nie robi: kto ostatnio sprawdzał wartość zwróconą przez funkcję <code>printf</code>?), to | |||
i tak pozostaje pytanie jakie | |||
Na to pytanie nie ma jednoznacznej odpowiedzi, jak zresztą na | Na to pytanie nie ma jednoznacznej odpowiedzi, jak zresztą na | ||
Linia 35: | Linia 30: | ||
Na jednym końcu są programy, które po prostu nie mogą "paść", na | Na jednym końcu są programy, które po prostu nie mogą "paść", na | ||
drugim np. niektóre | drugim np. niektóre progamy symulacyjne, które wykonują się w | ||
godzinę lub mniej. | godzinę lub mniej. | ||
Nawet jednak w tej ostatniej sytuacji, różne formy obsługi błędów | Nawet jednak w tej ostatniej sytuacji, różne formy obsługi błędów | ||
Linia 42: | Linia 37: | ||
if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(fin<nowiki>=</nowiki>fopen(input_file_name,"r"))) { | if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(fin<nowiki>=</nowiki>fopen(input_file_name,"r"))) { | ||
fprintf(stderr,"cannot open file input_file_name); | fprintf(stderr,"cannot open file input_file_name); | ||
exit(1); } | exit(1); | ||
} | |||
} | } | ||
lub | lub | ||
Linia 52: | Linia 48: | ||
} | } | ||
} | } | ||
mogą nam | mogą nam oszczędzić jeśli nie godzin, to wielu minut frustracji. | ||
Proszę zwrócić uwagę że oba przykłady dotyczą zasobów | Proszę zwrócić uwagę, że oba przykłady dotyczą zasobów | ||
zewnętrznych, nie do końca pod naszą kontrolą i tym bardziej powinny | zewnętrznych, nie do końca pod naszą kontrolą i tym bardziej powinny | ||
być sprawdzane, zwłaszcza, że | być sprawdzane, zwłaszcza, że będzie nas to kosztować minutę pisania | ||
i prawie tyle co nic w trakcie wykonania. | i prawie tyle co nic w trakcie wykonania. | ||
===Kontrola zakresu=== | ===Kontrola zakresu=== | ||
A co z bardziej kosztownymi testami? Typowym | A co z bardziej kosztownymi testami? Typowym przykładem jest | ||
sprawdzanie zakresu. Czy np. nasz stos <code>Stack</code> powinien | sprawdzanie zakresu. Czy np. nasz stos <code>Stack</code> powinien | ||
sprawdzać, czy wykonujemy <code>pop</code> lub <code>top</code> na stosie pustym | sprawdzać, czy wykonujemy <code>pop</code> lub <code>top</code> na stosie pustym | ||
albo <code>push</code> na stosie pełnym? Czy | albo <code>push</code> na stosie pełnym? Czy powinniśmy sprawdzać | ||
poprawność podanego indeksu w wyrażeniach <code>v[i]</code>? | poprawność podanego indeksu w wyrażeniach <code>v[i]</code>? | ||
Mam nadzieję że | Mam nadzieję, że Państwo nie oczekują jednoznacznej odpowiedzi na te | ||
pytania, bo jej | pytania, bo jej po prostu nie ma. Niestety, tego rodzaju testy mogą być | ||
bardzo kosztowne. Operacje dostępu do elementów są bardzo proste, | bardzo kosztowne. Operacje dostępu do elementów są bardzo proste, | ||
koszt testu będzie pewnie dominujący, a te operacje mogą być bardzo | koszt testu będzie pewnie dominujący, a te operacje mogą być bardzo | ||
często wykonywane. Z drugiej strony | często wykonywane. Z drugiej strony błędy przekroczenia zakresu | ||
są bardzo "wredne". Rozważmy np. taki prosty kod: | są bardzo "wredne". Rozważmy np. taki prosty kod: | ||
Linia 82: | Linia 78: | ||
while(1) | while(1) | ||
std::cerr<<++i<<" "<<s.pop()<<""; | std::cerr<<++i<<" "<<s.pop()<<""; | ||
([[media:Overflow.cpp | Źródło: overflow.cpp]]) | |||
Na moim komputerze powyższy program | Na moim komputerze powyższy program wykonał 20981 operacji <code>pop()</code>, | ||
zanim padł z komunikatem <tt>Naruszenie ochrony pamięci</tt>. Proszę | zanim padł z komunikatem <tt>Naruszenie ochrony pamięci</tt>. Proszę | ||
zauważyć że najpierw zapisał 995 liczb w pamięci | zauważyć, że najpierw zapisał 995 liczb w pamięci należącej nie wiadomo | ||
do kogo! Skutki takiego | do kogo! Skutki takiego błędu mogą więc wystąpić w zupełnie innym | ||
miejscu programu. | miejscu programu. | ||
Nie ma dobrego rozwiązania tego dylematu, ale zawsze może pomóc zdrowy | Nie ma dobrego rozwiązania tego dylematu, ale zawsze może pomóc zdrowy | ||
rozsądek. Użycie sprawdzenia zakresu w operacji mnożenia macierzy | rozsądek. Użycie sprawdzenia zakresu w operacji mnożenia macierzy | ||
miałoby katastrofalne skutki dla wydajności kodu. Z drugiej strony | miałoby katastrofalne skutki dla wydajności kodu. Z drugiej strony | ||
jest to prosty kod w którym łatwo zapewnić aby indeksy nie wychodziły | jest to prosty kod, w którym łatwo zapewnić aby indeksy nie wychodziły | ||
poza zakres. Tutaj więc kontrola zakresu jest niewskazana. | poza zakres. Tutaj więc kontrola zakresu jest niewskazana. | ||
Częstym rozwiązaniem jest | Częstym rozwiązaniem jest włączanie kontroli zakresu podczas | ||
debugowania i wyłączanie jej w "produkcyjnej" wersji programu. Moim | debugowania i wyłączanie jej w "produkcyjnej" wersji programu. Moim | ||
zdaniem | zdaniem może to być bardzo pożyteczne, zwłaszcza w językach, które | ||
dopuszczają | dopuszczają włączanie i wyłączanie sprawdzania zakresów za pomocą | ||
opcji kompilacji (oczywiście może to dotyczyć tylko typów | opcji kompilacji (oczywiście może to dotyczyć tylko typów | ||
wbudowanych). W przypadku stosu, mogą nam się przydać w tym celu | wbudowanych). W przypadku stosu, mogą nam się przydać w tym celu | ||
klasy wytyczne, opracowane w | klasy wytyczne, opracowane w [http://osilek.mimuw.edu.pl/index.php?title=Zaawansowane_CPP/Wyk%C5%82ad_7:_Klasy_wytycznych wykładzie 7]. Może warto | ||
dodać, że kontenery STL, dostarczające operację indeksowania, | dodać, że kontenery STL, dostarczające operację indeksowania, | ||
dostarczają również metodę dostępu ze sprawdzaniem: <code>at(int)</code>. | dostarczają również metodę dostępu ze sprawdzaniem: <code>at(int)</code>. | ||
Linia 109: | Linia 105: | ||
Załóżmy więc, że w pogramie mamy przynajmniej kilka linijek | Załóżmy więc, że w pogramie mamy przynajmniej kilka linijek | ||
wykrywających potencjalne | wykrywających potencjalne błędy. No i stało się. Wiemy już, że w | ||
programie wystąpił | programie wystąpił błąd, co teraz? Mamy wiele możliwości, wymienię | ||
tylko kilka z nich: | tylko kilka z nich: | ||
# Kończymy program z ewentualnym komunikatem o błędzie. | # Kończymy program z ewentualnym komunikatem o błędzie. | ||
# Kończymy program, ale najpierw sprzątamy po sobie: zwalniamy zasoby, których nie zwolni system operacyjny, zapisujemy dane, itp. | # Kończymy program, ale najpierw sprzątamy po sobie: zwalniamy zasoby, których nie zwolni system operacyjny, zapisujemy dane, itp. | ||
# Staramy się kontynuować program pomimo | # Staramy się kontynuować program pomimo błędu, próbując go poprawić, obejść lub zrezygnować z części funkcjonalności. | ||
W tym wykładzie nie | W tym wykładzie nie będzie interesować nas sama konkretna strategia, | ||
ale sposób rozdzielenia procesu wykrycia błędu od wyboru strategii. | ale sposób rozdzielenia procesu wykrycia błędu od wyboru strategii. | ||
Jest to problem który dotyczy każdego kodu, ale głównie funkcji | Jest to problem, który dotyczy każdego kodu, ale głównie funkcji | ||
bibliotecznych. Ich projektant/programista może wykryć, że w trakcie | bibliotecznych. Ich projektant/programista może wykryć, że w trakcie | ||
ich wykonywania wystąpiła nieprawidłowość, ale nie może jednak wiedzieć | ich wykonywania wystąpiła nieprawidłowość, ale nie może jednak wiedzieć | ||
Linia 126: | Linia 122: | ||
Najprostyszym sposobem jest zwrócenie jakiejś wartości sygnalizującej | Najprostyszym sposobem jest zwrócenie jakiejś wartości sygnalizującej | ||
błąd. Jeśli funkcja nie zwraca żadnego wyniku, to jest to proste | błąd. Jeśli funkcja nie zwraca żadnego wyniku, to jest to proste; | ||
jeśli jednak funkcja ma zwracać jakiś wynik, to nie zawsze da się | jeśli jednak funkcja ma zwracać jakiś wynik, to nie zawsze da się | ||
znaleźć taką wartość, która by jednoznacznie mogła definiować błąd. | znaleźć taką wartość, która by jednoznacznie mogła definiować błąd. | ||
Rozszerzenia tej metody, to zwrócenie informacji o przebiegu funkcji | Rozszerzenia tej metody, to zwrócenie informacji o przebiegu funkcji | ||
poprzez dodatkowy argument przekazywany przez referencje. Można też | |||
ustawiać i odczytywać jakieś zmienne stanu. Największą wadą tego | ustawiać i odczytywać jakieś zmienne stanu. Największą wadą tego | ||
podejścia jest konieczność każdorazowego sprawdzania tych wartości, co | podejścia jest konieczność każdorazowego sprawdzania tych wartości, co | ||
wymaga pisania dużej ilości trywialnego kodu. Z tego powodu | wymaga pisania dużej ilości trywialnego kodu. Z tego powodu | ||
sprawdzanie poprawności wywołania takich funkcji jest często | |||
opuszczane. W C++ dochodzi jeszcze niemożność zwrócenia wartości z | opuszczane. W C++ dochodzi jeszcze niemożność zwrócenia wartości z | ||
konstruktora (choć oczywiście możemy ustawić w nim zmienną stanu | konstruktora (choć oczywiście możemy ustawić w nim zmienną stanu | ||
informującą o powodzeniu konstrukcji). | |||
== | ==Wyjątki== | ||
C++ dostarcza nowego mechanizmu, są | C++ dostarcza nowego mechanizmu, jakim są wyjątki. Polega on na tym, że | ||
funkcja która | funkcja która błąd wykryje i nie chce lub nie może go obsłużyć sama, | ||
rzuca wyjątek, który może być dowolnym obiektem. Rzucenie wyjątku | rzuca wyjątek, który może być dowolnym obiektem. Rzucenie wyjątku | ||
powoduje natychmiastowe przerwanie wykonywania funkcji. Procedura | powoduje natychmiastowe przerwanie wykonywania funkcji. Procedura | ||
wywołująca może ten wyjątek złapać. Wyjątek niezłapany prowadzi do | wywołująca może ten wyjątek złapać. Wyjątek niezłapany prowadzi do | ||
zatrzymania programu, a więc wyjątki nie mogą zostać zignorowane. | |||
Zilustruję to na przykładzie naszego stosu, do którego dodam | Zilustruję to na przykładzie naszego stosu, do którego dodam | ||
instrukcje rzucające wyjątki | instrukcje rzucające wyjątki w przypadku przekroczenia zakresu | ||
(dla prostoty nie będę korzystał z klas wytycznych): | (dla prostoty nie będę korzystał z klas wytycznych): | ||
Linia 171: | Linia 167: | ||
bool is_empty() const {return (_top<nowiki>=</nowiki><nowiki>=</nowiki>0);} | bool is_empty() const {return (_top<nowiki>=</nowiki><nowiki>=</nowiki>0);} | ||
}; | }; | ||
([[media:Stack_except.h | Źródło: stack_except.h]]) | |||
Polecenie <code>throw</code> służy właśnie do rzucania wyjątków. W tym wypadku | Polecenie <code>throw</code> służy właśnie do rzucania wyjątków. W tym wypadku | ||
rzucane są stałe napisowe, które będą automatycznie konwertowane na typ | rzucane są stałe napisowe, które będą automatycznie konwertowane na typ | ||
<code>const char *</code>. | <code>const char *</code>. | ||
Wykonanie programu | Wykonanie programu: | ||
main() { | main() { | ||
Linia 187: | Linia 183: | ||
s.push(i); /* tu też gdyby udało się tu dojść */ | s.push(i); /* tu też gdyby udało się tu dojść */ | ||
} | } | ||
([[media:Overflow.cpp | Źródło: overflow.cpp]]) | |||
spowoduje przerwanie programu w trakcie wykonywania drugiego polecenia | spowoduje przerwanie programu w trakcie wykonywania drugiego polecenia | ||
<code>pop</code>. Komunikat, który się przy tym pojawia jest zależny | <code>pop</code>. Komunikat, który się przy tym pojawia jest zależny | ||
od implementacji. | od implementacji. | ||
Wyjątki można łapać korzystając z bloku <code>try</code> | Wyjątki można łapać, korzystając z bloku <code>try</code>: | ||
Stack<int,5> s; | Stack<int,5> s; | ||
Linia 211: | Linia 207: | ||
std::cerr<<msg<<std::endl; | std::cerr<<msg<<std::endl; | ||
} | } | ||
([[media:Stack_except.cpp | Źródło: stack_except.cpp]]) | |||
W bloku <code>try</code> umieszczamy instrukcje które mogą potencjalnie rzucić | W bloku <code>try</code> umieszczamy instrukcje, które mogą potencjalnie rzucić | ||
wyjątek. Za blokiem <code>try</code> umieszczamy jedną lub więcej klauzul <code>catch</code> | wyjątek. Za blokiem <code>try</code> umieszczamy jedną lub więcej klauzul <code>catch</code>, | ||
które te wyjątki łapią. Wyjątek rzucony w bloku <code>try</code> powoduje przekazanie | które te wyjątki łapią. Wyjątek rzucony w bloku <code>try</code> powoduje przekazanie | ||
sterowania do pierwszej pasującej klauzuli <code>catch</code>. | sterowania do pierwszej pasującej klauzuli <code>catch</code>. | ||
== | ==Wyjątki złapane== | ||
Przyjrzyjmy się teraz | Przyjrzyjmy się teraz dokładniej mechanizmowi rzucania i łapania | ||
wyjątków. Rozważmy prosty przykład: | wyjątków. Rozważmy prosty przykład: | ||
Linia 248: | Linia 244: | ||
cout<<"main"; | cout<<"main"; | ||
} | } | ||
([[media:Caught.cpp | Źródło: caught.cpp]]) | |||
Oto wynik wykonania tego programu: | Oto wynik wykonania tego programu: | ||
Linia 260: | Linia 256: | ||
main | main | ||
destructing 2 | destructing 2 | ||
Co możemy zauważyć? | Co możemy zauważyć? | ||
# Wyjątek przerwał wykonywanie funkcji <code>f()</code> i bloku <code>try</code>, sterowanie zostało przekazana do klauzuli <code>catch(int)</code>. | # Wyjątek przerwał wykonywanie funkcji <code>f()</code> i bloku <code>try</code>, sterowanie zostało przekazana do klauzuli <code>catch(int)</code>. | ||
Linia 266: | Linia 262: | ||
# Po wykonaniu klauzuli <code>catch</code> sterowanie zostało przekazane do następnego wyrażenia. | # Po wykonaniu klauzuli <code>catch</code> sterowanie zostało przekazane do następnego wyrażenia. | ||
Klauzula <code>catch(...)</code> wyłapuje każdy | Klauzula <code>catch(...)</code> wyłapuje każdy wyjątek. Jeśli np. pominiemy | ||
klauzulę <code>catch(int)</code>: | |||
catch(double){cout<<"zlapalem double-a";} | catch(double){cout<<"zlapalem double-a";} | ||
//catch(int){cout<<"zlapalem int-a";} | //catch(int){cout<<"zlapalem int-a";} | ||
catch(...){cout<<"zlapalem cos ";} | catch(...){cout<<"zlapalem cos ";} | ||
to wynikiem wywołania programu będzie: | to wynikiem wywołania programu będzie: | ||
Linia 283: | Linia 279: | ||
main | main | ||
destructing 2 | destructing 2 | ||
Z tego przykładu widać też, że w przypadku dopasowywania klauzul | Z tego przykładu widać też, że w przypadku dopasowywania klauzul | ||
<code>catch</code> nie następuje niejawna konwersja argumentów. | <code>catch</code> nie następuje niejawna konwersja argumentów. | ||
== | ==Niezłapane wyjątki== | ||
A co się stanie, jeśli wyjątku nie złapiemy ? | A co się stanie, jeśli wyjątku nie złapiemy? Żeby się o tym przekonać | ||
usuniemy kolejną klauzulę <code>catch</code>: | usuniemy kolejną klauzulę <code>catch</code>: | ||
Linia 295: | Linia 291: | ||
//catch(int){cout<<"zlapalem int-a";} | //catch(int){cout<<"zlapalem int-a";} | ||
//catch(...){cout<<"zlapalem cos ";} | //catch(...){cout<<"zlapalem cos ";} | ||
Wynik programu jest teraz zupełnie inny: | Wynik programu jest teraz zupełnie inny: | ||
Linia 303: | Linia 299: | ||
terminate called after throwing an instance of 'int' | terminate called after throwing an instance of 'int' | ||
Abort | Abort | ||
Niezłapany wyjątek spowodował wywołanie funkcji <code>abort()</code>, która | Niezłapany wyjątek spowodował wywołanie funkcji <code>abort()</code>, która | ||
zakończyła program bez wywołania destruktorów. | zakończyła program bez wywołania destruktorów. Ściśle rzecz | ||
biorąc, niezłapany wyjątek wywołuje funkcję <code>terminate()</code>, która z | biorąc, niezłapany wyjątek wywołuje funkcję <code>terminate()</code>, która z | ||
kolei domyślnie wywołuje funkcję <code>abort()</code>. Co do tego, czy wywoływane | kolei domyślnie wywołuje funkcję <code>abort()</code>. Co do tego, czy wywoływane | ||
są destruktory lokalnych obiektów (zwijanie stosu), to jest to | są destruktory lokalnych obiektów (zwijanie stosu), to jest to | ||
zachowanie zależne od implementacji. Jak widać, w implementacji | zachowanie zależne od implementacji. Jak widać, w implementacji | ||
<code>g++</code> | <code>g++</code> w przypadku niezłapania wyjątku destruktory obiektów nie są | ||
wywoływane. | wywoływane. | ||
Domyślne zachowanie funkcji <code>terminate()</code> można zmienić, ustawiając | Domyślne zachowanie funkcji <code>terminate()</code> można zmienić, ustawiając | ||
własną | własną funkcję, za pomocą: | ||
namespace std { | namespace std { | ||
Linia 320: | Linia 316: | ||
terminate_handler set_terminate(terminate_handler new_terminate); | terminate_handler set_terminate(terminate_handler new_terminate); | ||
} | } | ||
Funkcja ustawiana w tym poleceniu nie może zwrócić sterowania, taka | Funkcja ustawiana w tym poleceniu nie może zwrócić sterowania, taka | ||
próba kończy sie wywołaniem funkcji <code>abort()</code>. Oznacza to, że funkcja | próba kończy sie wywołaniem funkcji <code>abort()</code>. Oznacza to, że funkcja | ||
Linia 332: | Linia 328: | ||
throw 0; | throw 0; | ||
} | } | ||
([[media:Terminate.cpp | Źródło: terminate.cpp]]) | |||
==Wyjątki w destruktorach== | ==Wyjątki w destruktorach== | ||
Jeśli podczas opisanego powyżej procesu obsługi wyjątku, wywołana | Jeśli podczas opisanego powyżej procesu obsługi wyjątku, wywołana | ||
zostanie funkcja która sama wywoła wyjątek, to program zostanie | zostanie funkcja, która sama wywoła wyjątek, to program zostanie | ||
natychmiast przerwany wykonaniem funkcji <code>terminate()</code> (nie dotyczy | natychmiast przerwany wykonaniem funkcji <code>terminate()</code> (nie dotyczy | ||
to już funkcji wywoływanych wewnątrz klauzuli <code>catch</code>). W | to już funkcji wywoływanych wewnątrz klauzuli <code>catch</code>). W | ||
szczególności stanie się to jeśli któryś z destruktorów wywoływanych w | szczególności stanie się to, jeśli któryś z destruktorów wywoływanych w | ||
trakcie zwijania stosu rzuci wyjątek: | trakcie zwijania stosu rzuci wyjątek: | ||
struct X { | struct X { | ||
~X() { | |||
std::cerr<<std::uncaught_exception()<<""; | std::cerr<<std::uncaught_exception()<<""; | ||
throw 0;}; | throw 0;}; | ||
Linia 362: | Linia 358: | ||
catch(int) {}; | catch(int) {}; | ||
} | } | ||
([[media:Destruktor.cpp | Źródło: destruktor.cpp]]) | |||
W powyższym kodzie destruktor klasy <code>X</code> jest wołany dwa razy. Po raz | W powyższym kodzie destruktor klasy <code>X</code> jest wołany dwa razy. Po raz | ||
pierwszy, podczas wychodzenia z pierwszego bloku <code>try</code>. Jest to | pierwszy, podczas wychodzenia z pierwszego bloku <code>try</code>. Jest to | ||
normalne wywołanie spowodowane wyjściem poza zakres. | normalne wywołanie spowodowane wyjściem poza zakres. | ||
Destruktor rzuca wyjątek, który zostaje wyłapany | Destruktor rzuca wyjątek, który zostaje wyłapany przez | ||
klauzulę <code>catch(int)</code> na końcu bloku. Drugi raz | klauzulę <code>catch(int)</code> na końcu bloku. Drugi raz destruktor jest | ||
wołany jako część zwijania stosu po wyjątku rzuconym jawnie w drugim bloku | wołany jako część zwijania stosu po wyjątku rzuconym jawnie w drugim bloku | ||
<code>try</code>. Mimo | <code>try</code>. Mimo że łapiemy wyjątki <code>int</code>, i tak w tej sytuacji | ||
wywoływana jest funkcja <code>terminate()</code>, a w konsekwencji i | wywoływana jest funkcja <code>terminate()</code>, a w konsekwencji i | ||
<code>abort()</code>. Jest to jeden z powodów, dla których destruktory | <code>abort()</code>. Jest to jeden z powodów, dla których destruktory | ||
nie powinny rzucać wyjątków. Funkcja <code>uncaught_exception()</code> | nie powinny rzucać wyjątków. Funkcja <code>uncaught_exception()</code> | ||
umożliwia rozróżnienie tych dwu kontekstów wywołania destruktora. | umożliwia rozróżnienie tych dwu kontekstów wywołania destruktora. | ||
Zwraca ona prawdę jeśli jakiś wyjątek jest właśnie obsługiwany. | Zwraca ona prawdę, jeśli jakiś wyjątek jest właśnie obsługiwany. | ||
Inne powody nie rzucania wyjątków z destruktorow | Inne powody nie rzucania wyjątków z destruktorow wiążą się z dynamiczną | ||
alokacją pamięci i zostaną omówione w kolejnym wykładzie. | |||
==Hierachie wyjątków== | ==Hierachie wyjątków== | ||
Jako wyjątek może zostać wyrzucony dowolny obiekt. Umożliwia nam to | Jako wyjątek może zostać wyrzucony dowolny obiekt. Umożliwia nam to | ||
grupowanie wyjątków w hierarchie | grupowanie wyjątków w hierarchie za pomocą dziedziczenia. Zilustrujemy | ||
to za pomocą hierachii wyjątków z biblioteki standardowej, przedstawionej | to za pomocą hierachii wyjątków z biblioteki standardowej, przedstawionej | ||
na | na [[#rys.13.1|rysunku 13.1]]. | ||
{{kotwica|rys.13.1|}}<center>[[File:cpp-13-exceptions.svg|600x350px|thumb|center|Rysunek 13.1. Hierarchia wyjątków biblioteki standardowej.]] | |||
{Hierarchia wyjątków biblioteki standardowej. | </center> | ||
Można z niej korzystać np. następująco: | Można z niej korzystać np. następująco: | ||
Linia 406: | Linia 402: | ||
} | } | ||
} | } | ||
([[media:Hierarchy.cpp | Źródło: hierarchy.cpp]]) | |||
Kolejność klauzul <code>catch</code> jest ważna, ponieważ klauzule są | Kolejność klauzul <code>catch</code> jest ważna, ponieważ klauzule są | ||
sprawdzane po kolei | sprawdzane po kolei i pierwsza, która pasuje, zostanie wykonana. | ||
Gdybyśmy więc podali kaluzulę <code>catch(Exception &e)</code> jako pierwszą, | Gdybyśmy więc podali kaluzulę <code>catch(Exception &e)</code> jako pierwszą, | ||
przechwyciła by ona | przechwyciła by ona wszystkie standardowe wyjątki. Ważne jest też, aby | ||
korzystając z hierarchii dziedziczenia przechwytywać wyjątki przez | korzystając z hierarchii dziedziczenia, przechwytywać wyjątki przez | ||
referencję. Inaczej nie zostaną wywołane poprawne funkcje wirtualne. | referencję. Inaczej nie zostaną wywołane poprawne funkcje wirtualne. | ||
Zachęcam do eksperymentów z powyższym kodem. | Zachęcam do eksperymentów z powyższym kodem. | ||
==Deklaracje | ==Deklaracje wyjątków== | ||
C++ pozwala na deklarowanie listy możliwych wyjątków rzucanych z | C++ pozwala na deklarowanie listy możliwych wyjątków rzucanych z | ||
funkcji. Służy do tego deklaracja <code>throw(...)</code> | funkcji. Służy do tego deklaracja <code>throw(...)</code> umieszczana za | ||
deklaracją listy argumentów funkcji np.: | deklaracją listy argumentów funkcji, np.: | ||
void no_throw(int) throw(); | void no_throw(int) throw(); | ||
deklaruje, że funkcja <code>no_throw</code> nie rzuci żadnego wyjątku, natomiast | deklaruje, że funkcja <code>no_throw</code> nie rzuci żadnego wyjątku, natomiast | ||
void throw_std(int) throw(exception); | void throw_std(int) throw(exception); | ||
deklaruje, że funkcja <code>throw_std</code> będzie rzucać tylko wyjątki ze | deklaruje, że funkcja <code>throw_std</code> będzie rzucać tylko wyjątki ze | ||
standardowej | standardowej hierarchii. Brak deklaracji oznacza, że funkcja może rzucać | ||
co chce. | co chce. | ||
Linia 439: | Linia 435: | ||
void g() throw(int) {throw 0;}; | void g() throw(int) {throw 0;}; | ||
void no_throw(int i) {f(i;}; | void no_throw(int i) {f(i;}; | ||
Funkcja <code>f</code> proklamuje całemu światu, że może rzucać wyjątek | Funkcja <code>f</code> proklamuje całemu światu, że może rzucać wyjątek | ||
(poprzez brak deklaracji, że nie może), podobnie funkcja <code>g()</code>, a pomimo to kompilator nie | (poprzez brak deklaracji, że nie może), podobnie funkcja <code>g()</code>, a pomimo to kompilator nie | ||
pozwala na to, aby wywołać ją wewnątrz funkcji która jawnie deklaruje, | pozwala na to, aby wywołać ją wewnątrz funkcji, która jawnie deklaruje, | ||
że wyjątku nie rzuci. Proszę to porównać np. | że wyjątku nie rzuci. Proszę to porównać np. z zastosowaniem | ||
kwalifikatora <code>const</code>: funkcja zadeklarowana jako <code>const</code> nie | kwalifikatora <code>const</code>: funkcja zadeklarowana jako <code>const</code> nie | ||
może | może wywołać funkcji, które <code>const</code> nie są. W przypadku wyjątków | ||
narzucenie takiej konsystencji spowodowałoby | narzucenie takiej konsystencji spowodowałoby, że potrzeba by | ||
przerabiać ogromne ilości kodu, | przerabiać ogromne ilości kodu napisanego, zanim mechanizm wyjątków | ||
stał się używany. Sprawia to niestety, że deklaracje wyjątków nie są | stał się używany. Sprawia to niestety, że deklaracje wyjątków nie są | ||
zbyt użyteczne, łatwo bowiem niechcący napisać kod który je złamie. A | zbyt użyteczne, łatwo bowiem niechcący napisać kod, który je złamie. A | ||
konsekwencje tego są poważne. Sprawdzania konsystencji w czasie | konsekwencje tego są poważne. Sprawdzania konsystencji w czasie | ||
kompilacji wprawdzie nie ma, ale jest sprawdzanie w czasie wykonania. | kompilacji wprawdzie nie ma, ale jest sprawdzanie w czasie wykonania. | ||
Linia 457: | Linia 453: | ||
złapanie tego wyjątku nic nie pomoże. | złapanie tego wyjątku nic nie pomoże. | ||
Kolejnym | Kolejnym problemem są szablony funkcji i metody szablonów klas. Ich | ||
projektant nie może przewidzieć z jakimi argumentami zostaną one | projektant nie może przewidzieć, z jakimi argumentami zostaną one | ||
konkretyzowane, a więc jakie wyjątki mogą zostać rzucone. Dlatego w | konkretyzowane, a więc jakie wyjątki mogą zostać rzucone. Dlatego w | ||
szablonach lepiej deklaracje wyjątków pomijać. W ogóle, deklaracje | szablonach lepiej deklaracje wyjątków pomijać. W ogóle, deklaracje | ||
wyjątków należy umieszczać tam, gdzie uważamy, że rzucenie każdego innego | wyjątków należy umieszczać tam, gdzie uważamy, że rzucenie każdego innego | ||
wyjątku jest przejawem poważnego | wyjątku jest przejawem poważnego błędu. Najczęściej jest to sytuacja, | ||
kiedy chcemy zadeklarować, że dana funkcja w ogóle nie rzuca wyjątków. | kiedy chcemy zadeklarować, że dana funkcja w ogóle nie rzuca wyjątków. | ||
Jak już pisałem taką własność powinny posiadać destruktory. | Jak już pisałem taką własność powinny posiadać destruktory. | ||
Linia 468: | Linia 464: | ||
===Niespodziewane wyjątki=== | ===Niespodziewane wyjątki=== | ||
Podobnie jak w przypadku funkcji <code>terminate()</code> możemy podstawić | Podobnie jak w przypadku funkcji <code>terminate()</code>, możemy podstawić | ||
własną funkcję <code>unexpected()</code>. Podobnie jak <code>terminate()</code> funkcja | własną funkcję <code>unexpected()</code>. Podobnie jak <code>terminate()</code> funkcja | ||
<code>unexpected()</code> nie zwraca sterowania, może za to sama rzucić | <code>unexpected()</code> nie zwraca sterowania, może za to sama rzucić | ||
Linia 484: | Linia 480: | ||
catch(...) {}; | catch(...) {}; | ||
} | } | ||
([[media:Unexpected.cpp | Źródło: unexpected.cpp]]) | |||
Ponieważ <code>f()</code> rzuca <code>const char *</code>, a deklaruje tylko <code>int</code>-a, | Ponieważ <code>f()</code> rzuca <code>const char *</code>, a deklaruje tylko <code>int</code>-a, | ||
powyższy program wywoła funkcję <code>unexpected()</code>, która przerwie program. | powyższy program wywoła funkcję <code>unexpected()</code>, która przerwie program. | ||
Linia 492: | Linia 488: | ||
void unexpected_handler() {throw 0;}; | void unexpected_handler() {throw 0;}; | ||
std::set_unexpected(unexpected_handler); | std::set_unexpected(unexpected_handler); | ||
to zamiast <code>const char *</code> zostanie rzucony <code>int</code> i złapany przez | to zamiast <code>const char *</code> zostanie rzucony <code>int</code> i złapany przez | ||
klauzulę <code>catch(...)</code>. Tak się stanie ponieważ <code>int</code> jest na | klauzulę <code>catch(...)</code>. Tak się stanie ponieważ <code>int</code> jest na | ||
liście wyjątków funkcji <code>f()</code>. Gdyby nie był, to zostałaby wywołana | liście wyjątków funkcji <code>f()</code>. Gdyby nie był, to zostałaby wywołana | ||
funkcja <code>terminate()</code>. Jeżeli jednak deklaracja wyjątków funkcji | funkcja <code>terminate()</code>. Jeżeli jednak deklaracja wyjątków funkcji | ||
<code>f()</code> zawierać będzie | <code>f()</code> zawierać będzie wyjątek <code>std::bad_exception</code>, to każdy | ||
wyjątek rzucony przez <code>unexpected()</code> i nie znajdujący się na liście | wyjątek rzucony przez <code>unexpected()</code> i nie znajdujący się na liście | ||
zadeklarowanych wyjątków funkcji <code>f()</code> jest | zadeklarowanych wyjątków funkcji <code>f()</code>, jest podmieniany na | ||
<code>std::bad_exception()</code>: | <code>std::bad_exception()</code>: | ||
Linia 515: | Linia 511: | ||
catch(std::bad_exception ) {}; | catch(std::bad_exception ) {}; | ||
} | } | ||
([[media:Unexpected.cpp | Źródło: unexpected.cpp]]) | |||
==Wydajność wyjątków== | ==Wydajność wyjątków== | ||
Autor pozycji | Autor pozycji <i>"Język C++ bardziej efektywny"</i> S. Meyers sugerował, że użycie mechanizmu | ||
wyjątków może spowolnić program, nawet jeśli wyjątki nie będą rzucane. | wyjątków może spowolnić program, nawet jeśli wyjątki nie będą rzucane. | ||
Ponieważ od tego czasu minęło dobrych kilka lat, postanowiłem sam | Ponieważ od tego czasu minęło dobrych kilka lat, postanowiłem sam | ||
sprawdzić jak się sprawy mają. W tym celu skorzystałem z następujących | sprawdzić, jak się sprawy mają. W tym celu skorzystałem z następujących | ||
programików: | programików: | ||
Linia 538: | Linia 534: | ||
} | } | ||
} | } | ||
([[media:Exceptions.cpp | Źródło: exceptions.cpp]]) | |||
oraz | oraz | ||
Linia 553: | Linia 549: | ||
s+<nowiki>=</nowiki>scin(rand()/(double)RAND_MAX,f); | s+<nowiki>=</nowiki>scin(rand()/(double)RAND_MAX,f); | ||
} | } | ||
([[media:No_exceptions.cpp | Źródło: no_exceptions.cpp]]) | |||
Jak widać drugi z nich nie ma nawet śladu wyjątków. W pierwszym podejściu | Jak widać drugi z nich nie ma nawet śladu wyjątków. W pierwszym podejściu | ||
ustawilem flagę <code>f</code> na zero, co powodowało że żaden wyjątek nie był rzucany. | ustawilem flagę <code>f</code> na zero, co powodowało, że żaden wyjątek nie był rzucany. | ||
Czas wykonania obu programów (w sekundach) podany jest w poniższej tabelce | Czas wykonania obu programów (w sekundach) podany jest w poniższej tabelce: | ||
<div align=center> | |||
{| border=1 | {| border=1 | ||
|- | |- | ||
| || | |align="center"| || nie rzucane wyjątki || bez wyjątków || rzucane wyjątki || <tt>return</tt> | ||
|- | |- | ||
| -O0 || 15 | |align="center"| -O0 | ||
|align="center"| 15 | |||
|align="center"| 15 | |||
|align="center"| | |||
|align="center"| | |||
|- | |- | ||
| -O1 | |align="center"| -O1 | ||
|align="center"| 13 | |||
|align="center"| 4 | |||
|align="center"| | |||
|align="center"| | |||
|- | |- | ||
| -O2 | |align="center"| -O2 | ||
|align="center"| 13 | |||
|align="center"| 4 | |||
|align="center"| | |||
|align="center"| | |||
|- | |- | ||
| -O3 | |align="center"| -O3 | ||
|align="center"| 4 | |||
|align="center"| 4 | |||
|align="center"| 600 | |||
|align="center"| 4 | |||
|} | |} | ||
</div> | |||
Porównując kolumny 1 i 2 widać że dla pełnej optymalizacji | Porównując kolumny 1 i 2, widać, że dla pełnej optymalizacji nie ma | ||
żadnej różnicy. Szczegółowe badanie wykazało, że to włączenie opcji | żadnej różnicy. Szczegółowe badanie wykazało, że to włączenie opcji | ||
<code>-finline-functions</code> powoduje skok | <code>-finline-functions</code> powoduje skok prędkości pomiędzy dwoma | ||
ostatnimi wierszami w pierwszej kolumnie. Ten sam | ostatnimi wierszami w pierwszej kolumnie. Ten sam efekt można uzyskać, | ||
dodając do funkcji <code>scin</code> kwalifikator <code>inline</code>. | dodając do funkcji <code>scin</code> kwalifikator <code>inline</code>. | ||
Następnie porównałem koszt zwykłego powrotu z funkcji | Następnie porównałem koszt zwykłego powrotu z funkcji z kosztem | ||
rzucenia wyjątku. Wyniki są przedstawione w dwóch ostatnich kolumnach | rzucenia wyjątku. Wyniki są przedstawione w dwóch ostatnich kolumnach | ||
tabelki. Tu widać dramatyczną różnicę: obsługa wyjątku jest ponad 100 | tabelki. Tu widać dramatyczną różnicę: obsługa wyjątku jest ponad 100 | ||
razy wolniejsza. | razy wolniejsza. |
Aktualna wersja na dzień 10:55, 3 paź 2021
Wstęp
Jesteśmy istotami omylnymi, więc niezależnie od naszych starań, pisane przez nas programy bedą zawierały usterki, dostarczane do nich dane nie zawsze będą poprawne, a i sprzęt może nie działać tak jak trzeba.
Nie oznacza to, że należy zaniechać dążenia do pisania bezbłędnego kodu, wprost przeciwnie, jakość kodu powinna być jednym z naszych priorytetów, ale należy się też pogodzić z faktem, że błędy będą występować i powinniśmy być na takie sytuacje przygotowani. Jak to mówią "najwyższą formą zaufania jest kontrola".
Na potrzeby tego wykładu zdefiniujemy bardzo luźno błąd jako wystąpienie sytuacji, która wystąpić nie powinna. Nie będziemy też interesować się bardzo tym, jak błedy wykrywać, ale raczej co zrobić, kiedy takowy wykryjemy. W następnym podrozdziale omówię bardzo pobieżnie różne możliwości reakcji na wystąpienie błędu i wprowadzę pojęcie wyjątku. Reszta wykładu będzie poświęcona zagadnieniom związanym z pisaniem kodu używającego wyjątków.
Wykrywanie błędów
Zanim przejdziemy do sytuacji, w której wiemy, że wystąpił bład, musimy poświęcić kilka akapitów na zastanowienie się czy w ogóle należy błedy wykrywać i obsługiwać. Nawet jeśli większość z Państwa krzyknie "oczywiście, że
tak" (choć podejrzewam, że większość tego sama nie robi: kto ostatnio sprawdzał wartość zwróconą przez funkcję printf
?), to
i tak pozostaje pytanie, jakie błędy będziemy starali się wykrywać.
Na to pytanie nie ma jednoznacznej odpowiedzi, jak zresztą na większość pytań dotyczących decyzji projektowych. Różne projekty wymagają różnego poziomu niezawodności, a więc i różnych zabezpieczeń.
Na jednym końcu są programy, które po prostu nie mogą "paść", na drugim np. niektóre progamy symulacyjne, które wykonują się w godzinę lub mniej. Nawet jednak w tej ostatniej sytuacji, różne formy obsługi błędów mogą nam bardzo pomóc w debugowaniu. Linijki typu:
if( NULL==(fin=fopen(input_file_name,"r"))) { fprintf(stderr,"cannot open file input_file_name); exit(1); } }
lub
if( NULL==(p=malloc(n_bytes))) { fprintf(stderr,"cannot allocate memory for ..."); exit(1); } }
mogą nam oszczędzić jeśli nie godzin, to wielu minut frustracji. Proszę zwrócić uwagę, że oba przykłady dotyczą zasobów zewnętrznych, nie do końca pod naszą kontrolą i tym bardziej powinny być sprawdzane, zwłaszcza, że będzie nas to kosztować minutę pisania i prawie tyle co nic w trakcie wykonania.
Kontrola zakresu
A co z bardziej kosztownymi testami? Typowym przykładem jest
sprawdzanie zakresu. Czy np. nasz stos Stack
powinien
sprawdzać, czy wykonujemy pop
lub top
na stosie pustym
albo push
na stosie pełnym? Czy powinniśmy sprawdzać
poprawność podanego indeksu w wyrażeniach v[i]
?
Mam nadzieję, że Państwo nie oczekują jednoznacznej odpowiedzi na te pytania, bo jej po prostu nie ma. Niestety, tego rodzaju testy mogą być bardzo kosztowne. Operacje dostępu do elementów są bardzo proste, koszt testu będzie pewnie dominujący, a te operacje mogą być bardzo często wykonywane. Z drugiej strony błędy przekroczenia zakresu są bardzo "wredne". Rozważmy np. taki prosty kod:
Stack<int,5> s; for(int i=0;i<1000;i++) s.push(i); int i=0; while(1) std::cerr<<++i<<" "<<s.pop()<<"";
Na moim komputerze powyższy program wykonał 20981 operacji pop()
,
zanim padł z komunikatem Naruszenie ochrony pamięci. Proszę
zauważyć, że najpierw zapisał 995 liczb w pamięci należącej nie wiadomo
do kogo! Skutki takiego błędu mogą więc wystąpić w zupełnie innym
miejscu programu.
Nie ma dobrego rozwiązania tego dylematu, ale zawsze może pomóc zdrowy rozsądek. Użycie sprawdzenia zakresu w operacji mnożenia macierzy miałoby katastrofalne skutki dla wydajności kodu. Z drugiej strony jest to prosty kod, w którym łatwo zapewnić aby indeksy nie wychodziły poza zakres. Tutaj więc kontrola zakresu jest niewskazana.
Częstym rozwiązaniem jest włączanie kontroli zakresu podczas
debugowania i wyłączanie jej w "produkcyjnej" wersji programu. Moim
zdaniem może to być bardzo pożyteczne, zwłaszcza w językach, które
dopuszczają włączanie i wyłączanie sprawdzania zakresów za pomocą
opcji kompilacji (oczywiście może to dotyczyć tylko typów
wbudowanych). W przypadku stosu, mogą nam się przydać w tym celu
klasy wytyczne, opracowane w wykładzie 7. Może warto
dodać, że kontenery STL, dostarczające operację indeksowania,
dostarczają również metodę dostępu ze sprawdzaniem: at(int)
.
Obsługa błędów
Załóżmy więc, że w pogramie mamy przynajmniej kilka linijek wykrywających potencjalne błędy. No i stało się. Wiemy już, że w programie wystąpił błąd, co teraz? Mamy wiele możliwości, wymienię tylko kilka z nich:
- Kończymy program z ewentualnym komunikatem o błędzie.
- Kończymy program, ale najpierw sprzątamy po sobie: zwalniamy zasoby, których nie zwolni system operacyjny, zapisujemy dane, itp.
- Staramy się kontynuować program pomimo błędu, próbując go poprawić, obejść lub zrezygnować z części funkcjonalności.
W tym wykładzie nie będzie interesować nas sama konkretna strategia, ale sposób rozdzielenia procesu wykrycia błędu od wyboru strategii. Jest to problem, który dotyczy każdego kodu, ale głównie funkcji bibliotecznych. Ich projektant/programista może wykryć, że w trakcie ich wykonywania wystąpiła nieprawidłowość, ale nie może jednak wiedzieć jak z takim błędem postąpić. To jest decyzja osoby korzystającej z tej funkcji. Musi więc istnieć jakiś mechanizm przekazywania tej informacji z wywoływanej funkcji na zewnątrz do funkcji wywołującej.
Najprostyszym sposobem jest zwrócenie jakiejś wartości sygnalizującej błąd. Jeśli funkcja nie zwraca żadnego wyniku, to jest to proste; jeśli jednak funkcja ma zwracać jakiś wynik, to nie zawsze da się znaleźć taką wartość, która by jednoznacznie mogła definiować błąd. Rozszerzenia tej metody, to zwrócenie informacji o przebiegu funkcji poprzez dodatkowy argument przekazywany przez referencje. Można też ustawiać i odczytywać jakieś zmienne stanu. Największą wadą tego podejścia jest konieczność każdorazowego sprawdzania tych wartości, co wymaga pisania dużej ilości trywialnego kodu. Z tego powodu sprawdzanie poprawności wywołania takich funkcji jest często opuszczane. W C++ dochodzi jeszcze niemożność zwrócenia wartości z konstruktora (choć oczywiście możemy ustawić w nim zmienną stanu informującą o powodzeniu konstrukcji).
Wyjątki
C++ dostarcza nowego mechanizmu, jakim są wyjątki. Polega on na tym, że funkcja która błąd wykryje i nie chce lub nie może go obsłużyć sama, rzuca wyjątek, który może być dowolnym obiektem. Rzucenie wyjątku powoduje natychmiastowe przerwanie wykonywania funkcji. Procedura wywołująca może ten wyjątek złapać. Wyjątek niezłapany prowadzi do zatrzymania programu, a więc wyjątki nie mogą zostać zignorowane. Zilustruję to na przykładzie naszego stosu, do którego dodam instrukcje rzucające wyjątki w przypadku przekroczenia zakresu (dla prostoty nie będę korzystał z klas wytycznych):
template<typename T = int , size_t N = 100> class Stack { private: T _rep[N]; size_t _top; public: Stack():_top(0) {}; void push(T val) { if(_top == N) { throw "pushing on top of the full stack"; } _rep[_top++]=val; } T pop() { if(is_empty()) { throw "poping form an empty stack"; } return _rep[--_top]; } bool is_empty() const {return (_top==0);} };
Polecenie throw
służy właśnie do rzucania wyjątków. W tym wypadku
rzucane są stałe napisowe, które będą automatycznie konwertowane na typ
const char *
.
Wykonanie programu:
main() { Stack<int,5> s; s.push(1); s.pop(); s.pop(); /* tu będzie rzucony wyjątek */ for(int i=0;i<10;i++) s.push(i); /* tu też gdyby udało się tu dojść */ }
spowoduje przerwanie programu w trakcie wykonywania drugiego polecenia
pop
. Komunikat, który się przy tym pojawia jest zależny
od implementacji.
Wyjątki można łapać, korzystając z bloku try
:
Stack<int,5> s; s.push(1); try { s.pop(); s.pop(); } catch(const char *msg) { std::cerr<<msg<<std::endl; } try { for(int i=0;i<10;i++) s.push(i); } catch(const char *msg) { std::cerr<<msg<<std::endl; }
W bloku try
umieszczamy instrukcje, które mogą potencjalnie rzucić
wyjątek. Za blokiem try
umieszczamy jedną lub więcej klauzul catch
,
które te wyjątki łapią. Wyjątek rzucony w bloku try
powoduje przekazanie
sterowania do pierwszej pasującej klauzuli catch
.
Wyjątki złapane
Przyjrzyjmy się teraz dokładniej mechanizmowi rzucania i łapania wyjątków. Rozważmy prosty przykład:
struct X { int val; X(int i=0):val(i) {cerr<<"constructing "<<val<<"";} X() {cerr<<"destructing "<<val<<endl;} }; void f() { X x(1); throw 0; cout<<"f"; }; main(){ X y(2); try { X z(3); f(); cout<<"try"; } catch(double){cout<<"zlapalem double-a";} catch(int){cout<<"zlapalem int-a";} catch(...){cout<<"zlapalem cos ";} cout<<"main"; }
Oto wynik wykonania tego programu:
constructing 2 constructing 3 constructing 1 destructing 1 destructing 3 zlapalem int-a main destructing 2
Co możemy zauważyć?
- Wyjątek przerwał wykonywanie funkcji
f()
i blokutry
, sterowanie zostało przekazana do klauzulicatch(int)
. - Przedtem wywołane zostały destruktory obiektów
x
iz
, czyli lokalnych obiektów w zasięgu blokutry
. Ten proces nazywamy "zwijaniem stosu". - Po wykonaniu klauzuli
catch
sterowanie zostało przekazane do następnego wyrażenia.
Klauzula catch(...)
wyłapuje każdy wyjątek. Jeśli np. pominiemy
klauzulę catch(int)
:
catch(double){cout<<"zlapalem double-a";} //catch(int){cout<<"zlapalem int-a";} catch(...){cout<<"zlapalem cos ";}
to wynikiem wywołania programu będzie:
constructing 2 constructing 3 constructing 1 destructing 1 destructing 3 zlapalem cos main destructing 2
Z tego przykładu widać też, że w przypadku dopasowywania klauzul
catch
nie następuje niejawna konwersja argumentów.
Niezłapane wyjątki
A co się stanie, jeśli wyjątku nie złapiemy? Żeby się o tym przekonać
usuniemy kolejną klauzulę catch
:
catch(double){cout<<"zlapalem double-a";} //catch(int){cout<<"zlapalem int-a";} //catch(...){cout<<"zlapalem cos ";}
Wynik programu jest teraz zupełnie inny:
constructing 2 constructing 3 constructing 1 terminate called after throwing an instance of 'int' Abort
Niezłapany wyjątek spowodował wywołanie funkcji abort()
, która
zakończyła program bez wywołania destruktorów. Ściśle rzecz
biorąc, niezłapany wyjątek wywołuje funkcję terminate()
, która z
kolei domyślnie wywołuje funkcję abort()
. Co do tego, czy wywoływane
są destruktory lokalnych obiektów (zwijanie stosu), to jest to
zachowanie zależne od implementacji. Jak widać, w implementacji
g++
w przypadku niezłapania wyjątku destruktory obiektów nie są
wywoływane.
Domyślne zachowanie funkcji terminate()
można zmienić, ustawiając
własną funkcję, za pomocą:
namespace std { typedef void (*terminate_handler)(void); terminate_handler set_terminate(terminate_handler new_terminate); }
Funkcja ustawiana w tym poleceniu nie może zwrócić sterowania, taka
próba kończy sie wywołaniem funkcji abort()
. Oznacza to, że funkcja
new_terminate()
musi kończyć się wywołaniem abort()
lub
exit()
.
void my_terminate() {std::cerr<<"terminating "<<std::endl;exit(1);} main() { std::set_terminate(my_terminate); throw 0; }
Wyjątki w destruktorach
Jeśli podczas opisanego powyżej procesu obsługi wyjątku, wywołana
zostanie funkcja, która sama wywoła wyjątek, to program zostanie
natychmiast przerwany wykonaniem funkcji terminate()
(nie dotyczy
to już funkcji wywoływanych wewnątrz klauzuli catch
). W
szczególności stanie się to, jeśli któryś z destruktorów wywoływanych w
trakcie zwijania stosu rzuci wyjątek:
struct X { ~X() { std::cerr<<std::uncaught_exception()<<""; throw 0;}; } ; main() { try { X x; } catch(int) {}; try { X x; throw 0; } catch(int) {}; }
W powyższym kodzie destruktor klasy X
jest wołany dwa razy. Po raz
pierwszy, podczas wychodzenia z pierwszego bloku try
. Jest to
normalne wywołanie spowodowane wyjściem poza zakres.
Destruktor rzuca wyjątek, który zostaje wyłapany przez
klauzulę catch(int)
na końcu bloku. Drugi raz destruktor jest
wołany jako część zwijania stosu po wyjątku rzuconym jawnie w drugim bloku
try
. Mimo że łapiemy wyjątki int
, i tak w tej sytuacji
wywoływana jest funkcja terminate()
, a w konsekwencji i
abort()
. Jest to jeden z powodów, dla których destruktory
nie powinny rzucać wyjątków. Funkcja uncaught_exception()
umożliwia rozróżnienie tych dwu kontekstów wywołania destruktora.
Zwraca ona prawdę, jeśli jakiś wyjątek jest właśnie obsługiwany.
Inne powody nie rzucania wyjątków z destruktorow wiążą się z dynamiczną alokacją pamięci i zostaną omówione w kolejnym wykładzie.
Hierachie wyjątków
Jako wyjątek może zostać wyrzucony dowolny obiekt. Umożliwia nam to grupowanie wyjątków w hierarchie za pomocą dziedziczenia. Zilustrujemy to za pomocą hierachii wyjątków z biblioteki standardowej, przedstawionej na rysunku 13.1.
Można z niej korzystać np. następująco:
main() { try { throw domain_error() ; } catch(invalid_argument &e) { cerr<<e.what()<<""; } catch(logic_error &e) { cerr<<"logic "<<e.what()<<""; } catch(exception &e) { cerr<<"some exception "<<e.what()<<""; } }
Kolejność klauzul catch
jest ważna, ponieważ klauzule są
sprawdzane po kolei i pierwsza, która pasuje, zostanie wykonana.
Gdybyśmy więc podali kaluzulę catch(Exception &e)
jako pierwszą,
przechwyciła by ona wszystkie standardowe wyjątki. Ważne jest też, aby
korzystając z hierarchii dziedziczenia, przechwytywać wyjątki przez
referencję. Inaczej nie zostaną wywołane poprawne funkcje wirtualne.
Zachęcam do eksperymentów z powyższym kodem.
Deklaracje wyjątków
C++ pozwala na deklarowanie listy możliwych wyjątków rzucanych z
funkcji. Służy do tego deklaracja throw(...)
umieszczana za
deklaracją listy argumentów funkcji, np.:
void no_throw(int) throw();
deklaruje, że funkcja no_throw
nie rzuci żadnego wyjątku, natomiast
void throw_std(int) throw(exception);
deklaruje, że funkcja throw_std
będzie rzucać tylko wyjątki ze
standardowej hierarchii. Brak deklaracji oznacza, że funkcja może rzucać
co chce.
Niestety, C++ nie dostarcza nam praktycznie żadnych mechanizmów, które by mogły wymusić konsystencję tych deklaracji. Rozważmy implementację funkcji:
void f(int i) {throw i;} void g() throw(int) {throw 0;}; void no_throw(int i) {f(i;};
Funkcja f
proklamuje całemu światu, że może rzucać wyjątek
(poprzez brak deklaracji, że nie może), podobnie funkcja g()
, a pomimo to kompilator nie
pozwala na to, aby wywołać ją wewnątrz funkcji, która jawnie deklaruje,
że wyjątku nie rzuci. Proszę to porównać np. z zastosowaniem
kwalifikatora const
: funkcja zadeklarowana jako const
nie
może wywołać funkcji, które const
nie są. W przypadku wyjątków
narzucenie takiej konsystencji spowodowałoby, że potrzeba by
przerabiać ogromne ilości kodu napisanego, zanim mechanizm wyjątków
stał się używany. Sprawia to niestety, że deklaracje wyjątków nie są
zbyt użyteczne, łatwo bowiem niechcący napisać kod, który je złamie. A
konsekwencje tego są poważne. Sprawdzania konsystencji w czasie
kompilacji wprawdzie nie ma, ale jest sprawdzanie w czasie wykonania.
Jeżeli funkcja rzuci wyjątek, który nie znajduje się na jej liście
zadeklarowanych wyjątków, to następuje wywołanie funkcji
unexpected()
, która domyślnie wywołuje abort()
. Nawet
złapanie tego wyjątku nic nie pomoże.
Kolejnym problemem są szablony funkcji i metody szablonów klas. Ich projektant nie może przewidzieć, z jakimi argumentami zostaną one konkretyzowane, a więc jakie wyjątki mogą zostać rzucone. Dlatego w szablonach lepiej deklaracje wyjątków pomijać. W ogóle, deklaracje wyjątków należy umieszczać tam, gdzie uważamy, że rzucenie każdego innego wyjątku jest przejawem poważnego błędu. Najczęściej jest to sytuacja, kiedy chcemy zadeklarować, że dana funkcja w ogóle nie rzuca wyjątków. Jak już pisałem taką własność powinny posiadać destruktory.
Niespodziewane wyjątki
Podobnie jak w przypadku funkcji terminate()
, możemy podstawić
własną funkcję unexpected()
. Podobnie jak terminate()
funkcja
unexpected()
nie zwraca sterowania, może za to sama rzucić
wyjątek. W ten sposób możemy jej użyć do "podmiany"
niespodziewanego wyjątku na inny. Zobaczmy jak to działa:
void f() throw(int) { throw "niespodzianka!"; }; main() { try { f(); } catch(...) {}; }
Ponieważ f()
rzuca const char *
, a deklaruje tylko int
-a,
powyższy program wywoła funkcję unexpected()
, która przerwie program.
Jeżeli podmienimy wyjątek poprzez ustawienie odpowiedniej funkcji:
void unexpected_handler() {throw 0;}; std::set_unexpected(unexpected_handler);
to zamiast const char *
zostanie rzucony int
i złapany przez
klauzulę catch(...)
. Tak się stanie ponieważ int
jest na
liście wyjątków funkcji f()
. Gdyby nie był, to zostałaby wywołana
funkcja terminate()
. Jeżeli jednak deklaracja wyjątków funkcji
f()
zawierać będzie wyjątek std::bad_exception
, to każdy
wyjątek rzucony przez unexpected()
i nie znajdujący się na liście
zadeklarowanych wyjątków funkcji f()
, jest podmieniany na
std::bad_exception()
:
void f() throw(int,std::bad_exception) { throw "niespodzianka!"; }; void unexpected_handler() {throw 3.1415926;}; main() { std::set_unexpected(unexpected_handler); try { f(); } catch(std::bad_exception ) {}; }
Wydajność wyjątków
Autor pozycji "Język C++ bardziej efektywny" S. Meyers sugerował, że użycie mechanizmu wyjątków może spowolnić program, nawet jeśli wyjątki nie będą rzucane. Ponieważ od tego czasu minęło dobrych kilka lat, postanowiłem sam sprawdzić, jak się sprawy mają. W tym celu skorzystałem z następujących programików:
double scin(double x,bool flag) { if(flag) throw 0; return sin(x); } main(){ volatile int f=0; double s=0.0; for(int i=0;i<100000000;++i) { try { s+=scin(rand()/(double)RAND_MAX,f); } catch(int) {}; } }
oraz
double scin(double x,bool flag) { if(flag) return 0; return sin(x); } main(){ volatile int f=0; double s=0.0; for(int i=0;i<100000000;++i) s+=scin(rand()/(double)RAND_MAX,f); }
Jak widać drugi z nich nie ma nawet śladu wyjątków. W pierwszym podejściu
ustawilem flagę f
na zero, co powodowało, że żaden wyjątek nie był rzucany.
Czas wykonania obu programów (w sekundach) podany jest w poniższej tabelce:
nie rzucane wyjątki | bez wyjątków | rzucane wyjątki | return | |
-O0 | 15 | 15 | ||
-O1 | 13 | 4 | ||
-O2 | 13 | 4 | ||
-O3 | 4 | 4 | 600 | 4 |
Porównując kolumny 1 i 2, widać, że dla pełnej optymalizacji nie ma
żadnej różnicy. Szczegółowe badanie wykazało, że to włączenie opcji
-finline-functions
powoduje skok prędkości pomiędzy dwoma
ostatnimi wierszami w pierwszej kolumnie. Ten sam efekt można uzyskać,
dodając do funkcji scin
kwalifikator inline
.
Następnie porównałem koszt zwykłego powrotu z funkcji z kosztem rzucenia wyjątku. Wyniki są przedstawione w dwóch ostatnich kolumnach tabelki. Tu widać dramatyczną różnicę: obsługa wyjątku jest ponad 100 razy wolniejsza.