Jeżeli wyjątek wystąpi w
konstruktorze, nawet jeżeli obiekt jest tylko częściowo stworzony standard gwarantuje, że stworzone już dotąd obiekty zostaną poprawnie zniszczone. Tak samo w przypadku gdy wyjątek wystąpi w momencie inicjalizacji tablicy albo kontenera.
Jeżeli
destruktor działa na operacji, która może zniszczyć obiekt, powinien on z-wrapować taką operację w blok
try i obsłużyć wyjątek lokalnie.
Destruktory nie powinny rzucać wyjątków! Jeżeli podczas zwijania stosu destruktor rzuci wyjątkiem którego sam nie złapał program zostanie zterminowany.
#include <iostream>
using namespace std;
struct MyClass {
~MyClass() /* noexcept */ {
throw std::exception();
}
};
int main() {
MyClass a;
return 0;
}
Wynik:
terminate called after throwing an instance of 'std::exception'
what(): std::exception
The program has unexpectedly finished.
Kilka zasad dotyczących tworzenia, rzucania i łapania wyjątków:
- Obiekty wyjątków, które są rzucane, muszą mieć dostępny destruktor, a także konstruktor kopiujący lub konstruktor move.
- Najbardziej wyspecjalizowany catch musi pojawić się jako pierwszy.
- Aby wyrzucić wyjątek wyżej, trzeba podać throw bez wyrażenia (throw;).
- Aby złapać wyjątek każdego typu trzeba skorzystać z catch(...), który powinien zawsze pojawiać się jako ostatni.
#include <iostream>
#include <typeinfo>
#include <stdexcept>
using namespace std;
void old() {
throw std::runtime_error("old()");
}
void young() {
try {
old();
} catch(const std::bad_cast& e) {
cout << e.what() << endl;
} catch(...) {
cout << "unknown, rethrow" << endl;
throw;
}
}
int main() {
try {
young();
} catch(const std::runtime_error& e) {
cout << "in main: " << e.what() << endl;
}
return 0;
}
Wynik:
unknown, rethrow
in main: old()
Lista inicjalizacyjna konstruktora
Konstruktor nie może złapać wyjątków rzuconych z listy inicjalizacyjnej. Aby się przed tym ustrzec, blok
try musi znajdować się przed dwukropkiem. Zachowanie jest podobne w działaniu do zwykłych
try-catch, z tą różnicą że złapany wyjątek zostanie ponownie wyrzucony z bloku.
#include <iostream>
#include <stdexcept>
using namespace std;
struct Member {
Member() { }
Member(Member& m) {
throw std::runtime_error("Constructor");
}
};
struct BadCalss {
Member member;
BadCalss(Member& m) try : member(m) {
// body
} catch(...) {
cout << "some bug" << endl;
}
};
int main() {
Member m;
BadCalss bc(m);
return 0;
}
Wynik:
some bug
terminate called after throwing an instance of 'std::runtime_error'
what(): Constructor
The program has unexpectedly finished.
noexcept
W nowym standardzie pojawił się specjalny specyfikator
noexcept. Jeżeli wiemy, że funkcja nie będzie rzucać wyjątku, może z tej informacji skorzystać i programista i kompilator. Wspominał o tym również Scott Meyers w swoim
wykładzie na Going Native 2013. Kompilator w takim przypadku wygeneruje znacznie mniej kodu, który będzie bardziej optymalny w działaniu.
#include <iostream>
#include <stdexcept>
using namespace std;
void fun() noexcept {
throw std::runtime_error("fun()");
}
// gun() ma taki sam specyfikator rzucanych typów jak fun()
void gun() noexcept(noexcept(fun())) {
cout << "gun()" << endl;
}
int main() {
fun();
return 0;
}
Wynik:
terminate called after throwing an instance of 'std::runtime_error'
what(): fun()
The program has unexpectedly finished.
W rezultacie
noexpect powinno być stosowane w dwóch przypadkach.
Kiedy jesteśmy przekonani, że funkcja nie rzuci wyjątku albo/i gdy nie chcemy obsługiwać błędu. Jeżeli z funkcji (oznaczonej jako
noexcept) zostanie jednak rzucony wyjątek, natychmiast zawołane zostanie
std::terminate().
Inny przykład, w którym wyjątek przelatuje przez funkcję z
noexcept, co powoduje zterminowanie programu.
#include <iostream>
#include <stdexcept>
using namespace std;
void fun_a() {
throw std::runtime_error("bug");
}
void fun_b() noexcept {
fun_a();
}
void fun_c() {
try {
fun_b();
} catch(...) {
cout << "Exception caught" << endl;
}
}
int main() {
fun_c();
}
Wynik:
terminate called after throwing an instance of 'std::runtime_error'
what(): bug
The program has unexpectedly finished.
Niektóre kontenery jak np.
std::vector, sprawdzają (podczas realokacji wektora), czy "move konstruktor" jest oznaczony jako
noexcept. Jeżeli tak nie jest zostanie zwołany zwykły konstruktor kopiujący, co jest operacją dużo wolniejszą. Odpowiedni eksperyment stworzyłem w innym
artykule.
Poza "move konstruktorem"
noexcept standardowo powinno znaleźć się również w deklaracji destruktora.
Stare - throw()
W poprzedniej wersji standardu istniało słowo kluczowe
throw(), które obecnie jest uznawane za przestarzałe. Można było za jego pomocą wyspecyfikować jakiego rodzaju wyjątki mogą być wyrzucone z funkcji, albo (odpowiednik
noexpect) że funkcja nie rzuci żadnego wyjątku.
int fun(std::string& b) throw();