Skip to content

Commit ece33ed

Browse files
committed
Start writing the article
1 parent e5d3659 commit ece33ed

File tree

1 file changed

+19
-213
lines changed

1 file changed

+19
-213
lines changed

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

Lines changed: 19 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -9,234 +9,40 @@ hero_darken: true
99
tags: cpp, ai, cmake
1010
---
1111

12-
`std::expected` (C++23) is a vocabulary type for functions that can either produce a value (success) or a well-defined recoverable error (failure) without throwing. It generalizes the intent behind `std::optional`: instead of “value or nothing”, you get “value *or* error payload”.
12+
**How to handle errors in C++ has been a constant point of debate.** Do you use exceptions, error code, out-parameters or return nullptrs on failure? And how do you convey information on the nature of the failure? With C++17 we got `std::optional` for "value or nothing" semantics, but it lacks error context. [C++23 - finally - introduces `std::expected`](https://en.cppreference.com/w/cpp/utility/expected.html), a type that encapsulates either a value or an error, making error handling explicit and composable. Let's explore how `std::expected` can improve your C++ code.
1313

14-
Key points:
15-
- Success path is explicit (`expected<T,E>` holds `T`)
16-
- Failure path is explicit (holds `E`)
17-
- Zero-cost access in the success case (no heap)
18-
- Works alongside (not instead of) exceptions
19-
- Encourages documenting failure modes via the error type
14+
## `std::expected` in a nutshell
2015

21-
## Basic Form
16+
Semantically, `std::expected<T, E>` is a returnable type that can either hold a value of type `T` (indicating success) or an error of type `E` (indicating failure). This makes it clear to the caller that a function can fail and provides a structured way to handle that failure.
2217

23-
```cpp
24-
#include <expected>
25-
#include <string>
26-
#include <iostream>
27-
28-
std::expected<int, std::string> parse_int(std::string_view txt) {
29-
try {
30-
size_t pos = 0;
31-
int v = std::stoi(std::string(txt), &pos);
32-
if (pos != txt.size())
33-
return std::unexpected("Trailing characters");
34-
return v; // implicit expected<int,string> construction
35-
} catch (std::exception const& ex) {
36-
return std::unexpected(std::string("Parse error: ") + ex.what());
37-
}
38-
}
39-
40-
int main() {
41-
if (auto r = parse_int("42"); r) {
42-
std::cout << "Value = " << *r << "\n";
43-
} else {
44-
std::cout << "Error: " << r.error() << "\n";
45-
}
46-
}
47-
```
48-
49-
Access patterns:
50-
- `r.has_value()` or `if (r)` checks success
51-
- `*r` / `r.value()` gives the value (latter throws `bad_expected_access` if no value)
52-
- `r.error()` yields the error object (only if !r)
53-
54-
## Why Not Just std::optional?
55-
`std::optional<T>` only distinguishes “present” vs “absent”. You must invent an external channel (logs, out-params, magic enums) for error context. `std::expected<T,E>` bakes the error representation into the type signature, making intent self-documenting and enabling composition.
56-
57-
## A Realistic Scenario: QR Code Scanner
58-
59-
Design: Recoverable domain failures (e.g. “no QR code found”) return an error enum. Catastrophic issues (I/O, memory corruption, decoder invariants) throw exceptions.
18+
Let's look at a simple example of a function that computes the square root of a number, returning an error if the input is negative:
6019

6120
```cpp
6221
#include <expected>
63-
#include <string>
64-
#include <vector>
65-
#include <span>
66-
67-
enum class QrScanError {
68-
NoCodeDetected,
69-
LowContrast,
70-
UnsupportedEncoding
71-
};
22+
#include <cmath>
7223

73-
std::expected<std::string, QrScanError>
74-
decode_qr(std::span<const std::byte> imageData);
75-
76-
/*
77-
Usage:
78-
*/
79-
void process_image(std::span<const std::byte> img) {
80-
try {
81-
if (auto r = decode_qr(img); r) {
82-
// Success
83-
// use *r
84-
} else {
85-
switch (r.error()) {
86-
case QrScanError::NoCodeDetected: /* fallback */ break;
87-
case QrScanError::LowContrast: /* maybe retry */ break;
88-
case QrScanError::UnsupportedEncoding:/* report */ break;
89-
}
90-
}
91-
} catch (const std::exception& ex) {
92-
// Truly exceptional: corrupted file, allocation failure, etc.
24+
std::expected<double, std::string> safe_sqrt(double x) {
25+
if (x < 0) {
26+
return std::unexpected("Negative input");
9327
}
28+
return std::sqrt(x);
9429
}
9530
```
9631
97-
This separation clarifies which failures callers must handle and which remain exceptional.
98-
99-
## Transforming and Chaining (Monadic Style)
100-
101-
`and_then`, `or_else`, and `transform` let you compose stages cleanly.
102-
103-
```cpp
104-
#include <expected>
105-
#include <string>
106-
#include <cctype>
107-
108-
std::expected<std::string, std::string> fetch();
109-
std::expected<int, std::string> to_int(std::string_view);
110-
111-
auto pipeline() {
112-
return fetch()
113-
.transform([](std::string s) { return s + "_suffix"; })
114-
.and_then([](std::string const& s) { return to_int(s); })
115-
.or_else([](std::string const& err) {
116-
// map error to a unified form
117-
return std::unexpected("pipeline: " + err);
118-
});
119-
}
120-
```
121-
122-
- `transform` maps the value if present, leaves error untouched.
123-
- `and_then` expects a callable returning another `expected`, flattening nesting.
124-
- `or_else` lets you transform the error.
125-
126-
## Interop With Exceptions
127-
128-
You can still throw where unwinding is cleaner (constructor invariants, impossible states, allocation failures). Use `expected` when:
129-
- Caller is likely to recover or choose an alternate path
130-
- Failures are part of normal control flow
131-
- You want static exhaustiveness via an error enum
132-
133-
Use exceptions when:
134-
- Handling site is distant and recovery is rare
135-
- Failure indicates abnormal or truly exceptional conditions
136-
137-
## Choosing the Error Type
138-
139-
Common patterns:
140-
- Enum class (compact, compile-time reviewed)
141-
- `std::string` or `std::string_view` (flexible but less structured)
142-
- Custom struct holding code + context (line, file offset, etc.)
143-
- Lightweight error wrapper referencing shared static strings
144-
145-
```cpp
146-
struct ParseError {
147-
enum class Code { UnexpectedChar, Range, Empty } code;
148-
int position;
149-
};
150-
151-
std::expected<int, ParseError> parse_number(std::string_view txt);
152-
```
153-
154-
## Providing Defaults
155-
156-
```cpp
157-
int value_or_default(std::string_view s) {
158-
auto r = parse_int(s);
159-
return r.value_or(0); // 0 if error
160-
}
161-
```
162-
163-
## Converting Legacy APIs
164-
165-
Wrap a legacy function returning error codes:
166-
167-
```cpp
168-
enum legacy_err { OK=0, NOT_FOUND=1, PERM=2 };
169-
170-
legacy_err legacy_lookup(int key, int* out);
171-
172-
std::expected<int, legacy_err> modern_lookup(int key) {
173-
int v;
174-
if (auto e = legacy_lookup(key, &v); e == OK)
175-
return v;
176-
return std::unexpected(e);
177-
}
178-
```
179-
180-
## Performance Notes
181-
182-
- Represents a discriminated union; typically same size as `T` plus space for `E` (aligned to max of both) plus a boolean.
183-
- No heap allocations unless `T` or `E` allocate.
184-
- Branch predictor friendly for predominantly-success paths.
185-
186-
## Pitfalls
187-
188-
- Do not overuse for every function; pure success functions return plain `T`.
189-
- Avoid large `E` types; store references or lightweight handles if needed.
190-
- Be explicit in public APIs; prefer `expected<result_type, error_enum>` over vague string errors when stability matters.
191-
192-
## Minimal End-to-End Example
32+
To use this function, you can check if the result is valid and handle the error accordingly:
19333
19434
```cpp
195-
#include <expected>
196-
#include <string>
197-
#include <charconv>
198-
#include <system_error>
199-
200-
enum class HexError { Empty, Invalid };
201-
202-
std::expected<unsigned, HexError> parse_hex(std::string_view s) {
203-
if (s.empty()) return std::unexpected(HexError::Empty);
204-
unsigned value = 0;
205-
auto first = s.data();
206-
auto last = s.data() + s.size();
207-
auto res = std::from_chars(first, last, value, 16);
208-
if (res.ec != std::errc() || res.ptr != last)
209-
return std::unexpected(HexError::Invalid);
210-
return value;
211-
}
212-
213-
std::string describe(HexError e) {
214-
switch (e) {
215-
case HexError::Empty: return "input empty";
216-
case HexError::Invalid: return "invalid hex";
217-
}
218-
return "unknown";
219-
}
35+
#include <iostream>
22036
22137
int main() {
222-
for (auto txt : {"FF", "XYZ", ""}) {
223-
if (auto r = parse_hex(txt); r) {
224-
// success
225-
} else {
226-
auto msg = describe(r.error());
227-
(void)msg;
228-
}
38+
auto result = safe_sqrt(-1);
39+
if (result) {
40+
std::cout << "Square root: " << *result << '\n';
41+
} else {
42+
std::cout << "Error: " << result.error() << '\n';
22943
}
44+
return 0;
23045
}
231-
```
232-
233-
## Summary
234-
235-
`std::expected`:
236-
- Documents recoverable failure modes in the type system
237-
- Reduces implicit contracts and sentinel values
238-
- Composes cleanly with functional-style helpers
239-
- Complements (not replaces) exceptions
240-
241-
Adopt it where callers are expected to react to well-scoped failures. It improves clarity, testability, and intent.
24246
47+
* expected vs optional vs exceptions
48+
*

0 commit comments

Comments
 (0)