zurück zum Artikel

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

Rainer Grimm

(Bild: SocoXbreed/shutterstock.com)

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

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 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.

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.

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 [1])


URL dieses Artikels:
https://www.heise.de/-9285831

Links in diesem Artikel:
[1] mailto:map@ix.de