|
4 | 4 |
|
5 | 5 | ## What This Is |
6 | 6 |
|
7 | | -A complete implementation of `std::expected` (C++26) extended with six template specializations that allow `T` and/or `E` to be reference types, proposed for C++29. The reference semantics follow P2988 (`optional<T&>`): rebind on assignment, shallow const, dangling prevention via deleted constructors. |
| 7 | +A complete implementation of `std::expected` (C++26) extended to allow `T` and/or `E` to be reference types, proposed for C++29. This is a new `expected<T&, E>` specialization for a reference value, a relaxation of the primary and `void` templates to admit a reference error `E`, and a new `unexpected<E&>` — three `expected` class templates in all, each accepting an object or reference error. The reference semantics follow P2988 (`optional<T&>`): rebind on assignment, shallow const, dangling prevention via deleted constructors. |
8 | 8 |
|
9 | 9 | **File count:** 3 headers, ~4400 lines of implementation, 15 positive test files (469 tests), 54 negative compile tests. |
10 | 10 |
|
11 | 11 | **This guide is not a checklist.** It's a map of the decisions that shaped this code. Some are obviously correct. Some are defensible but debatable. Some might be wrong. Your job is to decide which is which. |
12 | 12 |
|
13 | 13 | --- |
14 | 14 |
|
15 | | -## Decision 1: Six Separate Specializations, Zero Abstraction |
| 15 | +## Decision 1: Three Class Templates, Reference Errors Folded In, Zero Abstraction |
16 | 16 |
|
17 | 17 | ### What was done |
18 | 18 |
|
19 | | -The implementation provides six complete, independent partial specializations: |
| 19 | +The implementation provides three complete, independent `expected` class |
| 20 | +templates, plus two `unexpected` templates. Reference *error* types are not |
| 21 | +separate specializations: each `expected` template permits `E` to be an object |
| 22 | +type *or* an lvalue reference, and stores the error as an `unexpected<E>` — which |
| 23 | +is the pointer-holding `unexpected<E&>` when `E` is a reference. So a single |
| 24 | +`expected<T&, E>` covers both `expected<T&, E>` and `expected<T&, E&>`, and the |
| 25 | +primary covers `expected<T, E>` and `expected<T, E&>`. |
20 | 26 |
|
21 | | -| Specialization | Lines | Storage | |
| 27 | +| Template | Value storage | Error storage | |
22 | 28 | |----------------|-------|---------| |
23 | | -| `expected<T, E>` | ~1100 | `union { T val_; E unex_; }` + bool | |
24 | | -| `expected<void, E>` | ~800 | `union { E unex_; }` + bool | |
25 | | -| `expected<T&, E>` | ~700 | `T*` + `union { E unex_; }` + bool | |
26 | | -| `expected<T, E&>` | ~700 | `union { T val_; }` + `E*` + bool | |
27 | | -| `expected<T&, E&>` | ~550 | `T*` + `E*` + bool | |
28 | | -| `expected<void, E&>` | ~450 | `E*` + bool | |
29 | | - |
30 | | -Each specialization is a self-contained class with its own constructors, assignment operators, observers, swap, equality, and monadic operations. There is no shared base class, no CRTP, no mixin, no macro that generates them. |
| 29 | +| `expected<T, E>` (primary) | `union { T val_; unexpected<E> unex_; }` + bool | `unexpected<E>` (pointer when `E` is `E&`) | |
| 30 | +| `expected<void, E>` | `union { unexpected<E> unex_; }` + bool | `unexpected<E>` | |
| 31 | +| `expected<T&, E>` | `union { T* val_; unexpected<E> unex_; }` + bool | `unexpected<E>` | |
| 32 | +| `unexpected<E>` | — | `E` by value | |
| 33 | +| `unexpected<E&>` | — | `E*` | |
| 34 | + |
| 35 | +The static assertion that used to read "`E` must not be a reference" now reads |
| 36 | +"`E` must not be an *rvalue* reference," which is the whole of what admits |
| 37 | +reference errors. Each template is a self-contained class with its own |
| 38 | +constructors, assignment operators, observers, swap, equality, and monadic |
| 39 | +operations. There is no shared base class, no CRTP, no mixin, no macro that |
| 40 | +generates them. |
31 | 41 |
|
32 | 42 | ### Why this matters |
33 | 43 |
|
34 | | -The total is ~4300 lines of template code in a single header. A factored implementation using a common base or policy template could plausibly cut this by 40-60%. The monadic operations alone account for 4 operations x 4 ref-qualified overloads x 6 specializations = 96 method definitions that share structural similarity. |
| 44 | +The total is ~4300 lines of template code in a single header. A factored implementation using a common base or policy template could plausibly cut this by 40-60%. The monadic operations alone account for 4 operations x 4 ref-qualified overloads across the three templates — dozens of method definitions that share structural similarity. |
35 | 45 |
|
36 | 46 | ### The argument for the current approach |
37 | 47 |
|
38 | 48 | - Standard library implementations (libstdc++, libc++, MSVC STL) use flat specializations for `optional` and `expected`. Inheritance-based factoring produces incomprehensible error messages. |
39 | 49 | - Each specialization can be read and audited independently against whatever future standard wording emerges. |
40 | | -- When constraints differ subtly between specializations (and they do — `T&` doesn't need `is_nothrow_move_constructible` checks, `E&` doesn't support `unexpected<G>` construction), a shared base must either disable features via SFINAE or push complexity into the base, which hides it rather than removing it. |
| 50 | +- When constraints differ subtly between templates (and they do — `T&` doesn't need `is_nothrow_move_constructible` checks, and a reference `E` accepts `unexpected<G>` construction only when `G` is itself a reference), a shared base must either disable features via SFINAE or push complexity into the base, which hides it rather than removing it. |
41 | 51 |
|
42 | 52 | ### The argument against |
43 | 53 |
|
44 | | -- **Maintenance burden.** A bug in `and_then` must be fixed in 6 places. A new monadic operation (hypothetical `or_transform`) must be added 24 times (4 overloads x 6 specializations). |
45 | | -- **Consistency risk.** Are all 6 specializations actually consistent? Small divergences creep in when hand-copying constraint clauses. The only way to verify is exhaustive side-by-side comparison. |
| 54 | +- **Maintenance burden.** A bug in `and_then` must be fixed in three templates. A new monadic operation (hypothetical `or_transform`) must be added 12 times (4 overloads x 3 templates). |
| 55 | +- **Consistency risk.** Are all three templates actually consistent, across both object and reference `E`? Small divergences creep in when hand-copying constraint clauses. The only way to verify is exhaustive side-by-side comparison. |
46 | 56 | - **Review fatigue.** A reviewer looking at 4300 lines of structurally similar code will inevitably skim. The 95th `requires` clause gets less scrutiny than the 5th. |
47 | 57 |
|
48 | 58 | ### What to decide |
@@ -172,7 +182,7 @@ Every deleted operation carries a C++26 `= delete("message")` string explaining |
172 | 182 |
|
173 | 183 | ### What was done |
174 | 184 |
|
175 | | -All four monadic operations (`and_then`, `or_else`, `transform`, `transform_error`) are provided for all six specializations, with four ref-qualified overloads each (`&`, `const&`, `&&`, `const&&`). |
| 185 | +All four monadic operations (`and_then`, `or_else`, `transform`, `transform_error`) are provided for all three `expected` templates, with four ref-qualified overloads each (`&`, `const&`, `&&`, `const&&`), for both object and reference `E`. |
176 | 186 |
|
177 | 187 | For reference specializations, the callable always receives `T&` (dereferenced pointer), regardless of the expected object's own value category. This means: |
178 | 188 |
|
@@ -208,21 +218,21 @@ std::move(e).and_then(f); // f still gets int&, not int&& |
208 | 218 |
|
209 | 219 | ### What to decide |
210 | 220 |
|
211 | | -Is the test suite sufficient for standardization review? The positive coverage is good — every operation has basic tests. But the systematic constraint/SFINAE testing that exists for the primary template is absent for three of the four reference specializations. This gap should be closed before submitting to WG21. |
| 221 | +Is the test suite sufficient for standardization review? The positive coverage is good — every operation has basic tests. But the systematic constraint/SFINAE testing that exists for the primary template and `expected<T&, E>` is absent for the reference-error (`E&`) configurations of the primary and `void` templates. This gap should be closed before submitting to WG21. |
212 | 222 |
|
213 | 223 | --- |
214 | 224 |
|
215 | 225 | ## Decision 10: Single Header vs Multi-Header |
216 | 226 |
|
217 | 227 | ### What exists |
218 | 228 |
|
219 | | -Everything is in `expected.hpp`. The `unexpected.hpp` and `bad_expected_access.hpp` are separate (matching the standard's `[expected.syn]` structure), but all six specializations live in one file. |
| 229 | +Everything is in `expected.hpp`. The `unexpected.hpp` and `bad_expected_access.hpp` are separate (matching the standard's `[expected.syn]` structure), but all three `expected` templates live in one file. |
220 | 230 |
|
221 | 231 | ### What to discuss |
222 | 232 |
|
223 | 233 | - **4400 lines in one file.** This is at the boundary of manageable. A reviewer can `grep` and navigate, but side-by-side comparison of specializations requires tooling. |
224 | 234 | - **The standard doesn't mandate file structure.** But for a reference implementation intended to demonstrate proposed wording, would splitting `expected_ref.hpp` (or similar) improve reviewability? |
225 | | -- **Compile times.** Every translation unit that includes `expected.hpp` parses all six specializations. For users who only need `expected<T, E>`, this is wasted work. The module build path (`expected.cppm`) mitigates this but is opt-in and not the default. |
| 235 | +- **Compile times.** Every translation unit that includes `expected.hpp` parses all three templates. For users who only need `expected<T, E>`, this is wasted work. The module build path (`expected.cppm`) mitigates this but is opt-in and not the default. |
226 | 236 |
|
227 | 237 | --- |
228 | 238 |
|
|
0 commit comments