Zum Hauptinhalt springen

Fehlerbehandlung

Der Kern der C++-Implementierung ist exception-frei: Jede Operation, die scheitern kann, gibt osf::Result<T> zurück — ein tl::expected<T, osf::Error>. Wer Exceptions bevorzugt, legt die dünne opt-in-Schicht osf::throwing darüber. Beide Stile lassen sich mischen.

osf::Error und osf::Result<T>

struct Error {
Code code; // stabile Kategorie — hierauf verzweigen
std::string message; // menschenlesbares Detail — nur zur Anzeige
};

template <typename T>
using Result = tl::expected<T, Error>;

Grundidiom:

auto r = osf::DataManager::loadFromFile(pfad);
if (!r) {
log("Laden fehlgeschlagen [{}]: {}",
osf::errorCategoryName(r.error().code), // stabiler Name, z. B. "io_error"
r.error().message);
return;
}
osf::DataManager const& mgr = *r; // oder r.value()

Regeln:

  • Auf code verzweigen, message nur anzeigen. Der Wortlaut der Message ist nicht Teil der API und darf sich ändern.
  • Alle Result-Rückgaben sind [[nodiscard]] — der Compiler mahnt ignorierte Fehler an.
  • errorCategoryName(code) liefert einen stabilen String-Bezeichner für Logs (statisch, kein Besitz).
  • Result<void> signalisiert reine Erfolg/Fehler-Operationen (writer.start(), writeToFile(...), …): if (!r) ….

Fehlercode-Katalog

Eingabe- und API-Fehler

CodeBedeutungTypische Quelle
InvalidArgumentAPI-Vorbedingung verletzt: ungültige ChannelDef, unbekannter Kanalindex am Writer, count == 0, Writer in falscher Lebenszyklus-Phase, nicht-positive AbtastrateWriter
IoErrorDatei/Stream-Fehler: nicht öffnenbar, Lese-/Schreibfehler, fsync-Fehlerüberall
NotFoundreserviert für Lookup-APIs (Kanal-Lookups geben stattdessen nullptr)
UnknownFallback ohne speziellere Kategorie; Code eines default-konstruierten Error
ParseErrorgenerischer Parse-Fehler, wenn keine speziellere Kategorie passt (selten)Parser

Magic-Header

CodeBedeutung
InvalidMagicHeaderErste Zeile ist kein wohlgeformter OSF-Header — meist „keine OSF-Datei"
UnsupportedVersionHeader parsebar, aber der Bezeichner ist keine der vier akzeptierten Schreibweisen (OSF4, OSF5, OCEAN_STREAM_FORMAT4, OCEAN_STREAMING_FORMAT4)
MagicHeaderTooLongKein Zeilenumbruch innerhalb von 128 Bytes — sicher keine OSF-Datei

Metablock

CodeBedeutung
InvalidMetablockStrukturfehler: Pflichtfeld fehlt, Zahl nicht parsebar, sizeOfLengthValue ≠ 2/4 (würde sonst jeden Block-Read still korrumpieren), Wurzelelement falsch
JsonParseErrorOSF5: Metablock-Body ist kein gültiges JSON (Parser-Diagnose in message)
XmlParseErrorOSF4: Metablock-Body ist kein wohlgeformtes XML (Diagnose + Byte-Offset in message)
RemovedInSpecDatei verwendet einen mit Spec-Rev 2026-05-04 entfernten Datentyp (pair, triple, candata, gpsdata). Wird hart abgelehnt — das alte Payload-Layout ist aus einem aktuellen Build nicht reproduzierbar; die Message nennt den Ersatz

Block-Strom

CodeBedeutung
UnknownChannelIndexBlock referenziert einen Kanalindex ohne Metablock-Definition. Ohne Definition ist die Breite des Längenfelds unbekannt → Korruptionssignal, harter Abbruch
InvalidBlockPayload strukturell defekt (falsche Länge für den Datentyp, äquidistanter Block auf String-Kanal, Sample sprengt die Blockkapazität des Streaming-Writers, …)
ChannelMixedBlockTypesEin Kanal liefert sowohl äquidistante (bcStartData/bcContinuedData) als auch timestamped Blöcke — von der Spec verboten
ContinuedDataWithoutStartbcContinuedData ohne vorausgehendes bcStartData — ohne offenes Segment hat die Fortsetzung keine Zeitbasis
RelStampWithoutAnchorbcContinuedRelStampData ohne vorherigen absoluten Zeitstempel — die Deltas haben keinen Anker
DataTypeMismatchAngeforderter Typ ≠ gespeicherter Typ (z. B. asDoublesFlat auf einem int32-Kanal, asStrings auf einem Binary-Kanal)

Was bewusst kein Fehler ist

SituationVerhalten
Datei endet mitten im Block (Stromausfall)Best-Effort: alle vollständigen Blöcke werden geliefert, stats.blocksTruncated = 1, Iteration endet sauber
Unbekannter zukünftiger Datentyp/KanaltypKanal parst als Unsupported, Blöcke werden als Skipped ausgerichtet konsumiert, restliche Kanäle laden normal
Deprecated/reservierte Control-Bytes (alte Felddateien)BlockKind::Skipped mit SkipReason, Zähler in stats
Deprecated Kanal-Felder (scale, offset, physicalunit1..3, …)stillschweigend toleriert und ignoriert (reale Felddateien tragen sie alle)
Kanal-Lookup ohne Treffernullptr, kein Result

Die werfende Schicht — osf::throwing

Header-only, opt-in (#include <osf/throwing.h>), bewusst nicht im Umbrella-Header <osf/osf.h> und nicht in die Bibliothek einkompiliert. Wer sie nie einbindet, zieht keinerlei Exception-Maschinerie ein.

#include <osf/throwing.h>

try {
auto mgr = osf::throwing::load("messung.osf"); // DataManager oder wirft
osf::throwing::writeToFile(mgr, "kopie.osf");

osf::StreamingWriter w{pfad};
auto ch = osf::throwing::unwrap(w.addChannel(def)); // Result<T> -> T oder wirft
osf::throwing::unwrap(w.start());
osf::throwing::unwrap(w.writeTimestampedSample<double>(ch, ts, wert));
osf::throwing::unwrap(w.close());
} catch (osf::Exception const& e) {
// e.what() — Message (oder Kategorie-Name, wenn Message leer)
// e.code() — Error::Code für programmatisches Verzweigen
// e.error() — der vollständige strukturierte osf::Error
}

Die Schicht besteht aus genau drei Bausteinen:

BausteinZweck
osf::Exception : std::runtime_errorträgt den vollständigen osf::Error; lebt in osf, nicht osf::throwing
osf::throwing::unwrap(Result<T>)universeller Adapter: Wert herausziehen oder werfen. Funktioniert mit jedem Result des Kerns, auch von Writer-Methoden — deshalb braucht es keine werfenden Wrapper pro Methode
osf::throwing::load / writeToFile / writeTowerfende Pendants der häufigsten High-Level-Operationen

Stilwahl in der Praxis

  • Bibliotheks-/Embedded-Code, Hot-Paths, Codebasen mit -fno-exceptions: beim Result-Kern bleiben.
  • Anwendungscode mit vorhandener Exception-Strategie: throwing an der Außenkante benutzen; intern bleibt alles Result.
  • Gemischt: unwrap punktuell dort, wo ein Fehler ohnehin nur propagiert würde — z. B. in einem CLI-main, das oben einen try/catch hat.

Sonderfall Writer: Sticky Errors

Der StreamingWriter merkt sich den ersten I/O-Fehler („Broken"- Zustand) und gibt ihn bei jedem Folgeaufruf zurück, einschließlich close(). In Schreibschleifen genügt deshalb ein Fehler-Check pro Iteration; die Ursache geht auch dann nicht verloren, wenn erst am Ende ausgewertet wird.