Obsługa wyjątków [ exception handling ]
Transkrypt
Obsługa wyjątków [ exception handling ]
Obsługa wyjątków [ exception handling ] • • • piszemy kod, wykonujący oczekiwane zadania kod obsługujący sytuacje niepożądane otacza kod pożądany • większa czytelność kodu • łatwiejsza kontrola nad błędami dowolnego wywołania danej funkcji • nie można zignorować zgłoszonego wyjątku zgłaszany (throw) wyjątek może być dowolnego typu (wbudowanego, abstrakcyjnego), zwykle używa się specjalnie napisanych klas class UserError { const char* const errInfo; public: UserError(const char* const msg = 0) : errInfo(msg) {} }; void fun() { // funkcja wyrzuci wyjątek typu UserError throw UserError("jestem beznadziejną funkcją"); } int main() { // na razie bez bloku try fun(); } Obsługa wyjątków [ co się dzieje - wymagania ] Zgłaszanie wyjątku będącego obiektem klasy - ograniczenia na rodzaj klas, których można użyć do tworzenia obiektów wyjątków • • • • posiada odpowiedni dostępny konstruktor do utworzenia obiektu wyjątku posiada dostępny konstruktor kopiujący posiada dostępny destruktor klasa nie jest klasą abstrakcyjną Etapy zgłoszenia wyjątku będącego obiektem klasy 1. wyrażenie throw tworzy obiekt tymczasowy - wywołanie odpowiedniego konstruktora klasy 2. utworzenie kopii obiektu tymczasowego - (wywołanie konstruktora kopiującego klasy) utworzenie obiektu reprezentującego obiekt wyjątku w celu przekazania go do procedury obsługi 3. usunięcie obiektu tymczasowego - (wywołanie destruktora) wykonywane przed przystąpieniem do szukania procedury obsługi • • • zwracany jest obiekt (wyjątek) przez wartość i następuje wyjście z danego bloku zasięgu lub z funkcji można definiować wyrzucenie dowolnie wielu typów obiektów obiekty lokalne utworzone do czasu wystąpienia wyjątku są usuwane Obsługa wyjątków [ przechwytywanie wyjątku ] • • • • • • obsłużenie wyjątków wymaga wstawienia kodu rzucającego wyjątki do bloku try, zgłoszone wyjątki obsługiwane są zaraz za tym blokiem dla każdego typu wyjątku potrzebna procedura obsługi wyjątku try { // tu może zostać rzucony wyjątek } catch (typA id1) { // obsługa wyjątku typu typA } catch (typB id2) { // obsługa wyjątku typu typB } catch (typC) { // jak widać tutaj nie ma identyfikatora // widocznie do obsługi wyjątku typu typC // wystarczy sama informacja o złapanym typie wyjątku } wejście do bloku catch oznacza obsłużenie wyjątku (co wcale nie musi oznaczać rozwiązanie problemu…) wykonana jest tylko jedna pasująca fraza catch program jest kontynuowany za blokiem try możliwe warianty: zakończenie lub kontynuacja (trzeba, po obsłużeniu wyjątku, jawnie wywołać funkcję która wygenerowała wyjątek - czyli np. blok try z funkcją w jakiejś pętli) Obsługa wyjątków [ przykład sytuacyjny ] class A { public: A() : a_(++c) { cerr << "ctorA:" << a_ << ' '; } A(const A& aa) : a_(++c) { cerr << "cctorA:" << a_ << ' '; } ~A() { cerr << "dtorA:" << a_ << ' '; } int a() const { return a_; } protected: int a_; static int c; // licznik powstałych obiektów }; int A::c = 0; void fun1a() { throw A(); } int main() { // fun1a(); - ctorA:1 terminate called after throwing an instance of 'A' Abort try { fun1a(); } // poniżej: ctorA:1 wyjatekA:1 dtorA:1 catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; } // poniżej: ctorA:1 cctorA:2 wyjatekA:2 dtorA:2 dtorA:1 } // catch (const A a) { cerr << "wyjatekA:" << a.a() << ' '; } Obsługa wyjątków [ dopasowanie wyjątków ] • • • • • • szukanie najbliższej procedury obsługi wyjątków według kolejności bloków catch w kodzie nie musi zajść dokładna zgodność typów aby nie tworzyć kolejnej kopii obiektu wyjątku zamiast przechwycenia wartości lepiej korzystać ze (stałej) referencji podczas dopasowywania nie są przeprowadzane automatyczne konwersje typów class A {}; class B { public: B(const A&) {} /* konstruktor konwertujący */ }; void fun() { throw A(); } int main() { try { fun(); } catch (B&) { // nie, bo nie zajdzie konwersja } catch (A&) { // pasujący typ } } obiekt lub referencja do obiektu klasy pochodnej pasują do procedury catch dotyczącej klasy bazowej (pamiętajmy o "przycinaniu" obiektu jeśli przekazywany jest przez wartość) kolejność bloków catch: od szczegółowego (klasa pochodna) do ogólnego (klasa bazowa) Obsługa wyjątków [ dopasowanie wyjątków ] • Wyjątek należący do klasy pochodnej może być obsłużony przez klauzulę catch przeznaczoną dla wyjątków klasy podstawowej // class A - tak jak poprzednio dwa slajdy wcześniej class B : public A { public: B() { cerr << "ctorB:" << a_ << ' '; } B(const B& bb) : A(bb) { cerr << "cctorB:" << a_ << ' '; } ~B() { cerr << "dtorB:" << a_ << ' '; } }; void fun1b() { throw B(); } int main() { try { fun1b(); } // kompilator ostrzeże, że pierwsza klauzula przechwyci wyjątek // przez referencję ctorA:1 ctorB:1 wyjatekA:1 dtorB:1 dtorA:1 // przez wartość ctorA:1 ctorB:1 cctorA:2 wyjatekA:2 dtorA:2 dtorB:1 dtorA:1 catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; } catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; } } Obsługa wyjątków [ dopasowanie wyjątków ] • Wybór klauzuli obsługi wyjątku według zasady pierwszego dopasowania obsługi dokona pierwsza napotkana klauzula mogąca obsłużyć wyjątek, a nie najlepiej dopasowana int main() { try { fun1b(); } // przez referencję ctorA:1 ctorB:1 wyjatekB:1 dtorB:1 dtorA:1 // przez wartość ctorA:1 ctorB:1 cctorA:2 cctorB:2 wyjatekB:2 dtorB:2 dtorA:2 // dtorB:1 dtorA:1 catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; } catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; } } • Podczas tworzenia obiektu wyjątku nie bada się aktualnego typu obiektu void fun2() { B b; A* a = &b; throw *a; } try { fun2(); } catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; } // klauzula poniżej: ctorA:1 ctorB:1 cctorA:2 dtorB:1 dtorA:1 wyjatekA:2 dtorA:2 catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; } Obsługa wyjątków [ łap cokolwiek, ponownie rzuć ] • procedura przechwytująca wyjątek dowolnego typu catch (…) { // dowolny wyjątek złapany, czyli taka fraza powinna być ostatnia // nic nie wiemy o typie, można zwolnić zasoby i ponownie rzucić wyjątek throw; // przechodzi do procedur obsługi wyjątków wyższego poziomu } • ponowne zgłoszenie wyjątku powoduje zgłoszenie pierwotnego obiektu reprezentującego wyjątek (czyli "rzucenie" go dalej) void fun3() { try { fun1b(); } // przypominam: void fun1b() { throw B(); } catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; throw; } } try { fun3(); } // przez referencję: ctorA:1 ctorB:1 wyjatekA:1 wyjatekB:1 dtorB:1 dtorA:1 // przez wartość: ctorA:1 ctorB:1 wyjatekA:1 cctorA:2 cctorB:2 wyjatekB:2 // dtorB:2 dtorA:2 dtorB:1 dtorA:1 catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; } catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; } Obsługa wyjątków [ sytuacja bez wyjścia ] • • • • wyjątek nie przechwycony na żadnym poziomie woła funkcję biblioteczną terminate() (plik nagłówkowy <exception>) funkcja terminate() jest wołana również gdy – destruktor obiektu lokalnego rzuci wyjątek podczas obsługi wyjątku – wyjątek zgłosi konstruktor lub destruktor obiektu statycznego lub globalnego domyślnie funkcja terminate() wywołuje funkcję abort() kończącą natychmiast działanie programu (core dump – można "debugować") – nie są wywołane destruktory obiektów globalnych i statycznych własna funkcja obsługi takiej sytuacji set_terminate() jako argument przyjmuje wskaźnik do bezargumentowej i nic nie zwracającej nowej funkcji obsługi, zwraca wskaźnik do poprzedniej funkcji obsługi (można ją odtworzyć) void newTerminate() { exit(0); /* powinna zakończyć działanie programu */ } void (*oldTerminate)() = set_terminate(newTerminate); // żeby ewentualnie odtworzyć class A { public: class B(); void fun() { throw B(); } wyjątek rzucony w destruktorze podczas ~A() { throw 'c'; } likwidacji obiektu przy obsłudze wyjątku }; powoduje wywołanie funkcji terminate(), int main() { w tym przypadku newTermiate() try { A a; a.fun(); } catch (...) { /* nic */ } } Obsługa wyjątków [ polimorficzne wołanie funkcji ] • wykorzystanie metod wirtualnych obiektów reprezentujących wyjątki class A { // ... jak poprzednio public: virtual void obslugaWyj() const { cerr << "Obsluga WyjatekA:" << a_ << ' '; } }; class B : public A { // ... jak poprzednio public: virtual void obslugaWyj() const { cerr << "Obsluga WyjatekB:" << a_ << ' '; } }; try { fun1b(); } // jak poprzednio, rzuca wyjątek typu B catch (const A& a) { a.obslugaWyj(); } // przez referencję – polimorfizm! // ctorA:1 ctorB:1 Obsluga WyjatekB:1 dtorB:1 dtorA:1 // przez wartość: // ctorA:1 ctorB:1 cctorA:2 Obsluga WyjatekA:2 dtorA:2 dtorB:1 dtorA:1 // obiekt "przycięty" do klasy A, nie ma polimorfizmu Obsługa wyjątków [ sprzątanie ] • gwarantowane jest, że przy wychodzeniu z zasięgu, dla obiektów, których konstruktory zostały wykonane do końca, wywołane zostaną destruktory class C { public: C() : c(++cs) { cerr << "ctorC:" << c << ' '; } ~C() { cerr << "dtorC:" << c << ' '; } protected: problem: int c; • jeśli wyjątek zostanie rzucony w konstruktorze static int cs; i nie wykona się on do końca, to destruktor }; nie jest wołany, zasoby zaalokowane int C::cs = 0; w konstruktorze na stercie, przed rzuceniem void fun4sub() { wyjątku, nie zostaną zwolnione C c2; rozwiązania: fun1a(); // rzuca wyjątek typu A • przechwycić wyjątek w konstruktorze i zwolnić C c3; w nim zasoby } • stosować technikę pozyskiwania zasobów void fun4() { w ramach inicjalizacji (RAII – Resource Acquisition C c1; Is Initialization), alokacja w konstruktorze, fun4sub(); zwalnianie w destruktorze – przykład: unique_ptr, C c4; czyli zdobycie zasobu wiąże się z budową } jakiegoś obiektu try { fun4(); } // ctorC:1 ctorC:2 ctorA:1 dtorC:2 dtorC:1 Obsluga WyjatekA:1 dtorA:1 catch (const A& a) { a.obslugaWyj(); } Obsługa wyjątków [ na poziomie funkcji ] Blok try funkcji służy do ochrony listy inicjowania składowych w konstruktorach klas (tzw. try funkcyjny) int fun5(int i) { if (!i) throw A(); return 2*i; } class D { public: D(int d0) try : d(fun5(d0)) { fun1b(); } catch (const A& a) { cerr << "ctorD-"; a.obslugaWyj(); } D(int d0, int) : d(fun5(d0)) { try { fun1b(); } catch (const A& a) { cerr << "ctorD-"; a.obslugaWyj(); } } protected: int d; }; try { D d(1,0); } catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); } // ctorA:1 ctorB:1 // ctorD-WyjatekB:1 dtorB:1 dtorA:1 try { D d(1); } catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); } patrz wyjaśnienie dalej // ctorA:1 ctorB:1 // ctorD-WyjatekB:1 main()-WyjatekB:1 dtorB:1 dtorA:1 try { D d(0,0); } catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); } // ctorA:1 main()-WyjatekA:1 dtorA:1 try { D d(0); } catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); } // ctorA:1 // ctorD-WyjatekA:1 main()-WyjatekA:1 dtorA:1 każda funkcja może mieć… ale lepiej wewnątrz… int main() try { throw ”mój wyjatek main”; } catch (const char* msg) { cout << msg << endl; return 1; // tu może zakończyć jak funkcja } Obsługa wyjątków [ blok try – wyjątek w konstruktorze ] Obiekt – zaczyna istnieć gdy konstruktor zakończy się pomyślnie (tzn. dojdziemy do końca ciała konstruktora lub do instrukcji return; ) Obiekt – kończy istnienie gdy rozpoczyna się jego destruktor. Sposobem zgłoszenia błędu konstrukcji jest zgłoszenie wyjątku. Zgłoszenie wyjątku przez konstruktor oznacza, że obiekt nie został skonstruowany i nie istnieje. Procedura obsługi funkcyjnego bloku try w konstruktorze lub destruktorze musi zakończyć się zgłoszeniem wyjątku – jeśli nie zakończy się jawnym zgłoszeniem wyjątku (oryginalnego lub jakiegoś nowego) i sterowanie osiągnie koniec bloku catch konstruktora lub destruktora, to oryginalny wyjątek jest automatycznie zgłoszony ponownie, tak jakby ostatnią instrukcją procedury było throw; Nie można sprawić, aby wyjątek zgłoszony przez konstruktory podobiektu bazowego lub składowego nie wyciekł poza zawierający je konstruktor. W języku C++ jeśli konstrukcja dowolnego podobiektu bazowego lub składowego się nie uda, to nie może się też udać konstrukcja całego obiektu. Jedynym zastosowaniem funkcyjnego bloku try konstruktora jest translacja wyjątku zgłoszonego przez podobiekt bazowy lub składowy. Jednak niewielka z tego korzyść, ponieważ trzeba pamiętać, że w procedurze obsługi funkcyjnego bloku try konstruktora wszystkie zmienne lokalne ciała konstruktora są już poza zasięgiem i żaden podobiekt bazowy ani składowy już nie istnieje! (… więc nie można „zwolnić” żadnych alokowanych zasobów). Obsługa wyjątków [ blok try – wyjątek w konstruktorze ] • • opcjonalnie można poinformować jakie wyjątki rzuca funkcja specyfikacja uzupełnia deklarację funkcji i pojawia się za listą argumentów • rzucenie wyjątku innego niż zadeklarowany powoduje wywołanie funkcji unexpected(), która domyślnie woła funkcję terminate() można ustawić własną obsługę takiej sytuacji poprzez podanie funkcji set_unexpected() adresu bezparametrowej funkcji typu void, można zachować wskaźnik do poprzedniej funkcji obsługi sytuacji nieoczekiwanej • void fun(); // ta funkcja może zgłosić dowolny wyjątek void fun() throw(A, B, UserErr); // tu mogą być zgłoszone wyjątki typu A, B, UserErr void fun() throw(); // funkcja deklaruje, że nie rzuci żadnego wyjątku C++98/03 void fun() noexcept; // funkcja deklaruje, że nie rzuci żadnego wyjątku C++11/14 • w klasach pochodnych, redefiniując funkcje składowe, nie można dodawać do listy wyjątków żadnych nowych typów – ale można podać mniej lub żaden • jeśli nie wiemy (nie jesteśmy pewni) jakie wyjątki mogą się pojawić, nie używajmy ich specyfikacji (vide: biblioteka standardowa C++ i szablony klas – wyjątki znane są opisane w dokumentacji, a pozostałe zależą od użytkownika) Obsługa wyjątków [ specyfikacje wyjątków ] – – – możliwe jest ponowne rzucenie wyjątku, jeśli jest to wyjątek z zadeklarowanej listy, przeszukiwanie podejmowane jest od nowa (od wywołania funkcji z taką specyfikacją wyjątków) jeśli jest to znowu wyjątek nie z deklarowanej listy, wywołana jest funkcja terminate() chyba, że na liście wyjątków jest również wyjątek std::bad_exception void fun() throw(A, B, C, bad_exception); to wyjątek nie przewidziany do rzucenia z danej funkcji zastępowany jest obiektem bad_exception i przeszukiwanie podejmowane jest od funkcji j.w. class A {}; class B {}; void myUnexpected() { throw invalid_argument(”O jej!”); } // void myUnexpected() { throw bad_cast(); } – ponownie zgłosi wyjątek nie z listy void myTerminate() { cout << ”Bez wyjscia: terminate” << endl; exit(1); } void fun1() throw ( A, invalid_argument ) { throw B(); } // void fun1() throw ( A, invalid_argument, bad_exception ) { throw B(); } – tu jest bad_exception, więc… int main() { set_unexpected( myUnexpected ); set_terminate( myTerminate ); try { fun1(); } catch( const exception& e) { cout << e.what() << endl; } } Obsługa wyjątków [ wyjątki standardowe ] Hierarchia klasy wyjątków w bibliotece standardowej C++ pliki nagłówkowe <exception>, <stdexcept> • • • klasa bazowa exception, klasy pochodne logic_error, runtime_error, bad_cast, bad_alloc, bad_exception, bad_typeid, ios_base::failure błędy logiczne: klasa bazowa logic_error, klasy pochodne invalid_argument, out_of_range, length_error, domain_error błędy wykonania: klasa bazowa runtime_error, klasy pochodne range_error, overflow_error, underflow_error class exception { public: exception() throw() { } virtual ~exception() throw(); virtual const char* what() const throw(); }; class tab { public: explicit tab(int val = 0) { for(int i = 0; i < 3; ++i) tab_[i] = val; } int& operator[] (int i) { if (i < 0 || i > 2) throw out_of_range("zły indeks tab"); return tab_[i]; try { tab t(3); t[1] = t[4]; } } catch (const exception& e) { cerr << e.what(); } protected: // zły indeks tab int tab_[3]; }; Obsługa wyjątków [ wyjątki standardowe ] Kod odporny na wyjątki [ gwarancje ] gwarancja podstawowa – po zgłoszeniu wyjątku nie nastąpi żaden wyciek pamięci, a obiekty pozostaną w stanie niekoniecznie przewidywalnym, ale umożliwiającym zniszczenie obiektów, gwarancja ta jest odpowiednia dla kodu radzącego sobie z nieudanymi operacjami, które już zmieniły stan obiektów gwarancja silna – po zgłoszeniu wyjątku stan obiektu pozostaje niezmieniony, oznacza to semantykę „zatwierdź lub cofnij” oraz to, że żadne referencje lub iteratory do kontenera nie zostaną unieważnione, gdy operacja się nie powiedzie gwarancja niezgłaszania wyjątków – funkcja nie zgłosi wyjątku bez względu na okoliczności, czasem jest niemożliwa implementacja silnej lub nawet podstawowej gwarancji, jeśli nie mamy gwarancji, że pewne funkcje (np. destruktory i funkcje zwalniające pamięć) nie zgłoszą wyjątku Kod odporny na wyjątki [ c-tor z gwarancją podstawową ] #include <iostream> #include <stdexcept> using namespace std; class Device { public: Device(int devno) { if (devno == 2) throw runtime_error(”Problem!”); } ~Device() {} }; class Broker { public: Broker(int devno1, int devno2) : dev1_(0), dev2_(0) { try { dev1_ = new Device(devno1); dev2_ = new Device(devno2); } catch ( … ) { delete dev1_; // bo po dev1_ mógł się nie udać tylko dev2_ throw; } } ~Broker() { delete dev1_; delete dev2_; } private: Broker(); Device* dev1_; Device* dev2_; }; int main() { try { Broker b( 1, 2 ); } catch (exception & e) { cerr << ”Wyjatek: ” << e.what() << endl; } }