CodeGym /Kurse /C++ SELF /error_code‑overload

error_code‑overload

C++ SELF
Level 64, Lektion 3
Verfügbar

1. Zwei API‑Stile in std::filesystem

Wenn Sie Code schreiben, der mit dem Dateisystem arbeitet, leben Sie plötzlich in einer Welt, in der die Außenwelt jederzeit „nein“ sagen kann. Eine Datei kann gelöscht worden sein, ein Verzeichnis umbenannt, Zugriffsrechte können das Lesen verhindern — und das ist nicht immer eine „Katastrophe“, manchmal ist es ein regulärer Logikzweig. Wenn Sie für jede solche Situation eine Ausnahme werfen und fangen, wird der Code nervös, laut und ähnelt einer Serie, bei der jede Folge mit dramatischer Musik endet.

Ein Beispiel für einen „erwarteten Fehler“: Sie möchten eine temporäre Datei löschen. Sie könnte nicht existieren — das ist normal, Sie wollten nur aufräumen. Oder Sie prüfen, ob ein Verzeichnis existiert: es kann fehlen, dann erstellen Sie es. In solchen Szenarien sind Ausnahmen oft übertrieben.

Werfende Funktionen und error_code‑Überladungen

In std::filesystem gibt es für viele Operationen zwei API‑Stile.

Der erste ist „werfend“: wenn etwas schiefgeht, wirft die Funktion eine Ausnahme (gewöhnlich std::filesystem::filesystem_error).

Der zweite ist „nicht werfend“: die Funktion nimmt eine Referenz auf ein std::error_code, schreibt dort den Fehler hinein und wirft keine Ausnahme. Dieser Stil ist genau dann passend, wenn der Fehler im Szenario erwartet ist. Die Idee der Überladungen mit error_code ist ein normaler Teil des Designs von filesystem.

Das sieht meist so aus: es gibt die Funktion fs::exists(p) (werfende Form) und daneben fs::exists(p, ec) (Form „Fehler als Wert“). Gleiches gilt für viele andere Operationen.

Die Idee ist einfach: Sie wählen den Fehlerbehandlungsstil passend zur Aufgabe. Wenn ein Fehler unerwartet ist (z. B. „konnte das wichtige Arbeitsverzeichnis der Anwendung nicht erstellen“), können Ausnahmen sinnvoll sein. Wenn der Fehler erwartbar ist (z. B. „die temporäre Datei könnte fehlen“), ist die error_code‑Überladung oft bequem.

Mini‑Anatomie von std::error_code

std::error_code ist ein kleines Objekt, das intern einen „Fehlercode“ und eine „Kategorie“ speichert (vereinfacht: woher der Fehler stammt und wie man ihn interpretiert). Im Alltag sind drei Dinge wichtig: kann man prüfen, ob ein Fehler vorliegt, kann man ihn zurücksetzen, und kann man eine menschenlesbare Meldung erhalten. Genau dafür nutzt man typischerweise: if (ec), ec.clear() und ec.message().

Ein wichtiger psychologischer Punkt: std::error_code ist keine Ausnahme, es „stürzt“ niemanden. Es liegt einfach daneben und sagt: „wenn du fragst, erzähle ich, was schiefgelaufen ist“. Das hat den Vorteil, dass Sie selbst entscheiden, wann ein Fehler kritisch ist und wann er nur Information ist.

Kleine Illustration (nicht filesystem‑bezogen, sondern nur um den Stil zu fühlen):

#include <iostream>
#include <system_error>

int main() {
    std::error_code ec;             // ec ist leer (kein Fehler)
    std::cout << std::boolalpha;

    std::cout << "has error: " << static_cast<bool>(ec) << '\n'; // has error: false

    ec = std::make_error_code(std::errc::permission_denied);
    std::cout << "has error: " << static_cast<bool>(ec) << '\n'; // has error: true
    std::cout << "message: " << ec.message() << '\n';            // message: Permission denied (Formulierung hängt vom OS ab)
}

In std::filesystem werden Sie ec häufiger von der Operation selbst erhalten, statt ihn manuell zu erzeugen.

Werfende Operationen und try/catch

Ausnahmen in filesystem verhalten sich logisch: Sie rufen eine Operation auf — wenn sie scheitert, kommt eine Ausnahme, Sie fangen sie. Für wirklich „außerplanmäßige“ Situationen ist das in Ordnung. Aber wenn Sie viele kleine Prüfungen/Aktionen durchführen, verwandelt sich der Code in einen Teppich aus try/catch, und dem Leser möchte man manchmal zum Fenster rauslaufen lassen (vorsichtig, durch std::filesystem::path).

Typisches Beispiel für den „werfenden“ Stil:

#include <filesystem>
#include <iostream>

int main() {
    namespace fs = std::filesystem;

    try {
        fs::rename("missing.txt", "moved.txt");
        std::cout << "renamed\n"; // renamed
    } catch (const fs::filesystem_error& e) {
        std::cerr << "filesystem_error: " << e.what() << '\n';
    }
}

Das Problem ist nicht, dass das „schlecht“ wäre. Das Problem ist, dass dieser Stil für erwartete Fehler zu „laut“ ist. Wenn nach Szenario eine Datei fehlen kann, wollen Sie nicht jedes Mal eine Mini‑Ermittlung starten à la „wer hat missing.txt gestohlen“.

error_code‑Stil: Basisschablone

Mit error_code‑Überladungen schreiben Sie verzweigteren Code, aber dafür ohne Ausnahmen und mit transparentem Fluss. Die Schlüsselkompetenz ist, die Reihenfolge strikt einzuhalten: zuerst prüfen wir ec, und erst dann interpretieren wir das bool/Ergebnis. Denn einige Funktionen liefern im Fehlerfall eine Antwort, die „wie ein normales Ergebnis“ aussieht (z. B. false) — und ohne ec würden Sie nicht wissen, ob das „nicht vorhanden“ oder „nicht überprüfbar“ bedeutet.

Das Muster sieht üblicherweise so aus:

#include <filesystem>
#include <iostream>
#include <system_error>

int main() {
    namespace fs = std::filesystem;

    std::error_code ec;
    fs::rename("missing.txt", "moved.txt", ec);

    if (ec) {
        std::cerr << "rename error: " << ec.message() << '\n';
        return 1;
    }

    std::cout << "renamed\n"; // renamed (wenn erfolgreich)
}

Hier ist wichtig: die rename‑Funktion in der error_code‑Form wirft keine Ausnahme, sondern füllt einfach ec.

Wichtiger Unterschied: false bedeutet nicht immer einen Fehler

Bei std::filesystem gibt es eine klassische „Falle“ für Einsteiger: einige Operationen geben ein bool zurück, wobei false „nichts geändert“ bedeuten kann — und das kann vollkommen normal sein. Der eigentliche Fehler liegt separat in ec. Genau deshalb sollte die Reihenfolge „zuerst ec, dann bool“ zur Gewohnheit werden.

Ein typisches Beispiel ist remove. Es gibt true zurück, wenn tatsächlich etwas gelöscht wurde, und false, wenn nichts zu löschen war. Das muss kein Fehler sein.

#include <filesystem>
#include <iostream>
#include <system_error>

int main() {
    namespace fs = std::filesystem;

    std::error_code ec;
    bool removed = fs::remove("maybe_tmp.log", ec);

    if (ec) {
        std::cerr << "remove error: " << ec.message() << '\n';
        return 1;
    }

    std::cout << std::boolalpha;
    std::cout << "removed: " << removed << '\n'; // removed: false (wenn die Datei nicht existierte)
}

Die Idee: removed == false kann bedeuten „Datei fehlte, alles ok“. Dagegen bedeutet ec, dass die Operation nicht korrekt ausgeführt werden konnte.

Am Beispiel von exists: „existiert nicht“ vs. „konnte nicht geprüft werden“

Die Funktion exists wirkt sehr simpel: Pfad existiert oder nicht. Die Realität ist komplexer: „konnte nicht geprüft werden“ ist ein dritter Zustand. Zum Beispiel fehlen Zugriffsrechte auf das Verzeichnis, in dem die Datei liegt, oder der Pfad ist aus Sicht des OS ungültig. Im werfenden Stil wäre das eine Ausnahme. Im error_code‑Stil zeigt sich das als ec != 0.

Und hier ist die Falle: exists(p, ec) kann im Fehlerfall false zurückgeben. Also bedeutet false sowohl „nicht vorhanden“ als auch „konnte nicht geprüft werden“. Daher müssen Sie ec prüfen.

#include <filesystem>
#include <iostream>
#include <system_error>

int main() {
    namespace fs = std::filesystem;

    std::error_code ec;
    bool ok = fs::exists("some/path", ec);

    if (ec) {
        std::cerr << "exists error: " << ec.message() << '\n';
        return 1;
    }

    std::cout << std::boolalpha << "exists: " << ok << '\n';
}

Wenn Sie das if (ec) vergessen, bekommen Sie ein Programm, das bei Zugriffsproblemen sagt „naja, wahrscheinlich existiert es nicht“ — und Sie werden an der falschen Stelle debuggen.

Disziplin im Umgang mit ec: alte Fehler nicht weitertragen

std::error_code ist eine normale Variable. Das heißt, sie kann einen alten Wert behalten. Das klingt offensichtlich, ist aber eine der häufigsten Ursachen für seltsame Bugs: Sie machen eine Operation, sie schreibt einen Fehler hinein, dann machen Sie eine zweite Operation (die eventuell nicht das erwartete Feld in ec berührt), und Sie prüfen ec und denken, der Fehler käme von der zweiten Operation.

Es gibt zwei gute Stile.

Der erste Stil ist, für jede Operation ein eigenes ec direkt daneben zu erzeugen:

std::error_code ec;
bool ok = fs::create_directories(dir, ec);
if (ec) { /* behandeln */ }

Der zweite Stil ist, ein einzelnes ec zu verwenden, aber vor jedem Aufruf ec.clear() aufzurufen:

ec.clear();
fs::rename(from, to, ec);
if (ec) { /* behandeln */ }

Für Einsteiger ist der erste Stil meist einfacher: geringeres Risiko, den Kontext zu verwechseln.

3. Beispiel: Mini‑Utility NoteVault

Damit es nicht so wirkt, als sei das alles nur Theorie, bauen wir eine einfache Konsolenanwendung ein. Nennen wir sie Mini‑Utility NoteVault: Notizen werden in einem Verzeichnis notes/ gespeichert, jede Notiz ist eine normale Textdatei. Früher hätten wir Dateien mit ifstream/ofstream gelesen/geschrieben; heute geht es darum, Pfade und Dateioperationen sicher zu handhaben: Verzeichnis erstellen, Existenz prüfen, umbenennen, löschen — und das Fehlen einer Datei nicht zur Tragödie machen.

Wir machen drei kleine Funktionen: den Pfad zum Notizenverzeichnis bekommen, sicherstellen, dass das Verzeichnis existiert, und eine Notiz „sanft“ löschen (wenn sie nicht existiert, ist das kein Fehler).

Basis: create_directories ohne Ausnahmen

Wir starten mit der Funktion EnsureNotesDir, die bei Bedarf notes/ erstellt. Ein Fehler hier kann tatsächlich wichtig sein: wenn Sie das Arbeitsverzeichnis überhaupt nicht erstellen können, kann die Anwendung nicht vernünftig arbeiten.

#include <filesystem>
#include <iostream>
#include <system_error>

namespace fs = std::filesystem;

fs::path NotesDir() {
    return fs::path{"notes"};
}

bool EnsureNotesDir(std::error_code& ec) {
    ec.clear();
    fs::create_directories(NotesDir(), ec);
    return !ec;
}

Beachten Sie: create_directories gibt ein bool zurück (erstellt oder schon vorhanden), aber in unserer Logik ist wichtiger, ob es insgesamt gelungen ist. Daher gibt die Funktion !ec zurück.

„Sanftes Löschen“: Datei nicht vorhanden = normaler Ausgang

Jetzt die Löschfunktion. Die Idee: der Benutzer kann eine Notiz zum Löschen anfordern, aber die Datei kann schon weg sein. Das ist kein Grund zum Absturz, sondern bedeutet einfach „nichts zu löschen“.

#include <filesystem>
#include <iostream>
#include <system_error>

namespace fs = std::filesystem;

bool RemoveNote(std::string_view name, std::error_code& ec) {
    ec.clear();

    fs::path p = NotesDir() / fs::path{name};
    p.replace_extension(".txt");

    bool removed = fs::remove(p, ec);
    if (ec) return false;

    return removed; // true = gelöscht, false = nicht vorhanden
}

Ein netter Bonus ist fs::path: wir ersetzen die Erweiterung über replace_extension, statt per Hand nach dem Punkt in der Zeichenkette zu suchen. Das spart mindestens einen Abend Arbeit.

Umbenennen einer Notiz im error_code‑Stil

Umbenennen ist eine Operation, bei der Fehler erwartbar sind: die Quell‑Datei kann fehlen; die Ziel‑Datei kann bereits existieren; es kann Zugriffsprobleme geben. Wir wollen eine sinnvolle Meldung ausgeben, statt Ausnahmen zu fangen.

#include <filesystem>
#include <system_error>
#include <string_view>

namespace fs = std::filesystem;

bool RenameNote(std::string_view from, std::string_view to, std::error_code& ec) {
    ec.clear();

    fs::path pFrom = NotesDir() / fs::path{from};
    pFrom.replace_extension(".txt");

    fs::path pTo = NotesDir() / fs::path{to};
    pTo.replace_extension(".txt");

    fs::rename(pFrom, pTo, ec);
    return !ec;
}

Der ganze Vertrag steht an einer Stelle: wenn ec leer ist — wurde umbenannt.

Skelett von main: Aktionen und verständliche Meldungen

Jetzt schreiben wir ein sehr einfaches main, das kein vollständiger CLI‑Parser ist (das ist ein eigenes großes Thema), aber bereits den normalen Stil zeigt: Aktion → ec prüfen → Meldung an den Nutzer.

#include <filesystem>
#include <iostream>
#include <string>
#include <system_error>

namespace fs = std::filesystem;

fs::path NotesDir();
bool EnsureNotesDir(std::error_code& ec);
bool RemoveNote(std::string_view name, std::error_code& ec);
bool RenameNote(std::string_view from, std::string_view to, std::error_code& ec);

int main() {
    std::error_code ec;

    if (!EnsureNotesDir(ec)) {
        std::cerr << "Cannot create notes dir: " << ec.message() << '\n';
        return 1;
    }

    bool removed = RemoveNote("temp", ec);
    if (ec) {
        std::cerr << "Remove failed: " << ec.message() << '\n';
        return 1;
    }
    std::cout << std::boolalpha << "Removed temp: " << removed << '\n'; // Removed temp: false

    bool renamed = RenameNote("draft", "done", ec);
    if (!renamed) {
        std::cerr << "Rename failed: " << ec.message() << '\n';
        return 1;
    }
    std::cout << "Renamed draft -> done\n"; // Renamed draft -> done

    return 0;
}

Ja, das ist zunächst direkt. Aber daraus sieht man bereits das Wesentliche: wir fangen keine Ausnahmen für erwartete Situationen, verlieren aber keine diagnostischen Meldungen.

4. Wie wählen: Ausnahmen oder error_code?

Es gibt kein universelles „immer so machen“. Aber es gibt eine sehr praktische Regel, die in Lehrprojekten und realen Projekten gut funktioniert.

Wenn ein Fehler bedeutet, dass „wir das Szenario nicht normal weiterführen können“ und es wirklich eine außerplanmäßige Situation ist, können Ausnahmen praktisch sein: der Code bleibt linear, und Sie fangen das Problem „auf der höchsten Ebene“. Wenn dagegen der Fehler Teil der normalen Logik ist (Datei kann fehlen, Verzeichnis kann unzugänglich sein, Nutzer kann sich im Namen vertippen), dann geben die error_code‑Überladungen Ihnen ruhigen, kontrollierten Code ohne ständige try/catch.

Im Rahmen dieses Themas üben wir genau den zweiten Ansatz: eine Anwendung zu bauen, die nicht erwartet, dass die Außenwelt perfekt ist.

5. Typische Fehler im Umgang mit error_code in std::filesystem

Fehler Nr. 1: nur das bool prüfen und ec ignorieren.
Das passiert oft bei exists und remove: Sie sehen false und schließen auf „nicht vorhanden“ oder „nicht gelöscht“. Das kann aber „konnte nicht geprüft werden“ oder „Zugriffsfehler“ bedeuten. Wenn Sie ec nicht prüfen, deaktivieren Sie die Diagnostik und hinterlassen ein Rätsel im Code.

Fehler Nr. 2: dasselbe std::error_code wiederverwenden und das Zurücksetzen vergessen.
ec wird nicht automatisch gelöscht „weil Sie das so wollten“. Wenn eine alte Fehlermeldung drinbleibt, kann die nächste Prüfung if (ec) fälschlich auslösen, und Sie werden eine unschuldige Operation beschuldigen.

Fehler Nr. 3: Ausnahmen und error_code im selben Szenario mischen.
Technisch ist das möglich, aber schwer lesbar und wartbar. An einer Stelle fällt der Code durch eine Ausnahme, an einer anderen schreibt er still in ec. Ein Einsteiger kommt frisch herein und geht leicht gealtert wieder heraus. Üblicherweise ist es besser, in einem Szenario einen Stil zu wählen.

Fehler Nr. 4: nur ec.message() ausgeben, ohne Kontext.
Die Fehlermeldung ist nützlich, aber ohne Pfad und Beschreibung der Aktion oft wenig aussagekräftig. Ein guter Minimalstandard ist, „was versucht wurde“ und „mit welchem Pfad“. Sonst wird das Log zu einer Sammlung rätselhafter Sätze wie „Permission denied“, als wäre es ein Türsteher im Club.

Fehler Nr. 5: mit error_code kritische Fehler „unter den Teppich“ kehren zu wollen.
error_code ist kein Mittel, um so zu tun, als gebe es keine Probleme. Es ist ein Mittel, erwartete Probleme ordentlich zu behandeln. Wenn die Anwendung das Arbeitsverzeichnis nicht erstellen oder eine wichtige Datei nicht schreiben kann, sollte das normalerweise explizit gemeldet und die Anwendung beendet werden, statt weiter in einem beschädigten Zustand zu laufen.

1
Aufgabe
C++ SELF, Level 64, Lektion 3
Gesperrt
Fehlersignal
Fehlersignal
1
Aufgabe
C++ SELF, Level 64, Lektion 3
Gesperrt
Geisterdatei entfernen
Geisterdatei entfernen
1
Aufgabe
C++ SELF, Level 64, Lektion 3
Gesperrt
Zwei Arten von false
Zwei Arten von false
1
Aufgabe
C++ SELF, Level 64, Lektion 3
Gesperrt
Erwarteter Fehler
Erwarteter Fehler
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION