C++23 是一版「补齐短板」的标准:它没有引入全新的心智模型,而是把十几年来大家反复手写的模式收进标准库。这篇挑四个最实用的讲清楚:std::expected(值或错误)、std::print(类型安全输出)、deducing this(显式对象参数)、monadic optional(可空值链式处理)。它们都需要 C++23 编译器;其中 deducing this 在 gcc 14 之后才完整支持,正文里会单独标版本。
1. 引子:错误处理的两难
写一个「解析用户输入成整数」的函数,返回值放不下「出错了」这个信息。三种老办法各有代价:返回int出错返回 0(分不清「合法的 0」和「出错的 0」)、抛异常(错误是罕见路径时太重)、返回std::optional<int>(能表示「没有值」,但说不出为什么没有)。std::expected<T, E>就是冲着第三种的缺口来的:它要么持有T(值),要么持有E(错误),两个通道都显式存在。
std::expected<int, std::string> ┌──────────────────────────────────┐ │ 有值(has_value() == true) │──── value() -> int │ or │ │ 有错(has_value() == false) │──── error() -> std::string └──────────────────────────────────┘ ↑ 两种状态,占一个对象官方文档:std::expected — cppreference
下面用「除零」这个最经典的场景跑一遍:除数合法返回商,除数为零返回一句错误描述。
// verify: std=c++23#include<expected>#include<iostream>#include<string>std::expected<int,std::string>divide(inta,intb){if(b==0)returnstd::unexpected{"divide by zero"};returna/b;}intmain(){autook=divide(10,2);autobad=divide(10,0);std::cout<<"ok.has_value()="<<ok.has_value()<<" value="<<ok.value()<<'\n';std::cout<<"bad.has_value()="<<bad.has_value()<<" error="<<bad.error()<<'\n';std::cout<<"bad.value_or(-1)="<<bad.value_or(-1)<<'\n';}ok.has_value()=1 value=5 bad.has_value()=0 error=divide by zero bad.value_or(-1)=-1std::unexpected{"divide by zero"}是构造错误态的方式;读值时用value()(错误态会抛std::bad_expected_access)或value_or(-1)(错误态返回兜底值)。它和optional的差别,以及和各错误策略的取舍,见下表。
| 策略 | 能表达「为什么错」 | 错误是否强制处理 | 高频可预期错误的代价 | 适用 |
|---|---|---|---|---|
| 返回错误码 / 哨兵值 | 否(或靠约定) | 否,容易漏判 | 低 | C ABI、性能敏感的内层循环 |
| 抛异常 | 是(异常类型) | 是(不 catch 就崩) | 高(栈展开) | 真正的「非正常路径」 |
std::optional<T> | 否 | 否 | 低 | 只需要「有没有」 |
std::expected<T,E> | 是 | 否(但错误和值都在类型里,显式) | 低 | 库边界、可预期失败 |
expected适合「失败是可预期的、调用方必须知道」的场景——它把「值 or 错误」写进类型签名,编译器帮你记得处理。
2. std::print:类型安全、格式化的输出
std::cout << "x = " << x << " and y = " << y << '\n'这种写法,把格式和变量混在一串<<里,既啰嗦又难本地化。C++23 引入std::print/std::println,用法和std::format一致:{}是占位符,{:.3f}是格式化规格。
// verify: std=c++23#include<print>intmain(){std::print("{} + {} = {}\n",2,3,2+3);std::println("pi = {:.3f}",3.1415926);std::println("{}","print 不刷缓冲,比 endl 快");}2 + 3 = 5 pi = 3.142 print 不刷缓冲,比 endl 快官方文档:std::print / std::println — cppreference
注意std::print没有std::endl那种「顺手刷缓冲」的副作用。这里面的差异是个性能考点:std::endl会强制 flush,而print只负责写入,刷新交给系统按需完成。格式字符串写错(比如{}数量对不上参数)在 C++23 里编译期就能报错,这是 iostream 做不到的。
3. deducing this:显式对象参数
传统成员函数里,this是隐式的,你拿不到它的类型,也没法在成员函数上做「按值 vs 按引用」的分派。deducing this(显式对象参数,explicit object parameter)把this变成第一个显式参数,写this auto const& self或this Point& self。
// verify: std=c++23// verify: compiler=gcc-head#include<iostream>structPoint{intx{1},y{2};autonorm(thisautoconst&self){returnself.x*self.x+self.y*self.y;}int&pick(thisPoint&self,booluseX){returnuseX?self.x:self.y;}};intmain(){Point p;std::cout<<"norm="<<p.norm()<<'\n';p.pick(true)=10;std::cout<<"x="<<p.x<<" y="<<p.y<<'\n';}norm=5 x=10 y=2官方文档:Member functions — cppreference(其中「Explicit object parameter」一节)
它最大的价值是免掉 CRTP(curiously recurring template pattern,奇异递归模板模式)。以前想在基类里拿到派生类类型,得写template<class Derived> struct Base { ... };再struct D : Base<D> {};现在一个this auto&& self就够了,调用时self会自动推导成实际对象类型。
| 写法 | 拿到派生类类型的方式 | 样板量 |
|---|---|---|
| 传统成员函数 | 拿不到,this固定为当前类指针 | 无 |
| CRTP | static_cast<Derived&>(*this) | 多(模板基类 + 继承声明) |
| deducing this | this auto&& self自动推导 | 少 |
(deducing this 需要 gcc 14+ 或 clang 18+,所以这个示例指定了compiler=gcc-head,其余示例用默认的 clang-17 即可。)
4. monadic optional:可空值的链式处理
处理「可能没有值」时,最常见的写法是一串if (opt) { ... } else { ... }。C++23 给std::optional补上 monadic(单子式)操作:and_then(有值则继续、返回新 optional)、transform(有值则映射、原地包裹)、or_else(无值则走兜底)。
// verify: std=c++23#include<iostream>#include<optional>#include<string>std::optional<int>toInt(conststd::string&s){try{returnstd::stoi(s);}catch(...){returnstd::nullopt;}}intmain(){autor=std::optional<std::string>{"42"}.and_then(toInt).transform([](intx){returnx+1;}).or_else([]{returnstd::optional<int>{0};});std::cout<<"pipeline(\"42\") = "<<*r<<'\n';autor2=std::optional<std::string>{"abc"}.and_then(toInt).transform([](intx){returnx+1;}).or_else([]{returnstd::optional<int>{0};});std::cout<<"pipeline(\"abc\") = "<<*r2<<'\n';}pipeline("42") = 43 pipeline("abc") = 0官方文档:std::optional::and_then — cppreference
整个「字符串 → 整数 → 加一」的流水线写成一串,空值在任意一步短路、最后用or_else收口,不再需要嵌套if。expected也有同样的一套(and_then/transform/or_else/transform_error),所以学了 optional 的这套,expected 直接复用。
| 操作 | 有值时 | 无值时 | 返回类型 |
|---|---|---|---|
transform(f) | f(v)包进 optional | 保持空 | optional<decltype(f(v))> |
and_then(f) | f(v)(f 自己返回 optional) | 保持空 | optional<...>(f 的返回) |
or_else(f) | 保持原值 | f()(f 返回 optional) | 与原 optional 同类型 |
5. 完整示例:四个特性一起用
把一个「读取配置 → 解析整数 → 打印结果」的流程串起来,覆盖这篇全部要点。
// verify: std=c++23#include<expected>#include<optional>#include<print>#include<string>std::optional<int>parse(conststd::string&s){try{returnstd::stoi(s);}catch(...){returnstd::nullopt;}}intmain(){// optional 链:解析失败兜底 8080autoport=parse("9090").and_then([](intx){returnstd::optional<int>{x};}).or_else([]{returnstd::optional<int>{8080};});// expected 链:范围校验,非法走错误通道autocheck=[](intp)->std::expected<int,std::string>{if(p<1||p>65535){returnstd::expected<int,std::string>{std::unexpected{"port out of range"}};}returnstd::expected<int,std::string>{p};};autoresult=std::expected<int,std::string>{*port}.and_then(check);autobad=std::expected<int,std::string>{99999}.and_then(check);std::println("port = {}",result.value());std::println("bad has_error={} err={}",bad.has_value(),bad.error());}port = 9090 bad has_error=false err=port out of range两段链各司其职:optional 的and_then/or_else处理「有没有值」,expected 的and_then处理「值合不合法」。注意 optional 的and_then要求返回 optional、expected 的and_then要求返回 expected,两者不能混用,这也是类型系统在帮你把「可空」和「可错」这两件事分开。
6. 延伸阅读
- std::expected — cppreference:值或错误的完整接口,含 monadic 操作。
- std::print — cppreference:格式字符串语法与
std::format同源。 - Member functions (deducing this) — cppreference:显式对象参数的语法与推导规则。
- std::optional::and_then — cppreference:monadic optional 的入口,同页能跳到
transform/or_else。
本知识库内的相关篇目:
- 《C++20 升 C++23 迁移清单:开关、破坏性改动与收益》 —— 从 C++20 升到 C++23 是「低风险、有正收益」的一次迁移
- 《前瞻:std::generator、reflection 与 C++ 的未来方向》 —— C++23 之后,C++26 正在路上
- 《C++ 错误处理策略:异常 vs 错误码 vs expected 的真实取舍》 —— 函数失败到底该抛异常、返回错误码顺带把工程与架构这条线也铺一下。
7. 一句话总结
std::expected补上「值 or 错误」的缺口,std::print让输出类型安全又简洁,deducing this 消灭 CRTP 样板(需 gcc 14+),monadic optional 让可空值能像流水线一样链式处理——这四个是 C++23 里最值得先上手、也最能立刻替换掉手写样板的一组特性。