Skip to content

Commit f7a4ec5

Browse files
committed
Add more content
1 parent ece33ed commit f7a4ec5

File tree

1 file changed

+150
-10
lines changed

1 file changed

+150
-10
lines changed

_posts/2025-10-06-cpp-std-expected.md

Lines changed: 150 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,162 @@ std::expected<double, std::string> safe_sqrt(double x) {
2727
}
2828
return std::sqrt(x);
2929
}
30+
31+
...
32+
33+
// Usage
34+
const auto result = safe_sqrt(-1);
35+
if (result) {
36+
std::cout << "Square root: " << *result << '\n';
37+
} else {
38+
std::cout << "Error: " << result.error() << '\n';
39+
}
40+
3041
```
42+
In this example, `safe_sqrt` returns an `std::expected<double, std::string>`. If the input is valid, it returns the square root; otherwise, it returns an error message. The caller can then check if the result is valid and handle the error accordingly. So how does this compare to traditional error handling methods?
43+
44+
### Comparison to Traditional Error Handling
45+
46+
Before `std::expected`, there were typically two main approaches to error handling in C++: exceptions and error codes. While exceptions can be powerful, they typically bring with them more complexity in control flow and then there is the discussion which errors should cause an exception to be thrown and which should not. The benefit of exceptions is that they allow for clean separation of error handling code and for propagation of errors up the call stack.
47+
Error codes on the other hand tend to either clutter the code by requiring out-parameters or have the problem of being either ignored or misunderstood by the caller. While [nodiscard](https://en.cppreference.com/w/cpp/language/attributes/nodiscard) can help with ignored return values, it still does not solve the problem that the caller has to semantically understand the meaning of the return value.
48+
49+
`std::expected` provides a middle ground. It makes error handling explicit in the type system, allowing to pass semantic information about the error back to the caller. The beauty of `std::expected`is also, that it can help to discern between expected or recoverable errors (e.g. file not found, invalid input) and unexpected or unrecoverable errors (e.g. out of memory, logic errors) which should still be handled via exceptions.
50+
51+
> **Tip:** Use `std::expected` for recoverable errors where the caller can take action based on the error, and reserve exceptions for truly exceptional situations.
52+
53+
Let's look at a more complex example that demonstrates how `std::expected` can be used in a real-world scenario.
54+
55+
### Real world example: Reading a QR code from an image
56+
57+
Let's suppose we want to write a function that reads a QR code from binary image data. The function generally has three paths:
58+
59+
1. The image contains a valid QR code and we can return the decoded string.
60+
2. The image does not contain a QR code and we want to return an error indicating that.
61+
3. The image data is unreadable (e.g. corrupted or unrecognizable format) and we want to throw an exception.
3162
32-
To use this function, you can check if the result is valid and handle the error accordingly:
63+
While the first two paths are expected and recoverable errors, the third path is an unexpected error that should be handled via exceptions. So the implementation could look like this:
3364
3465
```cpp
35-
#include <iostream>
66+
#include <expected>
67+
#include <string>
68+
#include <stdexcept>
69+
#include <vector>
70+
71+
std::expected<std::string, std::string> read_qr_code(std::vector<uint8_t> const& image_data) {
72+
73+
if (image_data.empty() || check_if_corrupted(image_data)) {
74+
throw std::invalid_argument("Invalid image data");
75+
}
76+
77+
// Assume parse_image_data is a function that parses the image data and returns the QR code string or throws on failure
78+
std::string parsed_data = parse_image_data(image_data); // May throw exceptions on failure
79+
80+
if (parsed_data.empty()) {
81+
return std::unexpected("No QR code found");
82+
}
83+
84+
return parsed_data;
85+
}
86+
```
87+
88+
Note that in this example, both the success and error type are strings, but they could be any type. In a lot of cases, it might still make sense to use an enum or a custom error type for the error case to make it more structured. However, by using `std::expected`, we already are able to add a lot more context to the function without cluttering the code. This already is a big improvement over returning, but there is more.
89+
90+
### Monadic chaining with `and_then`
91+
3692

37-
int main() {
38-
auto result = safe_sqrt(-1);
39-
if (result) {
40-
std::cout << "Square root: " << *result << '\n';
93+
Monadic chaining lets you compose a sequence of operations that may fail, without deeply nested `if`/`else`. With `std::expected`, you chain:
94+
- `and_then` when the next step itself may fail and returns another `expected`.
95+
- `transform` when the next step cannot fail and just maps the value.
96+
- `or_else` to act on or recover from an error.
97+
98+
Below is a continuation of the QR example, showing a pipeline that:
99+
1) reads the QR payload,
100+
2) validates it,
101+
3) parses it as a URI,
102+
4) extracts the host (pure mapping),
103+
5) adds context to the error if anything failed.
104+
105+
```cpp
106+
// Reuse the earlier read_qr_code signature:
107+
// std::expected<std::string, std::string> read_qr_code(const std::vector<uint8_t>&);
108+
109+
#include <expected>
110+
#include <string>
111+
#include <vector>
112+
#include <cctype>
113+
114+
struct Uri {
115+
std::string scheme;
116+
std::string host;
117+
std::string path;
118+
};
119+
120+
std::expected<std::string, std::string>
121+
validate_payload(std::string const& s) {
122+
if (s.empty()) {
123+
return std::unexpected("Empty QR payload");
124+
}
125+
if (s.size() > 4096) { // arbitrary sanity limit
126+
return std::unexpected("QR payload too large");
127+
}
128+
return s; // valid as-is
129+
}
130+
131+
std::expected<Uri, std::string>
132+
parse_uri(std::string const& s) {
133+
auto starts_with = [&](std::string const& p) {
134+
return s.rfind(p, 0) == 0;
135+
};
136+
137+
Uri u;
138+
if (starts_with("https://")) {
139+
u.scheme = "https";
140+
} else if (starts_with("http://")) {
141+
u.scheme = "http";
41142
} else {
42-
std::cout << "Error: " << result.error() << '\n';
143+
return std::unexpected("Not an http(s) URI");
43144
}
44-
return 0;
145+
146+
// very naïve split: scheme://host/path
147+
auto pos = s.find("://");
148+
auto rest = s.substr(pos + 3);
149+
auto slash = rest.find('/');
150+
if (slash == std::string::npos) {
151+
u.host = rest;
152+
u.path = "/";
153+
} else {
154+
u.host = rest.substr(0, slash);
155+
u.path = rest.substr(slash);
156+
}
157+
if (u.host.empty()) {
158+
return std::unexpected("Missing host");
159+
}
160+
return u;
161+
}
162+
163+
std::string host_from(Uri const& u) {
164+
return u.host; // pure mapping, cannot fail
45165
}
46166

47-
* expected vs optional vs exceptions
48-
*
167+
std::expected<std::string, std::string>
168+
annotate_error(std::string const& err) {
169+
return std::unexpected(std::string{"QR processing failed: "} + err);
170+
}
171+
172+
// Usage: linear, early-exiting pipeline
173+
std::expected<std::string, std::string>
174+
extract_qr_host(std::vector<uint8_t> const& image) {
175+
return read_qr_code(image)
176+
.and_then(validate_payload) // may fail -> expected<Payload, E>
177+
.and_then(parse_uri) // may fail -> expected<Uri, E>
178+
.transform(host_from) // cannot fail -> expected<std::string, E>
179+
.or_else(annotate_error); // act on error path, keep E the same
180+
}
181+
```
182+
183+
Notes and gotchas:
184+
- Keep the error type `E` consistent across `and_then`/`or_else` steps. If you must change it, use `transform_error`.
185+
- Use `and_then` only with functions that return `expected<..., E>`. Use `transform` for pure mappings returning plain values.
186+
- The chain short-circuits on the first error, returning that error downstream.
187+
- If a step can throw, those exceptions still propagate unless caught and converted to `std::unexpected`.
188+
- Prefer passing by `const&` in chain steps to avoid copies. If you need to move, adapt the function to accept by value and `std::move` internally.

0 commit comments

Comments
 (0)