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.
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 [1])
URL dieses Artikels:
https://www.heise.de/-9285831
Links in diesem Artikel:
[1] mailto:map@ix.de
Copyright © 2023 Heise Medien