阅读前建议先了解:optional 与 variant、Lambda。本篇介绍的新增能力属于 C++23。
返回 -1、false 或空 optional 可以表示失败,但调用方未必知道原因。输出参数把值与状态分散到不同位置;异常适合某些边界,却不总符合“失败很常见,调用方应分支处理”的接口设计。
C++23 的 expected<T, E> 在同一个对象里保存成功值 T 或错误 E。它不是异步 Future,也不会自动记录日志或强迫调用方检查。
先不引入文本解析。下面只允许偶数对半分;遇到奇数就返回原因。
#include <expected>
#include <iostream>
#include <string>
std::expected<int, std::string> half(int value) {
if (value % 2 != 0) return std::unexpected(std::string("odd number"));
return value / 2;
}
int main() {
const auto good = half(8);
const auto bad = half(7);
if (good && !bad) {
std::cout << "value=" << *good << ", error=" << bad.error() << '\n';
}
}沿着调用看三步:half(8) 返回整数 4,保存为成功值;half(7) 返回 unexpected 包装的字符串,保存为错误;调用方先检查状态,再分别用 *good 和 bad.error() 取出对应内容。
expected<int, std::string> 中 int 是成功类型,string 是错误类型。unexpected 是选择错误分支的标记,并不表示抛出了异常。只用 optional<int> 可以表示失败,却无法传回这里的错误文字。
传统接口可写 bool parse(text, value, error)。下面把两种结果一起返回,成功和错误路径都可以直接观察:
#include <charconv>
#include <expected>
#include <iostream>
#include <string>
#include <string_view>
#include <system_error>
std::expected<int, std::string> parse(std::string_view text) {
int value = 0;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
if (error != std::errc{} || end != text.data() + text.size())
return std::unexpected(std::string("invalid integer"));
return value;
}
int main() {
auto good = parse("42");
auto bad = parse("oops");
if (good && !bad) std::cout << "value=" << *good << ", error=" << bad.error() << '\n';
}本例检查了整个字符串是否消费完,避免把 42abc 当成成功。unexpected 明确选择错误分支,普通整数选择值分支;T 与 E 即使同型也仍需这种状态区分。optional 只有“有/无”,expected 增加可解释的错误。
本函数实际允许负整数;若业务要求正数,还要单独验证 value > 0。类型包装不会补上领域规则。
这些操作常称为 monadic(按成功或失败状态串联计算)操作,不需要先学习函数式编程理论。transform 把成功值映射为普通新值,and_then 的回调返回新的 expected,失败时跳过后续成功回调。
#include <cassert>
#include <expected>
#include <iostream>
#include <string>
std::expected<int, std::string> checked(int value) {
if (value < 0) return std::unexpected(std::string("negative"));
return value;
}
int main() {
int calls = 0;
auto twice = [&](int value) { ++calls; return value * 2; };
auto good = checked(20).transform(twice)
.and_then([](int value) { return checked(value + 2); });
auto bad = checked(-1).transform(twice);
assert(calls == 1);
assert(good && !bad);
std::cout << "value=" << *good << ", error=" << bad.error() << '\n';
}错误路径没有调用 twice,错误继续传播。or_else 处理错误并返回新的 expected;transform_error 改变错误类型。接口的基础支持宏为 202202,包含这组串联操作的支持宏为 202211;不能只检查头文件存在。
使用 *result、result->member 前先确认有值;error() 只在错误状态读取。value() 在错误状态会抛 bad_expected_access,并非无异常读取。expected<void, E> 表示成功时无需返回数据。C++23 不支持 expected<T&, E>,若要借用对象应明确使用指针或 reference_wrapper 及其寿命契约。
expected 始终保存值或错误,没有 variant 式的 valueless_by_exception 状态。内部通常是联合存储加状态,但布局不是标准保证;值/错误本身仍可能分配或抛异常。回调抛出的异常不会自动变成 E,要在明确的异常边界转换。
适合解析、校验和预期会失败的操作。跨 API 选择统一错误类型,避免每层重复包装;只有“是否存在”而不关心原因时 optional 更直接。
从仓库根目录运行;源码就是本文的完整 cpp 围栏:
python3 tools/verify_examples.py --compiler clang++ --path docs/cpp23/expected.md带 requires 元数据的示例按特性测试宏检查支持程度;缺少能力时报告跳过及原因,跳过不代表通过。使用真正的 GCC 时将 clang++ 替换为 g++。