errnoopen() (Posix-Funktion)exit() (stdlib.h) oder vorzeitiges return in main()interrno
errno hat eine Reihe von Problemen
Globale Variable aus errno.h
Kodiert nur den jeweils letzten Fehlergrund (automatische Überschreibung)
→ Prüfung wird sehr leicht vergessen, Grund ist dann ggf. bei Folgefehlern nicht mehr nachvollziehbar
Wertekodierung ist abhängig von der Zielplattform
→ andere Fehlercodes unter Linux/UNIX als unter Windows
errno und Rückgabewerte existieren in C++ natürlich weiterhinopen()
Es gibt allerdings noch weitere Wege
std::expected – nicht Thema in diesem KapitelAusnahmebehandlung ist tief in C++ verankert
new und delete können Ausnahmen werfenKonzept hinter Exceptions ist relativ simpel
Stattdessen: Signalisierung eines Fehlerfalls an aufrufende Funktion
→ „Heißes Eisen wird weggeschoben und ist das Problem des Vorgesetzten”
try: Beginn eines Code-Blocks, in dem eine Ausnahme ausgelöst werden könntethrow: Werfen einer Ausnahmecatch: Code-Block, der potenziell die Ausnahme behandeln kannBetreten des catch-Blocks, wenn die Signatur der Ausnahme passt
Ansonsten: An den Nächsten weiterwerfen
→ rethrow
Achtung: Beim Werfen wird die bisherige Funktion verlassen
Der Reihe nach catch-Handler ablaufen
Falls ein Ausnahmetyp auf einen Handler passt, wird er verwendet
→ Nachfolgende Handler werden ignoriert
Kein passender Handler?
→ Aufwärtstraversierung der Aufrufkette zu darüberliegenden Funktionen
Falls auf der Ebene ein try-Block existiert → Schritt 1, sonst Schritt 3
std::terminate()
main()throw geworfen werdencatch vorhanden sein
...)
printf)#include <iostream>
#include <string>
using std::cout, std::endl;
class Error {
std::string msg;
public:
Error(std::string s) : msg(s) {}
const char* message() { return msg.c_str(); }
};
void throws_char_ptr() {
try {
throw "pointer error";
}
catch(int i) { // Kein Matching gegen geworfenen Datentyp -> weiterreichen
cout << i << endl;
}
}
void throws_class() {
try {
throw Error("class error");
}
catch(Error e) {
cout << "Handled in function catch: " << e.message() << endl;
}
}
int main() {
cout << "main(): enter" << endl;
try {
throws_class();
throws_char_ptr();
}
catch(...) {
cout << "Caught unhandled exception" << endl;
}
cout << "main(): exit" << endl;
}
new und delete ebenfalls erlaubt (problematisch)Allgemeine Herangehensweise bei Ausnahmen:
Throw by value, catch by reference
Der Hintergrund ist simpel
Throw by value stellt sicher, dass keine Speicherlecks entstehen
catch by reference erlaubt Zugriff auf Methoden der Kinder einer gefangenen Klasse
(Polymorphie statt versehentlicher Typkonvertierung)
std::exceptionstd::exception
what()const char* what() const noexcept override;std::exception#include <iostream>
#include <string>
#include <exception>
using std::cout, std::endl;
using std::exception;
class MyException: public exception {
public:
const char* what() const noexcept override {
return "MyException :)";
}
};
class IntError: public exception {
public:
const char* what() const noexcept override {
return "IntError";
}
};
int main() {
try {
try {
throw IntError();
} catch (IntError& me) { // Fängt aber nicht MyException
cout << me.what() << endl;
} catch (exception& e) { // Alle Klassen vom Typ std::exception -> MyException
cout << e.what() << endl;
}
} catch (...) {
// Zur Sicherheit, falls etwas übersehen wurde
cout << "Caught unhandled exception" << endl;
}
}
Ermöglicht deutlich bessere und übersichtlichere catch-Blöcke als generische Ellipse
throw geworfen werden#include <iostream>
#include <string>
#include <exception>
using std::cout, std::endl;
using std::exception;
class MyException: public exception {
public:
const char* what() const noexcept override {
return "MyException :)";
}
};
void foo() {
try {
throw MyException();
} catch (MyException& me) {
cout << "ME: " << me.what() << endl;
throw; // Rethrow für generischen Handler
}
}
int main() {
try { foo(); }
catch (exception& e) {
cout << "E: " << e.what() << endl;
}
}
finally in anderen ProgrammiersprachenAusnahmebehandlung in vielen populären Programmiersprachen ebenfalls üblich
z.B. Java, Python oder Javascript
Unterschied: Nach den catch-Blöcken ein Block mit Schlüsselwort finally
Funktional wie ein Destruktor für die Ausnahmebehandlung
→ Wird in jedem Fall ausgeführt, unabhängig vom catch-Block
Existiert in C++ nicht, wird aber garantiert einmal an anderer Stelle auftauchen
throw auftreten könnte#include <iostream>
using std::cout, std:: endl;
class Member {
public:
Member() { cout << "MC" << endl; }
~Member() { cout << "MD" << endl; }
};
class Class {
Member member;
public:
Class() {
cout << "CC" << endl;
throw 4711;
}
~Class() { cout << "CD" << endl; }
};
int main() {
try {
Class c;
}
catch (int i) {
cout << "Caught " << i << endl;
}
}
Kritisch wird es erst bei den Destruktoren
try-Block auch hier erlaubt, aber…
Es gilt: Destruktoren werden am Ende eines Scopes aufgerufen, beim Stack Unwinding
Tritt hier eine Ausnahme auf
→ C++-Runtime beendet das Programm ohne zu zögern
Konsequenz: Niemals throw im Destruktor propagieren
catch muss an Ort und Stelle geschehenExceptions im Destruktor am besten vermeiden
#include <iostream>
using std::cout, std:: endl;
class Member {
public:
Member() { cout << "MC" << endl; }
~Member() { cout << "MD" << endl; }
};
class Class {
Member member;
public:
Class() { cout << "CC" << endl; }
~Class() {
cout << "CD" << endl;
try { throw 4711; }
catch (int i) {} // So ok
}
};
int main() {
try {
Class c;
}
catch (int i) {
cout << "Caught " << i << endl;
}
}
noexceptnoexcept zu markieren
void foo() noexcept { /* ... */ };std::vector):
noexcept → bei Move-Konstruktor: Rückfall auf Copy-Konstruktor 🙁 (deutlich langsamer)nullptr gesetzt sein (→ ggf. doppeltes free(), darf niemals passieren)noexcept#include <iostream>
#include <vector>
#include <utility>
using std::cout, std::endl;
class Foo {
int id_;
public:
Foo(int id) : id_(id) { }
Foo(const Foo & other) : id_(other.id_){
cout << "Copy Foo" << id_ << endl;
}
Foo(Foo && other) noexcept : id_(other.id_) {
cout << "Move Foo" << id_ << endl;
}
};
int main() {
Foo foo1(1);
Foo foo2(2);
Foo foo3(3);
// std::vector anfangs leer
std::vector<Foo> vec; // vec.capacity() == 0
vec.push_back(std::move(foo1)); // == 1
// Internes malloc(vec.capacity() * 2)
// Anschließendes Verschieben aus altem Speicher in neu allokierten Bereich
// Verschieben ohne noexcept mittels Copy statt Move
vec.push_back(std::move(foo2));
// Kapazität (2) voll -> Verdoppelung auf 4 und wieder ein Verschieben
vec.push_back(std::move(foo3));
}
Long story short: Annotiert ungefährliche Move-Konstruktoren mit noexcept
deletetrycatchthrowcatch-Anweisungen ist wichtig