C++23 实用特性精选:expected、spanstream 与 Monadic 操作
C++23 实用特性精选:expected、spanstream 与 Monadic 操作
C++23 不是一个大爆炸式的标准版本——它更像是 C++20 骨架之上的肌肉和筋络。在处理"没有大语法引进"的版本时,真正改变日常编码体验的往往是那些看起来不起眼的新增类型和成员函数。
本文聚焦三个在工程实践中影响最大的 C++23 新增:std::expected(取代异常和输出参数的新范式)、std::spanstream(零拷贝字符串流)、以及 std::optional 的 monadic 扩展。
1. std::expected<T, E>:函数式错误处理
1.1 三种错误处理范式
C++ 历史上并存着三种错误处理方式:
// 方式 1:异常
int div(int a, int b) {
if (b == 0) throw std::domain_error("divide by zero");
return a / b;
}
// 方式 2:输出参数 + 错误码
enum class Error { Overflow, DivideByZero };
Error div(int a, int b, int& result) {
if (b == 0) return Error::DivideByZero;
result = a / b;
return {}; // 或者定义一个 Error::None
}
// 方式 3:std::optional(只在失败时丢失信息)
std::optional<int> div(int a, int b) {
if (b == 0) return std::nullopt;
return a / b;
}三者的困境:
- 异常有运行时开销,在嵌入式/游戏/高频交易中被禁用
- 输出参数丑陋且让类型难以组合
optional丢掉了错误原因
std::expected<T, E> 提供了第四种方式:返回值要么是正确的结果 T,要么是错误信息 E。
1.2 基本用法
std::expected<int, std::string> divide(int a, int b) {
if (b == 0) return std::unexpected("division by zero");
return a / b;
}
auto result = divide(10, 2);
if (result) {
std::cout << *result << '\n'; // 5
std::cout << result.value() << '\n'; // 5,带异常检查
}. 和 -> 直接访问内部的值,如果 expected 持有的是错误,则是未定义行为(类似 optional 的 nullopt 解引用)。value() 在错误状态下抛 std::bad_expected_access<E>。
1.3 Monadic 操作链
这是 expected 最具表现力的部分:
auto process(int x) -> std::expected<int, std::string> {
return divide(100, x)
.and_then([](int val) { // 只有成功时才调用
return std::expected<int, std::string>{val * 2};
})
.transform([](int val) { // 映射成功值
return val + 5;
})
.or_else([](std::string err) { // 只有失败时才调用
return std::expected<int, std::string>{0}; // fallback
});
}操作链语义:
| 方法 | 在成功时 | 在失败时 |
|---|---|---|
and_then(f) | 调用 f(value),返回新的 expected | 透传错误 |
transform(f) | 调用 f(value),用返回值包成新的 expected | 透传错误 |
or_else(f) | 透传成功值 | 调用 f(error),返回新的 expected |
transform_error(f) | 透传成功值 | 调用 f(error),用返回值替代原错误 |
这些组合子让错误处理变成了管道,而不是 if/else 树。
1.4 与异常的关系
expected 不取代异常——它提供了另一种选择。在无异常的代码库中,expected 是第一公民;在混合代码库中,expected 是"这条调用链禁止异常"的信号。两者可以桥接:
// 将 expected 转回异常世界
int safe_div(int a, int b) {
return divide(a, b).value(); // 错误时抛 std::bad_expected_access
}
// 将异常世界包装为 expected
template<typename F, typename... Args>
auto try_call(F&& f, Args&&... args)
-> std::expected<std::invoke_result_t<F, Args...>, std::exception_ptr>
{
try {
return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
} catch (...) {
return std::unexpected(std::current_exception());
}
}2. std::spanstream:零拷贝字符流
传统的 std::stringstream 对内部的 std::string 有所有权——它拷贝和分配。当你的数据已经在一个 std::string 或 char 数组中时,再构造一个 stringstream 就是一次不必要的拷贝。
std::spanstream(基于 std::span<char> 操作)在 C++23 中登场:
// 从已有 buffer 读取——零分配
char data[] = "42 3.14 hello";
std::ispanstream iss(std::span{data}); // 基于现有内存,不拷贝
int i; double d; std::string s;
iss >> i >> d >> s; // i=42, d=3.14, s="hello"
// 写入到栈上的固定大小 buffer
char buf[128];
std::ospanstream oss(std::span{buf});
oss << "value: " << 42 << '\n';
// buf 现在就包含了格式化的内容
// 不需要 std::string() 来提取底层内容核心场景:
- 嵌入式/实时系统:完全避免堆分配
- 协议格式化和解析:在预先分配的环形缓冲区上直接读写
- 性能敏感路径:消息序列化/反序列化零拷贝
spanstream 不替代 stringstream——当你的数据不在 string 中,或者你需要控制内存分配时,它是正确的选择。
3. std::optional 的 Monadic 扩展
C++23 为 std::optional 添加了和 std::expected 同款的 monadic 操作:
std::optional<int> parse(std::string_view s) {
try { return std::stoi(std::string(s)); }
catch (...) { return std::nullopt; }
}
auto result = parse("42")
.and_then([](int x) -> std::optional<double> { // 返回 optional
if (x == 0) return std::nullopt;
return 100.0 / x;
})
.transform([](double x) { return x * 2; }) // 映射值
.or_else([] { return std::optional<double>{0.0}; }); // fallback
// result == 100.0 / 42 * 2optional 的 monadic 操作和 expected 的结构完全一致,区别只是没有 error 类型——失败只用一个 nullopt 表示。操作链的名称为 and_then、transform、or_else,一致性好。
4. C++23 其他值得关注的特性
| 特性 | 简要说明 |
|---|---|
std::ranges::to<T>() | 将 view 直接转为容器 |
std::print / std::println | 类型安全、支持 Unicode 的格式化输出(基于 std::format) |
std::flat_map / std::flat_set | 基于连续内存存储,缓存友好 |
std::generator<T> | 基于 co_yield 的同步生成器 |
import std; | 一个 import 导入全部标准库(module 形式) |
deducing this | 显式对象参数,消除 const& / & / && 的成员函数四重载 |
这些特性一起让 C++23 成为了一个"质量建设"的标准——不需要重新学习语言,但日常代码会变简洁很多。
总结
std::expected 解决"我想返回值,但有时会失败"的问题,让错误处理从 if/else 森林变成了可组合的管道。std::spanstream 填补了流系统与 span 之间的空缺,让字符串格式化/解析可以完全免去堆分配。std::optional 的 monadic 扩展让"多个可能失败的步骤"的串联变得像写连续代码一样自然。
这三个特性共享一个主题:在不引入新语法和运行时开销的前提下,让类型系统帮你梳理控制流。