EMZETT.
Login

Kurz: C++ meldet Fehler über Ausnahmen (exceptions). Sie wandern die Aufrufkette hoch, bis jemand sie fängt. Dabei werden alle lokalen Objekte ordentlich zerstört (RAII):

Teil des Kurses C++

Kapitel 10 von 15 im Kurs C++. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Ausnahmen

C++ meldet Fehler über Ausnahmen (exceptions). Sie wandern die Aufrufkette hoch, bis jemand sie fängt. Dabei werden alle lokalen Objekte ordentlich zerstört (RAII):

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
 
double teile(double a, double b) {
    if (b == 0) throw std::invalid_argument("Division durch 0");
    return a / b;
}
 
int main() {
    try {
        std::cout << teile(10, 4) << '\n';
        std::cout << teile(1, 0) << '\n';
    } catch (const std::invalid_argument& e) {
        std::cout << "Fehler: " << e.what() << '\n';
    }
 
    try {
        std::vector<int> v{1, 2, 3};
        v.at(10);
    } catch (const std::out_of_range& e) {
        std::cout << "out_of_range\n";
    }
 
    try {
        std::stoi("abc");
    } catch (const std::exception& e) {                     // allgemeinster Fänger zuletzt
        std::cout << "allgemein: " << e.what() << '\n';
    }
    return 0;
}

Ausgabe:

2.5
Fehler: Division durch 0
out_of_range
allgemein: stoi

Fange Ausnahmen per const-Referenz. Die wichtigsten Typen stehen in <stdexcept>: std::runtime_error, std::logic_error, std::invalid_argument, std::out_of_range, std::length_error, alle erben von std::exception.

Eigene Ausnahmen

#include <iostream>
#include <stdexcept>
#include <string>
 
class GuthabenFehler : public std::runtime_error {
public:
    GuthabenFehler(double fehlbetrag)
        : std::runtime_error("Nicht genug Guthaben"), fehlbetrag_(fehlbetrag) {}
    double fehlbetrag() const { return fehlbetrag_; }
private:
    double fehlbetrag_;
};
 
double abheben(double stand, double betrag) {
    if (betrag > stand) throw GuthabenFehler(betrag - stand);
    return stand - betrag;
}
 
int main() {
    try {
        std::cout << abheben(100, 30) << '\n';
        std::cout << abheben(100, 130) << '\n';
    } catch (const GuthabenFehler& e) {
        std::cout << e.what() << ", es fehlen " << e.fehlbetrag() << '\n';
    }
    return 0;
}

Ausgabe:

70
Nicht genug Guthaben, es fehlen 30

RAII und Ausnahmen

Weil Destruktoren beim Aufräumen laufen, braucht C++ kein finally:

#include <iostream>
#include <memory>
#include <stdexcept>
 
struct Verbindung {
    Verbindung() { std::cout << "öffne\n"; }
    ~Verbindung() { std::cout << "schließe\n"; }
};
 
void arbeite() {
    Verbindung v;                         // wird bei Ausnahme automatisch geschlossen
    std::cout << "arbeite ...\n";
    throw std::runtime_error("kaputt");
}
 
int main() {
    try { arbeite(); } catch (const std::exception& e) { std::cout << "gefangen: " << e.what() << '\n'; }
    return 0;
}

Ausgabe:

öffne
arbeite ...
schließe
gefangen: kaputt

Fehler ohne Ausnahmen: optional, variant, expected

Nicht jeder Fehler ist außergewöhnlich. Wenn “kein Ergebnis” ein normaler Fall ist, passen Typen besser:

#include <iostream>
#include <optional>
#include <string>
#include <variant>
#include <charconv>
 
std::optional<int> parse(const std::string& s) {
    int wert{};
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), wert);
    if (ec == std::errc{} && ptr == s.data() + s.size()) return wert;
    return std::nullopt;
}
 
using Antwort = std::variant<int, std::string>;          // entweder int oder string
 
int main() {
    auto a = parse("42"), b = parse("x42");
    std::cout << a.value_or(-1) << ' ' << b.value_or(-1) << ' ' << a.has_value() << b.has_value() << '\n';
    if (auto z = parse("7")) std::cout << "ok " << *z << '\n';
 
    Antwort r = 12;
    Antwort e = std::string("Fehler");
    for (const Antwort& x : {r, e}) {
        std::visit([](const auto& w) { std::cout << "Wert: " << w << '\n'; }, x);
    }
    std::cout << std::holds_alternative<int>(r) << std::get<int>(r) << '\n';
    return 0;
}

Ausgabe:

42 -1 10
ok 7
Wert: 12
Wert: Fehler
112
WerkzeugWann
std::optional<T>”vielleicht ein Wert” (Suche, Parsen)
std::variant<A, B>genau eine von mehreren Möglichkeiten
std::expected<T, E> (C++23)Ergebnis oder Fehler ohne Ausnahme
Ausnahmenseltene, ernsthafte Fehler (Datei fehlt, Netzwerk, ungültiger Zustand)
Rückgabecode + [[nodiscard]]Schnittstellen zu C und Echtzeitcode

assert und Verträge

#include <cassert>
#include <iostream>
 
int teile(int a, int b) {
    assert(b != 0 && "Divisor darf nicht 0 sein");     // Annahme (nur in Debug-Builds)
    return a / b;
}
 
int main() {
    std::cout << teile(10, 2) << '\n';
    return 0;
}

Ausgabe:

5

assert prüft Programmierfehler (verletzte Annahmen), nicht Benutzerfehler. Mit -DNDEBUG verschwinden die Prüfungen. static_assert prüft zur Übersetzungszeit.

Regeln

  • Ausnahmen für Fehler, die der direkte Aufrufer meist nicht behandeln kann
  • Destruktoren dürfen keine Ausnahme nach außen werfen (noexcept ist Standard)
  • Fange nur, was du behandeln kannst; nicht catch (...) überall
  • Gib in Ausnahmen aussagekräftige Meldungen mit

Merke

  • try { } catch (const Typ& e) { }, throw Typ(...); Typen aus <stdexcept>
  • Eigene Ausnahmen erben von std::runtime_error oder std::exception
  • RAII ersetzt finally: Destruktoren räumen bei Ausnahmen auf
  • std::optional, std::variant, std::expected bilden erwartbare Fehler im Typ ab
  • assert für Annahmen, static_assert für Prüfungen beim Übersetzen

Übungsaufgabe

Schreibe std::optional<double> wurzel(double x), das für negative Zahlen nullopt liefert, und rufe es mit mehreren Werten auf.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Templates und Concepts

Weiter: Dateien, Streams und Dateisystem

Alle Kapitel: C++ im Überblick