C++23: Eine neue Art der Fehlerbehandlung mit std::expected

C++23 erweitert die Schnittstelle von std::optional und fĂĽhrt den neuen Datentyp std::expected fĂĽr die Fehlerbehandlung ein.

vorlesen Druckansicht 4 Kommentare lesen

(Bild: SocoXbreed/shutterstock.com)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Der Datentyp std::optional steht seit C++17 zur Verfügung. Mit C++23 erhält er eine erweiterte monadische Schnittstelle, die ich im Folgenden näher vorstelle. Doch zuvor werfen wir einen kurzen Blick zurück auf den Datentyp.

Der Datentyp std::optional ist sehr praktisch für Berechnungen wie z.B. Datenbankabfragen, die ein Ergebnis haben können. Dieser Datentyp benötigt den Header <optional>.

Modernes C++ – Rainer Grimm
Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Mit den verschiedenen Konstruktoren und der Funktion std::make_optional lässt sich einfach ein optionales Objekt opt, mit oder ohne einen Wert definieren. opt.emplace konstruiert den enthaltenen Wert an Ort und Stelle und opt.reset zerstört den Containerwert. Du kannst einen std::optional-Container explizit fragen, ob er einen Wert hat, oder du kannst ihn in einem logischen Ausdruck überprüfen. opt.value gibt den Wert zurück, und opt.value_or gibt den Wert oder einen Defaultwert zurück. Wenn opt keinen Wert enthält, löst der Aufruf opt.value eine std::bad_optional_access-Ausnahme aus.

Hier ein einfaches Beispiel mit std::optional:

// optional.cpp

#include <optional>
#include <iostream>
#include <vector>

std::optional<int> getFirst(const std::vector<int>& vec){
  if ( !vec.empty() ) return std::optional<int>(vec[0]);
  else return std::optional<int>();
}

int main() {

    std::cout << '\n';
    
    std::vector<int> myVec{1, 2, 3};
    std::vector<int> myEmptyVec;
    
    auto myInt= getFirst(myVec);
    
    if (myInt){
        std::cout << "*myInt: "  << *myInt << '\n';
        std::cout << "myInt.value(): " << myInt.value() << '\n';
        std::cout << "myInt.value_or(2017):" << myInt.value_or(2017) << '\n';
    }
    
    std::cout << '\n';
    
    auto myEmptyInt= getFirst(myEmptyVec);
    
    if (!myEmptyInt){
        std::cout << "myEmptyInt.value_or(2017):" << myEmptyInt.value_or(2017) << '\n';
    }

    std::cout << '\n';

}

Ich verwende std::optional in der Funktion getFirst. getFirst gibt das erste Element zurück, wenn es existiert. Wenn nicht, erhältst du ein std::optional-Objekt. Die main-Funktion hat zwei Vektoren. Beide rufen getFirst auf und geben ein std::optional-Objekt zurück. Im Fall von myInt hat das Objekt einen Wert; im Fall von myEmptyInt hat das Objekt keinen Wert. Das Programm zeigt den Wert von myInt und myEmptyInt an. myInt.value_or(2017) gibt den Wert zurück, aber myEmptyInt.value_or(2017) gibt den Defaultwert zurück.

Hier die Ausgabe des Programms:

In C++23 wird std::optional um die monadischen Operationen opt.and_then, opt.transform und opt.or_else erweitert.

  • opt.and_then gibt das Ergebnis des angegebenen Funktionsaufrufs zurĂĽck, wenn es existiert, ansonsten ein leeres std::optional.
  • opt.transform gibt einen std::optional zurĂĽck, der den transformierten Wert enthält, oder einen leeren std::optional.
  • opt.or_else gibt das std::optional zurĂĽck, wenn es einen Wert enthält, oder andernfalls das Ergebnis der angegebenen Funktion.

Diese monadischen Operationen ermöglichen die Komposition von Operationen auf std::optional:

// optionalMonadic.cpp

#include <iostream>
#include <optional>
#include <vector>
#include <string>

std::optional<int> getInt(std::string arg) {
    try {
        return {std::stoi(arg)};
    }
    catch (...) {
        return { };
    }
}

 
int main() {
 
    std::cout << '\n'; 

    std::vector<std::optional<std::string>> strings = {"66", "foo", "-5"};

    for (auto s: strings) {
        auto res = s.and_then(getInt)
                  .transform( [](int n) { return n + 100;})
                  .transform( [](int n) { return std::to_string(n); })
                  .or_else([] { return std::optional{std::string("Error") }; });
        std::cout << *res << ' ';
    }

    std::cout << '\n';

}

Die range-based for-Schleife iteriert durch den std::vector<std::optional<std::string>>. Zuerst wandelt die Funktion getInt jedes Element in eine Ganzzahl um, addiert 100 dazu, wandelt es wieder in einen String um und zeigt es schließlich an. Wenn die anfängliche Umwandlung in int fehlschlägt, wird der String Error zurückgegeben und angezeigt.

std::expected unterstĂĽtzt bereits die monadische Schnittstelle.

std::expected<T, E> bietet eine Möglichkeit, einen von zwei Werten zu speichern. Eine Instanz von std::expected enthält immer einen Wert: entweder den erwarteten Wert vom Typ T oder den unerwarteten Wert vom Typ E. Dieser Vokabeltyp benötigt den Header <expected>. Dank std::expected kannst du Funktionen implementieren, die entweder einen Wert oder einen Fehler zurückgeben. Der gespeicherte Wert wird direkt innerhalb des vom erwarteten Objekt belegten Speichers zugewiesen. Es findet keine dynamische Speicherzuweisung statt.

std::expected besitzt eine ähnliche Schnittstelle wie std::optional. Im Gegensatz zu std::optional kann std::exptected eine Fehlermeldung zurückgeben.

Mit den verschiedenen Konstruktoren kannst du ein erwartetes Objekt exp mit einem erwarteten Wert definieren. exp.emplace konstruiert den enthaltenen Wert an Ort und Stelle. Du kannst einen std::expected-Container explizit fragen, ob er einen Wert hat, oder du kannst ihn in einem logischen Ausdruck überprüfen. exp.value gibt den erwarteten Wert zurück, und exp.value_or gibt den erwarteten Wert oder einen Defaultwert zurück. Wenn exp einen unerwarteten Wert hat, löst der Aufruf exp.value eine std::bad_expected_access-Ausnahme aus.

std::unexpected steht fĂĽr den unerwarteten Wert, der in std::expected gespeichert ist.

// expected.cpp

#include <iostream>
#include <expected>
#include <vector>
#include <string>

std::expected<int, std::string> getInt(std::string arg) {
    try {
        return std::stoi(arg);
    }
    catch (...) {
        return std::unexpected{std::string(arg + ": Error")};
    }
}

 
int main() {

    std::cout << '\n';

    std::vector<std::string> strings = {"66", "foo", "-5"};

    for (auto s: strings) {                                 // (1)
        auto res = getInt(s);
        if (res) {
            std::cout << res.value() << ' ';                // (3)
        }
        else {
            std::cout << res.error() << ' ';                // (4)
        }
    }

    std::cout << '\n';

    for (auto s: strings) {                                 // (2)
        auto res = getInt(s);
        std::cout << res.value_or(2023) << ' ';             // (5)
    }

    std::cout << '\n';

}

Die Funktion getInt wandelt jeden String in eine Ganzzahl um und gibt einen std::expected<int, std::string> zurĂĽck. int steht fĂĽr den erwarteten und std::string fĂĽr den unerwarteten Wert. Die beiden range-based for-Schleifen (Zeilen 1 und 2) iterieren durch den std::vector<std::string>. In der ersten range-based for-Schleife (Zeile 1) wird der erwartete (Zeile 3) oder der unerwartete Wert (Zeile 4) angezeigt. In der zweiten range-based for-Schleife (Zeile 2) wird der erwartete oder der Defaultwert 2023 (Zeile 5) angezeigt.

std::exptected unterstĂĽtzt monadische Operationen zur bequemen Komposition von Funktionen: exp.and_then, exp.transform, exp.or_else und exp.transform_error.

  • exp.and_then gibt das Ergebnis des angegebenen Funktionsaufrufs zurĂĽck, wenn es existiert, oder einen leeren std::expected.
  • exp.transform gibt einen std::expected zurĂĽck, der den umgewandelten Wert enthält, oder einen leeren std::expected.
  • exp.or_else gibt den std::exptected zurĂĽck, wenn er einen Wert enthält, oder andernfalls das Ergebnis der angegebenen Funktion.
  • exp.transform_error gibt den expected-Wert des std::expected zurĂĽck, falls er existiert. Falls nicht, gibt es den transformierten unexpected-Wert des std::expected zurĂĽck.

Das folgende Programm basiert auf dem vorherigen Programm optionalMonadic.cpp. Im Wesentlichen habe ich den Datentyp std::optional durch std::expected ersetzt.

// expectedMonadic.cpp

#include <iostream>
#include <expected>
#include <vector>
#include <string>


std::expected<int, std::string> getInt(std::string arg) {
    try {
        return std::stoi(arg);
    }
    catch (...) {
        return std::unexpected{std::string(arg + ": Error")};
    }
}
 
int main() {

    std::cout << '\n';

    std::vector<std::string> strings = {"66", "foo", "-5"};

    for (auto s: strings) {
        auto res = getInt(s)
                   .transform( [](int n) { return n + 100; })
                   .transform( [](int n) { return std::to_string(n); });
        std::cout << *res << ' ';        
    }   

    std::cout << '\n';                                  
                                        
}

Die range-based for-Schleife iteriert durch den std::vector<std::string>. Zuerst wandelt die Funktion getInt jeden String in eine Ganzzahl um, addiert 100 dazu, wandelt sie wieder in eine Zeichenkette zurück und zeigt sie schließlich an. Wenn die anfängliche Umwandlung in eine Ganzzahl fehlschlägt, wird die Zeichenkette arg + ": Error" zurückgegeben und angezeigt.

Die vier assoziativen Container std::flat_map, std::flat_multimap, std::flat_set und std::flat_multiset in C++23 sind ein einfacher Ersatz fĂĽr die geordneten assoziativen Container std::map,std::multimap, std::set und std::multiset. In C++23 haben wir sie aus einem Grund: Performanz. (map)