C++23 不是终点。往前看,C++26 已经有两块大饼在烤:std::generator把协程变成写惰性序列的日常工具(这一条 C++23 就落地了),而 static reflection 和 contracts 则试图补上 C++ 一直缺的两块:「让程序在编译期审视自己的类型」和「把契约写进函数签名」。这篇按「已能用 → 即将来 → 现在怎么办」三层拆,能跑的部分真跑,没跑起来的部分如实标。
1. std::generator:协程做惰性序列(C++23,已实测)
传统写法想生成一个序列,要么一次性算好塞进std::vector(急切求值,占内存),要么自己写迭代器(样板多)。std::generator<T>用协程把「产生下一个值」写成co_yield,值是在被请求时才一个一个算出来的:惰性求值(lazy evaluation)。
调用方 for (int v : fib()) 生成器内部 ───────────────────────── ───────────────── 请求下一个值 ──────────► co_yield a; (挂起,交出 a) 拿到 a 继续循环 ◄────────── 恢复执行,算下一个 请求下一个值 ──────────► co_yield a; (再次挂起) ... ... break / 结束 ◄────────── 协程结束,生成器销毁官方文档:std::generator — cppreference
下面用「无穷斐波那契序列」演示:生成器本身是个死循环,但调用方break就停,前面的值只算了 8 个。
// verify: std=c++23// verify: compiler=gcc-head#include<generator>#include<print>std::generator<int>fib(){inta=0,b=1;while(true){co_yielda;intnext=a+b;a=b;b=next;}}intmain(){intcount=0;for(intv:fib()){if(count++==8)break;std::print("{} ",v);}std::print("\n");}0 1 1 2 3 5 8 13fib()里是while (true),但程序正常结束,因为co_yield把控制权交还给调用方,调用方break后生成器被析构,协程随之销毁。std::generator需要<generator>头文件和 gcc 14+/libstdc++ 14 以上(clang 17 还没有),所以这里显式指定compiler=gcc-head。
| 方式 | 求值时机 | 内存 | 写法 | 适用 |
|---|---|---|---|---|
std::vector | 急切,一次算完 | 存全部 | 简单 | 序列短、要随机访问 |
| 手写迭代器 | 惰性 | 按需 | 样板多 | 需要完全控制 |
std::generator | 惰性,co_yield | 只存当前状态 | 最简 | 流式/无限/大序列 |
2. static reflection:让类型「看清自己」(P2996,C++26 候选,主流编译器未实现)
C++ 到今天都没有真正的反射(reflection):你没法在编译期遍历一个类的成员名字。这导致序列化、ORM、日志框架都得靠宏或外部工具。C++26 的候选方案是 static reflection(P2996),核心是一个^运算符:^int拿到int类型的反射,[:...:]把反射「拼回」代码。
// P2996 提案里的示意语法(C++26 候选,主流编译器尚未实现,实测报错见下文)template<classT>std::stringdescribe(){std::string out;for(automember:^T){// 遍历 T 的成员反射out+=[:member:];// 拼出成员名}returnout;}上面这段跑不起来,这次有实测证据:把^反射语法丢给主流编译器,gcc 15(-std=c++23)直接报错:
error: expected primary-expression before '^' tokenclang 的实验分支(clang_bb_p2996,即 godbolt 上的 “reflection - C++26”)同样没完整实现:^Point报error: expected expression,template for循环还要额外的-fexpansion-statements。所以结论是:P2996 的^反射运算符目前主流编译器和实验分支都还没落地。要确认真实进度,看 cppreference 的编译器支持表(cpp/compiler_support)里 reflection 那一栏;想尝鲜,godbolt 上已有clang_bb_p2996、edg-experimental-reflection、gcc-reflection-trunk这几个实验编译器,但语法还不稳定,别在生产代码里依赖。
官方文档:C++ compiler support — cppreference:reflection / contracts 在各编译器的实现状态,迁移前先查这里。
它和std::generator的区别在于成熟度:generator 已经进了 C++23 标准、能跑;reflection 还在提案阶段,语法都可能再改。所以「前瞻」的正确姿势是:了解它要解决什么问题,但别在它稳定前依赖它。
3. contracts:把契约写进签名(gcc 15 已实验支持)
和 reflection 并列的另一块是 contracts(契约,P2900 系列):给函数声明前置条件(precondition)、后置条件(postcondition)和不变量(invariant),让「调用者必须保证什么」成为类型系统的一部分,而不是注释。好消息是 contracts 不再只是「未来」:gcc 15 已经实验性支持(加-fcontracts编译),这篇实测:
// contracts 的 attribute 语法(gcc 15 加 -fcontracts 可实测)// verify: skip (需要 gcc 15 的 -fcontracts,Wandbox 默认 gcc 13.2 不支持 contracts)#include<cstdio>intf(intx)[[pre:x>0]][[post r:r>0]]{returnx*2;}intmain(){std::printf("f(5)=%d\n",f(5));std::printf("f(-1)=%d\n",f(-1));// 违反前置条件 x > 0return0;}f(5)=10 contract violation in function f at /app/example.cpp:2: x > 0 terminate called without an active exception Program terminated with signal SIGABRT (6)f(5)正常返回10;f(-1)违反前置条件x > 0,直接触发contract violation并std::terminate(SIGABRT)。这就是 contracts 的价值,把断言从「函数体里的一行assert」升级为「接口的一部分」:编译器可以在调用处检查、在定义处检查,甚至静态推导。
不过要清醒:gcc 之外的主流编译器(clang 等)还没稳定实现,contracts 语法在 P2900 / P3850 之间还有变动,所以生产落地仍要谨慎。在它普及之前,替代是assert,但它只是运行期检查、契约只活在注释里:
// 现在的替代:契约只能写在注释里,靠 assert 在运行期兜底(片段)#include<cassert>intdivide(inta,intb){// 前置条件:b != 0assert(b!=0);// 运行期检查,Release 下可能被 -DNDEBUG 删掉returna/b;}assert的问题在于:它默认只在 Debug 生效(-DNDEBUG会删掉),而且调用者看不到「我该保证什么」。契约写在函数签名里,编译器才能替你在调用处强制检查。
4. 现在的替代方案:没有反射怎么办
在 reflection 落地之前,想在「序列化 / 打印所有字段」这类场景里遍历成员,主流的替代是 X-macro(X 宏):把字段列成一张表,用一个宏反复展开。
// verify: std=c++17#include<iostream>#include<string>#defineFIELDS(X)\X(std::string,name)\X(int,age)structPerson{#defineDECL(type,field)type field;FIELDS(DECL)#undefDECL};intmain(){Person p{"miao",3};#definePRINT(type,field)std::cout<<#field<<"="<<p.field<<' ';FIELDS(PRINT)#undefPRINTstd::cout<<'\n';}name=miao age=3FIELDS这一张表既用来声明成员(DECL),又用来遍历打印(PRINT)。「声明一次、多处复用」就是它替代反射的思路。代价是字段类型只能写裸类型名(带逗号的模板类型会被宏参数切开),这也是为什么大家盼着真反射。
| 方案 | 能否遍历成员 | 类型能否带逗号 | 是否需要外部工具 |
|---|---|---|---|
| 手写序列化函数 | 否(每个字段手写) | 任意 | 否 |
| X-macro | 能(一张表展开) | 不能 | 否 |
| boost.pfr(结构化绑定推导聚合) | 能(限聚合) | 任意 | 是(第三方库) |
| static reflection(C++26) | 能 | 任意 | 否(语言内置) |
5. 完整示例:惰性序列 + 过滤
把std::generator的「惰性」用一个更实用的场景收尾:生成无穷平方数,取其中偶数的前 5 个。生成器不知道、也不关心调用方只要 5 个,它只是被动地「被要一个、给一个」。
// verify: std=c++23// verify: compiler=gcc-head#include<generator>#include<print>std::generator<int>squares(){for(inti=1;;++i)co_yieldi*i;}intmain(){intcount=0;for(intv:squares()){if(v%2==0){std::print("{} ",v);if(++count==5)break;}}std::print("\n");}4 16 36 64 100squares()理论上会算到天荒地老,但程序只跑到第 10 个平方数(100)就break停手。前面那些值都是「现要现算」,这正是惰性序列和一次性vector的本质区别。
6. 延伸阅读
- std::generator — cppreference:C++23 已落地的生成器,接口和语义都在这一页。
- Coroutines — cppreference:
co_yield/co_await/co_return的底层机制,generator 的地基。 - C++ compiler support — cppreference:查 reflection / contracts 何时能用,是「前瞻」类内容的唯一权威进度表。
本知识库内的相关篇目:
- 《C++23 核心新特性:std::expected、std::print、deducing this、monadic optional》 —— C++23 没有 C++11/C++20 那种天翻地覆的变化,但补上了一批「日常高频」的拼图
- 《C++20 升 C++23 迁移清单:开关、破坏性改动与收益》 —— 从 C++20 升到 C++23 是「低风险、有正收益」的一次迁移
- 《编译期反射模拟:C++ 没有反射怎么办》 —— C++ 至今没有运行时反射,想「遍历结构体成员」「自动序列化」只能靠模拟顺带把高级模板与编译期这条线也铺一下。
7. 一句话总结
std::generator是现在就能用的惰性序列工具(gcc 14+,co_yield写起来像 for 循环);contracts 在 gcc 15 已能实验性用(加-fcontracts);reflection 还是 C++26 的未来方向,主流编译器都没实现(^报错),所以务实的态度是:先用 generator 把协程吃透,contracts 可以在新编译器上尝鲜,reflection 落地前继续用宏和结构化绑定,别在未稳定的语法上押注。