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; }
}