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.
![](https://heise.cloudimg.io/width/610/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/4/2/9/2/2/8/2/shutterstock_1338995891-b9b6d04c15d14c34.jpeg)
(Bild: SocoXbreed/shutterstock.com)
- Rainer Grimm
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.
std::optional
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>
.
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:
Die monadische Erweiterung von std::optional
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 leeresstd::optional
.opt.transform
gibt einenstd::optional
zurück, der den transformierten Wert enthält, oder einen leerenstd::optional
.opt.or_else
gibt dasstd::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
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 leerenstd::expected
.exp.transform
gibt einenstd::expected
zurück, der den umgewandelten Wert enthält, oder einen leerenstd::expected
.exp.or_else
gibt denstd::exptected
zurück, wenn er einen Wert enthält, oder andernfalls das Ergebnis der angegebenen Funktion.exp.transform_error
gibt den expected-Wert desstd::expected
zurück, falls er existiert. Falls nicht, gibt es den transformierten unexpected-Wert desstd::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.
Wie geht’s weiter?
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)