Skip to content

Commit e5d3659

Browse files
committed
First stub
1 parent 1352f05 commit e5d3659

File tree

1 file changed

+242
-0
lines changed

1 file changed

+242
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
---
2+
author: Dominik
3+
layout: post
4+
title: "std::expected in C++23: A Better Way to Handle Errors"
5+
description: "Practical introduction to std::expected: intent-revealing, structured error handling for recoverable failure paths in modern C++."
6+
image: /images/cpp_logo.png
7+
hero_image: /images/cpp_logo.png
8+
hero_darken: true
9+
tags: cpp, ai, cmake
10+
---
11+
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”.
13+
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
20+
21+
## Basic Form
22+
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.
60+
61+
```cpp
62+
#include <expected>
63+
#include <string>
64+
#include <vector>
65+
#include <span>
66+
67+
enum class QrScanError {
68+
NoCodeDetected,
69+
LowContrast,
70+
UnsupportedEncoding
71+
};
72+
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.
93+
}
94+
}
95+
```
96+
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
193+
194+
```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+
}
220+
221+
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+
}
229+
}
230+
}
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.
242+

0 commit comments

Comments
 (0)