New feature in C++ 23
Hello everyone!
This newsletter will be talking about a recently added feature to C++ 23.
You might ask, why care about C++ 23 when I can do almost everything I need with the previous versions?
C++ keeps evolving, with each standard balancing safety, performance, and expressiveness.
C++23 focuses more on quality-of-life features than revolutionary changes.
Let’s look at one such feature.
std::expected
If you’ve ever struggled with mixing exceptions, error codes, or std::optional, std::expected might just be the tool you’ve been waiting for.
It is a type designed to simplify error handling while keeping your code explicit and type-safe.
Think of it as a container that either holds a valid result or an error. Unlike exceptions, it doesn’t rely on hidden control flow, everything is explicit.
Unlike std::optional, it can carry meaningful error information.
Ex:
#include <expected>
#include <string>
#include <iostream>
std::expected<int, std::string> divide(int a, int b) {
if (b == 0)
return std::unexpected("Division by zero");
return a / b;
}
int main() {
auto result = divide(10, 0);
if (!result)
std::cout << "Error: " << result.error() << "\n";
else
std::cout << "Result: " << *result << "\n";
result = divide(4, 2);
if (!result)
std::cout << "Error: " << result.error() << "\n";
else
std::cout << "Result: " << *result << "\n";
}Output:
Error: Division by zero
Result: 2Why it’s useful:
- Explicit: You can’t ignore errors accidentally.
-
Composable: Works nicely with functions returning other
std::expectedvalues. - Safe: No exceptions to catch, no hidden control flow.
A few basic operations on this datatype:
Let’s start with this:
std::expected<int, std::string> result = divide(x, y);Access:
if(result) {
/* if this block is entered,
result has the value and not
the error */
/* the following can be done
to access the value field */
int a1 = *result;
int a2 = result.value();
}
if(!result) {
/* if this block is entered,
result has the error and not
the value */
/* the following can be done
to access the error field */
std::string err = result.error();
}Accessing the value when there’s an error (or the error when there’s a value) is unsafe; use if(result) or .value_or() to avoid exceptions or undefined behavior.
std::expected<T, E>::value_or(T default_value)
- It is a very handy member function that lets you provide a fallback value if the
std::expectedcontains an error. - It’s a safer and more concise way to handle errors than manually checking with
if(result)or using exceptions.
Consider:
int a1 = result.value_or(-1);
std::cout << a1 << std::endl;- Here if
resulthas a value thena1also has that value. - Otherwise, if
resultcontains an error, then the value ofa1will be-1.
Comparison:
std::expected<int, std::string> a = 10;
std::expected<int, std::string> b = 10;
if (a == b) { /* true */ }
std::expected<int, std::string> c = std::unexpected("fail");
if (a != c) { /* true */ }Composable operations:
std::expected supports functional-style operations like and_then and transform to chain computations safely:
auto result_pass = divide(20, 2)
.and_then([](int x){ return divide(x, 2); })
.transform([](int x){ return x * 3; });
std::cout << "Case 1: " << result_pass.value_or(-1) << "\n";
auto result_fail = divide(20, 0) // division by zero
.and_then([](int x){ return divide(x, 2); })
.transform([](int x){ return x * 3; });
std::cout << "Case 2: " << result_fail.value_or(-1) << "\n"; -
and_thenruns the lambda only if the previous result is valid. -
transformapplies a function to the value without touching the error. -
value_or(-1)provides a fallback if any error occurred in the chain.
Output:
Case 1: 15
Case 2: -1There are many more things that can be done with std::expected.
Author – ScepticallySam
