CodeGym /Kurse /C++ SELF /Zuverlaessiges Laden: Strategie fail fast vs best effort

Zuverlaessiges Laden: Strategie fail fast vs best effort

C++ SELF
Level 63, Lektion 4
Verfügbar

1. Drei Schichten des zuverlaessigen Ladens

Wenn ein Einsteiger zum ersten Mal Dateiladen hinzufuegt, malt das Gehirn ein einfaches Bild: „geoeffnet → gelesen → benutzt“. In Wirklichkeit verhält sich eine Datei oft wie eine „Kiste mit Kabeln aus alten Programmversionen, einer Katze und dem Universum“: dort koennen leere Zeilen, kaputte Eintraege, seltsame Zeichen und unerwartete Formate auftauchen. Deshalb denken wir schrittweise in Schichten, sonst beginnen Fehler im Projekt wie Zugvoegel zu wandern.

Die Logik ist praktisch im Kopf als eine „Verantwortungskette“ aus drei Schichten zu halten:

  • I/O: wir lesen physikalisch Bytes/Zeilen aus der Datei und pruefen, dass der Stream noch lebt.
  • Parsing: wir verwandeln die Zeile in Felder (z. B. id, done, title).
  • Validierung: wir ueberpruefen, dass die Felder fuer unser Modell sinnvoll sind und die Invarianten nicht brechen.

Unten eine praktische Schemaansicht (das wird auch unser „Ladevertrag“ sein):

flowchart TD
    A[Datei oeffnen] --> B{Wurde Stream geoeffnet?}
    B -- nein --> X[Fehler beim Oeffnen]
    B -- ja --> C[Zeilenweise lesen getline]
    C --> D{Wurde Zeile gelesen?}
    D -- nein, EOF --> Z[Erfolg: Dateiende]
    D -- nein, Fehler --> Y[Lesefehler]
    D -- ja --> E[Zeile in Felder parsen]
    E --> F{Parsing erfolgreich?}
    F -- nein --> G[Fehler: Eintragsformat]
    F -- ja --> H[Invarianten validieren]
    H --> I{Gueltig?}
    I -- nein --> J[Fehler: semantische Datenprobleme]
    I -- ja --> K[Eintrag in vector einfuegen]
    K --> C

2. Mini‑Modell fuer Beispiele: Aufgabenliste

Um nicht „in der Luft“ zu erklaeren, nehmen wir ein einfaches Modell, aehnlich dem, was Sie schon oft in Konsolenaufgaben umgesetzt haben: eine Aufgabenliste (todo). Sie ist einfach genug, um nicht abzulenken, und lebendig genug, um Parsing, Validierung und verschiedene Fehlerpolitiken zu demonstrieren.

Im Speicher haben wir folgendes Modell:

#include <string>

struct Task {
    int id{};
    bool done{};
    std::string title;
};

Vereinbaren wir das Dateiformat (textbasiert, „eine Zeile = ein Eintrag“):

id;done;title

Wobei done entweder 0 oder 1 ist. Zum Beispiel:

1;0;Buy milk
2;1;Learn C++ streams
3;0;Stop writing while(!eof())

Warum habe ich ; gewaehlt? Weil Leerzeichen im title fuer uns wichtig sind, und „Tokenlesen via >>“ genau diese Leerzeichen abschneidet. Deshalb lautet die Logik: „ganze Zeile lesen → innerhalb zerlegen“.

3. Parsen und Validierung einer Zeile

Sorgfaeltige Zerlegung der Zeile

Bevor wir darueber streiten „fail fast oder best effort“, muessen wir eine stabile Methode haben, eine Zeile zu zerlegen. Hier lauert eine psychologische Falle: es scheint, Parsing seien einfach „drei Felder“. Wenn Sie aber Parsing, Validierung und Dateilesen am selben Ort mischen, werden Sie sich nach einer Woche selbst als Uebersetzer fuer einen alten Kodex fuehlen.

Wir schreiben eine kleine Funktion, die „die Zeile in drei Teile spaltet“. Fuer ein textbasiertes Format mit Trenner ist es praktisch, std::getline innerhalb eines std::stringstream zu verwenden:

#include <sstream>
#include <string>

bool split_task_line(const std::string& line,
                     std::string& id_s,
                     std::string& done_s,
                     std::string& title) {
    std::stringstream ss(line);
    return std::getline(ss, id_s, ';')
        && std::getline(ss, done_s, ';')
        && std::getline(ss, title);
}

Wichtig ist hier: das dritte getline(ss, title) liest „bis zum Ende“, also kann title Leerzeichen enthalten — und das ist gewollt. Wenn die Zeile nicht zwei ; enthaelt, gibt die Funktion false zurueck, und wir melden ehrlich: „Format ist kaputt“.

Zahlen und Flags mit from_chars parsen

Jetzt der Schritt, der Ihr Laden deutlich robuster macht: sauberes Parsen von Zahlen ohne Ausnahmen. Wir wissen, dass stoi bei schlechten Eingaben zu Laufzeitfehlern fuehren kann. Beim Dateiladen ist „schlechte Eingabe“ keine Ausnahme, sondern ein normaler Fall: die Datei kann manuell veraendert sein, von einer anderen Programmversion zerstoert worden sein oder einfach leer sein.

Wir verwenden std::from_chars: es wirft keine Ausnahmen, sondern liefert einen Fehlercode. Das ist ideal fuer „zuverlaessiges Laden“.

#include <charconv>
#include <optional>
#include <string_view>

std::optional<int> parse_int(std::string_view s) {
    int value = 0;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec != std::errc{} || ptr != s.data() + s.size()) return std::nullopt;
    return value;
}

Beachte die kleine Pruefung von ptr != end: sie filtert Strings wie "12abc" heraus. Andernfalls haetten Sie angeblich erfolgreich 12 gelesen, waehrend der Rest still ignoriert worden waere — das ist eine der schlimmsten Fehlerklassen: „Das Programm laeuft, aber liefert falsche Ergebnisse“.

Fuer done machen wir etwas Aehnliches, aber mit einem eingeschraenkten Wertebereich:

#include <optional>
#include <string_view>

std::optional<bool> parse_done(std::string_view s) {
    if (s == "0") return false;
    if (s == "1") return true;
    return std::nullopt;
}

Validierung der Invarianten

Wenn die Zeile erfolgreich in drei Felder geparst wurde, seufzt man gern auf und denkt: „Hurra, der Eintrag ist fertig“. Das ist aber nur die formale Fertigstellung; wir muessen noch pruefen, ob er auch inhaltlich tauglich ist. Das ist die Validierung.

Die inhaltlichen Regeln (Invarianten) fuer unsere Task sind: id muss groesser als null sein, der Titel darf nicht leer sein und der Titel darf nicht zu lang sein.

Wir schreiben eine Validierungsfunktion, die einen Fehlertext (oder einen leeren String) zurueckgibt:

#include <string>

std::string validate_task(const Task& t) {
    if (t.id <= 0) return "id must be > 0";
    if (t.title.empty()) return "title must not be empty";
    if (t.title.size() > 200) return "title is too long";
    return "";
}

Warum ein String und kein bool? Weil ein bool ohne Nachricht fuer die Diagnose kaum hilfreich ist. Eine Meldung, besonders mit der Zeilennummer, spart Ihnen Stunden Arbeit.

Funktion parse_task_line: Parsing + Validierung

Nun koennen wir alles in einer Funktion zusammenfassen, die eine Zeile nimmt und entweder ein Task erstellt oder ein Problem zurueckgibt. Im einfachen Stil zeigen wir die Variante mit std::optional<Task> und einer separaten Fehlerstring — das ist praegnant und kompiliert fast ueberall.

#include <optional>
#include <string>

std::optional<Task> parse_task_line(const std::string& line, std::string& error) {
    std::string id_s, done_s, title;
    if (!split_task_line(line, id_s, done_s, title)) {
        error = "bad format, expected: id;done;title";
        return std::nullopt;
    }

    auto id = parse_int(id_s);
    auto done = parse_done(done_s);
    if (!id || !done) {
        error = "bad number in id/done";
        return std::nullopt;
    }

    Task t{.id = *id, .done = *done, .title = title};
    error = validate_task(t);
    if (!error.empty()) return std::nullopt;

    return t;
}

Ja, das sind ein paar mehr als 10 Zeilen, aber beachten Sie das Prinzip: wir trennen explizit „Format“, „Zahlenparsing“ und „Invarianten“. Wenn etwas kaputt geht, wissen Sie genau, was passiert ist, statt nur zu sehen „irgendwas lief schief, viel Glueck“.

4. Fehlerbehandlungsstrategien

Fail fast und best effort: was sie sind und wann anwenden

An dieser Stelle moechte man oft fragen: „Welcher Ansatz ist richtig?“ Fail fast und best effort sind keine Werturteile, sondern zwei Politiken, die je nach Aufgabe ausgewaehlt werden. Das Problem von Einsteigern ist, dass die Politik oft zufaellig entsteht: „es hat sich so ergeben“. Wir werden stattdessen bewusst waehlen.

Vergleichen wir sie in einer Tabelle, damit der Unterschied nicht abstrakt bleibt:

Politik Was tun bei erstem schlechten Eintrag Was wird zurueckgegeben Wann praktisch
Fail fast Wir stoppen das Laden und betrachten die Datei als inkorrekt „Erfolg mit allen Daten“ oder „Fehler“ Konfigurationen, kritische Daten, Formatmigrationsszenarien, wenn partielle Daten gefaehrlich sind
Best effort Wir ueberspringen den fehlerhaften Eintrag und machen weiter Liste valider Eintraege + Report ueber Probleme Logs, Import von Nutzerdaten, „schmutzige“ Dateien, wenn es besser ist, etwas zu zeigen

Wichtig: selbst bei best effort sollte man normalerweise I/O‑Fehler nicht ignorieren. Wenn der Stream mitten drin wegen Festplattenproblemen zusammenbricht, hilft „Zeile ueberspringen“ nicht. Best effort bezieht sich auf fehlerhafte Eintraege, nicht auf einen zerstoerten Inputstream an sich.

Fail fast: Implementierung des Loaders

Fail fast ist leichter zu testen und einfacher dem Nutzer zu erklaeren: „Datei ist kaputt, Laden abgebrochen“. Das ist praktisch, wenn die Datei Teil Ihres Vertrags ist: sie muss komplett korrekt sein.

Wir schreiben einen Loader, der zeilenweise liest, parst und beim ersten Problem mit einer klaren Fehlermeldung und Zeilennummer aufhoert:

#include <fstream>
#include <optional>
#include <string>
#include <vector>

std::optional<std::vector<Task>> load_tasks_fail_fast(const std::string& filename,
                                                      std::string& error) {
    std::ifstream in(filename);
    if (!in) { error = "cannot open file"; return std::nullopt; }

    std::vector<Task> tasks;
    std::string line;
    int line_no = 0;

    while (std::getline(in, line)) {
        ++line_no;
        if (line.empty()) continue;

        std::string local_error;
        auto t = parse_task_line(line, local_error);
        if (!t) { error = "line " + std::to_string(line_no) + ": " + local_error; return std::nullopt; }

        tasks.push_back(*t);
    }

    if (!in.eof()) { error = "read error"; return std::nullopt; }
    return tasks;
}

Beachten Sie das if (!in.eof()) nach der Schleife. Wir haben bereits erlaeutert, dass getline nicht nur bei EOF stoppt. Diese Pruefung ist Ihr „Ehrlichkeitssensor“: sie laesst Sie nicht versehentlich einen Lesefehler als normales Dateiende akzeptieren.

Wenn Ihr Compiler C++23 std::expected unterstuetzt, passt die Signatur hier sehr schoen zu „entweder Wert oder Fehler“. Der Header <expected> ist im Standard vorgesehen, seine Verfuegbarkeit haengt aber von Standardbibliothek und Compiler-Version ab.

Best effort: Implementierung des Loaders und Report

Best effort waehlt man meist dort, wo die Daten „von Natur aus schmutzig“ sind: ein Nutzer exportiert CSV aus Excel, editierte per Hand, verlor einen Trenner, und viele Zeilen sind defekt. Best effort sagt: „Ich habe 93 Aufgaben geladen, 7 Zeilen uebersprungen — hier die Gruende“.

Reportstruktur:

#include <string>
#include <vector>

struct LoadReport {
    std::vector<Task> tasks;
    std::vector<std::string> problems;
};

Und der Loader selbst:

#include <fstream>
#include <string>

LoadReport load_tasks_best_effort(const std::string& filename) {
    std::ifstream in(filename);

    LoadReport r;
    if (!in) { r.problems.push_back("cannot open file"); return r; }

    std::string line;
    int line_no = 0;

    while (std::getline(in, line)) {
        ++line_no;
        if (line.empty()) continue;

        std::string err;
        auto t = parse_task_line(line, err);
        if (!t) {
            r.problems.push_back("line " + std::to_string(line_no) + ": " + err);
            continue;
        }
        r.tasks.push_back(*t);
    }

    if (!in.eof()) r.problems.push_back("read error (not EOF)");
    return r;
}

Hier sieht man bewusst, dass „Fehler beim Oeffnen“ als Problem im Report landet, statt als Ausnahme oder hartes Abbrechen. Das ist fuer best effort typisch: der Report ist selbst das weiche Ergebnis.

Ein weiterer praktischer Punkt: die Liste problems kann sehr gross werden. In echten Anwendungen setzt man oft ein Limit: zum Beispiel nur die ersten 50 Probleme behalten und dann „und noch N weitere…“ schreiben.

Integration ins main

Wenn Sie das Laden in eine Anwendung einbauen, ist es wichtig, dass das Verhalten vorhersehbar ist. Ein typischer Einsteigerfehler: „Ich habe irgendwas geladen, und wenn nicht, laeuft das Programm trotzdem weiter in einem seltsamen Zustand“. Wir wollen, dass die Entscheidungstelle klar ist: entweder Daten sind vorhanden, oder nicht, oder sie sind partiell und das spiegelt sich in der Logik wider.

Beispiel fuer fail fast:

#include <iostream>

int main() {
    std::string error;
    auto tasks = load_tasks_fail_fast("tasks.txt", error);

    if (!tasks) {
        std::cerr << "Load failed: " << error << '\n';
        return 1;
    }

    std::cout << "Loaded tasks: " << tasks->size() << '\n'; // Loaded tasks: 3
}

Und ein Beispiel fuer best effort:

#include <iostream>

int main() {
    auto report = load_tasks_best_effort("tasks.txt");

    std::cout << "Loaded tasks: " << report.tasks.size() << '\n';
    std::cout << "Problems: " << report.problems.size() << '\n';

    if (!report.problems.empty()) {
        std::cerr << report.problems.front() << '\n'; // line 4: bad format...
    }
}

Beachten Sie: best effort tut nicht so, als waere alles perfekt. Es informiert ehrlich darueber, dass Probleme aufgetreten sind, sonst entsteht ein „stiller Fehler“, den Nutzer spaeter entdecken.

5. Typische Fehler

Fehler Nr.1: Lesen, Parsing und Validierung in einer Schleife ohne Struktur mischen.
Wenn alles „innerhalb von while(getline)“ passiert, wird der Code schnell zu einer Suppe: manche Formatpruefungen hier, Sinnpruefungen dort, Streamzustand woanders. Die Abhilfe ist meist einfach: parse_* und validate_* in eigene Funktionen auslagern, sodass der Loader ein klares „Szenario“ liest statt viele Details.

Fehler Nr.2: jedes Stoppen von getline als normales EOF ansehen.
while (getline(...)) stoppt sowohl bei EOF als auch bei Fehler. Wenn man nach der Schleife nicht in.eof() prueft, kann ein echter Lesefehler als reguläres Dateiende durchgehen und man lädt nur einen Teil der Daten ohne Warnung.

Fehler Nr.3: stoi ohne Schutz verwenden und sich ueber Absturz bei kaputter Datei wundern.
stoi ist praktisch fuer Demos, aber in robusten Ladepfaden oft eine Zeitbombe: die Datei ist leicht beschaedigt, und das Programm stuerzt ab. Besser ist ein ausnahmefreier Parser (z. B. from_chars) oder mindestens Fehler in diagnostizierbare Ergebnisse umwandeln.

Fehler Nr.4: bei best effort Fehler schweigend verschlucken.
Best effort bedeutet nicht Schweigen, sondern partiellen Erfolg. Wenn schlechte Zeilen uebersprungen werden ohne Informationen darueber zu behalten, wird der Nutzer verlorene Daten sehen und das Programm als unzuverlaessig einstufen. Schon eine einzelne Meldung wie "line 17: bad number in id" erhoeht das Vertrauen, weil sie ehrlich und ueberpruefbar ist.

Fehler Nr.5: die Ladepolitik nicht als Teil des Funktionscontracts festhalten.
Ein unangenehmes Szenario: ein Projektteil geht davon aus, dass das Laden fail fast ist (und danach sind Daten garantiert korrekt), ein anderer Teil denkt, es sei best effort (und akzeptiert Luecken). Am Ende stuerzt man dort, wo man es nicht erwartet, oder arbeitet mit halben Daten. Loesung: waehlen Sie eine Politik und spiegeln Sie sie im Ergebnis-Typ (optional / Fehler oder Report), nicht nur in Kommentaren „eigentlich sollte es so sein“.

1
Aufgabe
C++ SELF, Level 63, Lektion 4
Gesperrt
Zugangsterminal
Zugangsterminal
1
Aufgabe
C++ SELF, Level 63, Lektion 4
Gesperrt
Aufgabenkarte
Aufgabenkarte
1
Aufgabe
C++ SELF, Level 63, Lektion 4
Gesperrt
Import mit sofortigem Abbruch
Import mit sofortigem Abbruch
1
Aufgabe
C++ SELF, Level 63, Lektion 4
Gesperrt
Import mit Bericht
Import mit Bericht
1
Umfrage/Quiz
Dateien und Streams, Level 63, Lektion 4
Nicht verfügbar
Dateien und Streams
Dateien und Streams
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION