C++23 - 最新特性
📑 C++ 演进系列:总览 | C++11 | C++14 | C++17 | C++20 | C++23 | C++26
C++23 是当前已发布的最新标准,继续完善和扩展 C++ 的功能。它在 C++20 的基础上查漏补缺:补齐 Ranges/Concepts/Modules 留下的短板,新增一批面向实际工程痛点的小特性(错误处理、格式化输出、容器适配),并进一步向"零开销抽象 + 类型安全"靠拢。下面逐项展开。
Deducing this(推导 this)
C++23 之前,成员函数若要同时支持 const/非 const、左值/右值重载,必须手写多份几乎相同的代码——典型的样板代码来源。Deducing this 允许把 this 显式写成第一个形参(this auto&& self 或 this const Self& self),由编译器根据调用对象的值类别与 const 性推导出 self 的确切类型。
这一特性带来三个实际收益:
- 消除 const/非 const 成对重载:访问器不再需要写两遍。
- 简化 CRTP:派生类不再需要把自己作为模板参数传给基类,基类通过
this auto&& self直接拿到派生类型。 - 统一完美转发成员函数:一个模板签名覆盖左值、右值、const 全部场景。
1 | |
注意 self 是对象本身的引用而非指针,调用成员直接用 .;并且它是显式形参,可以加 const、&&、noexcept 等修饰,比隐式 this 更灵活。
这个显式形参在标准中称为显式对象参数(explicit object parameter),所以该特性也常被叫做 “explicit this”。除了消除重载,它还解锁了几个进阶用法:
- 递归 lambda:lambda 没有
this,原本无法直接自引用递归;显式对象参数让 lambda 把自己作为形参传入,从而实现递归。 - 按值传递
self:写this auto self可获得对象的副本,适合值语义链式调用或不可变访问。 - 推导返回类型:
self的确切类型已知,配合decltype(auto)能正确转发 const/值类别到返回类型。
1 | |
对比 deducing this 出现前 C++ 实现递归 lambda 的两种迂回写法,能看出它为何"最优雅":
1 | |
std::function 方案有类型擦除开销,且依赖 [&] 捕获 fib 自身;自传递方案零开销,但调用点要写 fib(fib, ...),把 self 传来传去。deducing this 让 self 成为隐式对象参数,调用退化成普通的 fib(10)——既零开销又干净。
多语言视角下闭包递归的三种解法(命名引用 / 自传递 / 不动点组合子)见 闭包递归。
显式对象参数的引用形式有三种常见写法,差别在于对右值的处理:
| 显式对象参数 | 普通左值 w | const 左值 cw | 右值 Widget{} | const 右值 std::move(cw) |
|---|---|---|---|---|
this auto& self | Widget& ✓ | const Widget& ✓ | ✗ 推导失败 | ✗ |
this auto&& self | Widget& ✓ | const Widget& ✓ | Widget&& ✓ | const Widget&& ✓ |
this auto self | 按值拷贝 | 按值拷贝 | 按值拷贝 | 按值拷贝 |
关键点:auto&& 是转发引用(forwarding reference),而 auto& 不是。当实参是右值时,auto& 推导出的 Widget& 无法绑定到右值,该重载会被静默剔除(SFINAE 友好)。因此:
- 需要一个签名覆盖左值/右值/const 全部场景,或配合
std::forward<decltype(self)>(self)完美转发成员调用 -> 用auto&&。 - 明确只想处理左值,并希望拒绝右值调用(例如阻止对临时对象执行有副作用的操作)-> 用
auto&。
1 | |
注意限制:显式对象参数不能用于 virtual 函数(虚派发依赖隐式 this);同一函数也不能同时有显式对象参数版本和隐式 this 重载。
if consteval
C++20 的 std::is_constant_evaluated() 能在函数内判断当前是否处于常量求值上下文,但它是个运行期函数调用,语法上不够直观,且在某些 constexpr 上下文里行为微妙(比如它本身也能在编译期返回 false 时仍要求分支可编译)。C++23 引入 if consteval 作为关键字形式的等价物,语义更清晰:编译期走 if 分支,运行期走 else 分支。
典型用途:同一个 constexpr 函数在编译期用通用但可移植的实现、运行期用平台相关的高效内联(如向量指令、位操作技巧)。两套实现共享同一签名,调用方无感。
1 | |
注意:if consteval 不接受条件表达式(区别于 if constexpr (...)),它本身就是"是否常量求值"的判断;分支里仍需保证语法合法。
auto(x) 和 auto{x}
C++23 把 auto 升级成可作用于表达式的"衰减拷贝"操作符:auto(x) 对 x 做一次类型衰减(array→pointer、引用剥离)后按值构造副本;auto{x} 类似但采用花括号初始化(禁止窄化转换)。它等价于 decay_t<decltype(x)>(x),但写法更短、意图更明确。
主要用途是在泛型代码里显式制造一份按值持有的拷贝,避免引用悬挂或意外修改原对象——例如把范围 for 的循环变量、回调捕获的值显式拷贝出来,或在模板里把参数"固化"为值类型。
1 | |
括号形式 auto(x) 允许窄化(与构造语义一致),花括号形式 auto{x} 拒绝窄化——选择哪一种取决于是否需要这种安全网。
静态运算符(Static Operators)
C++23 允许把 operator()、operator[] 等小括号运算符声明为 static。对于无状态的可调用对象(不访问任何成员),static operator() 消除了隐式 this 形参:调用时不传对象指针、无虚调用开销,并且能直接转换为普通函数指针,无需 + 技巧。
这使无状态仿函数真正等同于自由函数,同时保留了可被模板推导、可内联的优势。标准库 std::less、std::plus 等也借此优化。
1 | |
注意 static operator() 只适用于不需要访问 this 的场景;一旦需要读成员,仍用普通非静态形式。二元运算符的两个操作数都作为显式参数传入。
多维 operator[]
C++23 之前,operator[] 只能接受单个参数,访问多维数据只能链式 m[i][j][k](每层返回代理对象,开销与可读性都差)或手写 at(i,j,k)。C++23 允许 operator[] 接收多个形参(用逗号分隔,注意逗号在此是形参分隔符而非逗号运算符),可以直接写 m[i, j, k]。
这对矩阵、张量、图像缓冲、多维数组视图(配合 std::mdspan)尤其有用——一次调用拿到元素,无需中间代理层。
1 | |
实际中更常见的写法是 T& operator[](std::size_t i, std::size_t j, std::size_t k),直接返回元素引用。这里配合 requires 约束展示了与 Concepts 的组合用法。
[[assume]] 属性
[[assume(expr)]] 向编译器声明"表达式 expr 在此处一定为真",编译器据此做优化(消除分支、简化范围分析)。它与 assert 的关键区别:assert 在调试期检查、失败时中止;[[assume]] 不生成任何检查代码,若假设为假则是未定义行为——编译器会基于错误前提推导出任意结果。
适用场景:你通过契约/前置条件确知某条件成立,但编译器无法静态推断时(如除数非零、索引在范围内、枚举取值集合)。务必仅在确信时使用,否则会引入隐蔽的 UB。
1 | |
第二个例子展示了"条件分支后假设为假"的用法:进入某分支意味着前置条件不成立,编译器据此收窄 value 的取值范围。仅在确信条件恒成立时使用——错误的假设比没有假设更危险。
size_t 字面量后缀
C++23 为整数字面量新增 uz/UZ 后缀,使其类型为 std::size_t。在此之前,0 是 int、0u 是 unsigned int、0ul 是 unsigned long——都与 size_t 不完全匹配,在和 size()、容器索引比较时常触发有符号/无符号警告或隐式转换。
uz 后缀让"这是个尺寸/索引"的意图直接体现在字面量上,消除 static_cast<std::size_t>(...) 这类啰嗦写法,也避免跨平台 size_t 宽度不同(32/64 位)带来的移植问题。
1 | |
写循环计数器与容器 size() 比较时,用 for (std::size_t i = 0uz; i < vec.size(); ++i) 比混用 int 更安全。
std::print 和 std::println
C++23 把 {fmt} 库的核心能力纳入标准:std::print / std::println 提供类型安全、编译期检查格式串的格式化输出,取代既啰嗦又易错、且每次刷新缓冲区的 std::cout << ... << std::endl。
优势:格式串在编译期校验(参数数量/类型不匹配直接报错),性能优于 iostream(无 locale 切换、无虚调用),语法与 Python format/{fmt} 一致、学习成本低。println 自动追加换行,也可指定输出流。
1 | |
常用格式说明符:{} 默认、{:.2f} 定点两位小数、{:b} 二进制、{:x} 十六进制、{:>10} 右对齐宽度 10、{:#x} 带前缀的十六进制。自定义类型可通过特化 std::formatter 接入。
标准库模块
C++23 标准化了标准库的模块接口:import std; 一次性导入整个标准库,import std.compat; 额外导入 C 兼容头(如 <cstdio> 对应的 stdio.h)。这取代了上千次 #include,从根本上解决了头文件重复解析的编译性能问题——模块是预编译的语义实体,导入不重复展开、不泄漏宏、不影响编译顺序。
对大型项目,模块可显著缩短编译时间(头文件解析在 C++ 编译耗时中占比极高),并消除宏污染与头文件循环依赖。代价是构建系统需要支持模块扫描(CMake 有相应支持),工具链集成仍在完善中。
1 | |
注意:模块与头文件可以混用,但宏无法跨模块边界传递——这是特性而非缺陷,强制了更好的封装。
std::expected
std::expected<T, E> 是类型安全的错误处理类型:要么持有一个成功值 T,要么持有一个错误值 E。它填补了"异常过重、返回码无类型约束、optional 无错误详情"之间的空白——适合可预期、需调用方显式处理的失败(解析失败、文件不存在、网络超时)。
与异常的分工:异常用于真正意外、跨层、罕见的错误;expected 用于业务流程内可预期的失败分支。它强制调用方处理错误(不像返回码可被忽略),又没有异常的运行期开销。
C++23 还为它补齐了 monadic 接口:and_then(成功时映射到新的 expected)、or_else(失败时映射)、transform(变换成功值)、transform_error(变换错误值),可像管道一样串联,避免层层 if。
1 | |
常用接口:has_value()/operator bool 判成功、value() 取值(失败抛异常)、error() 取错误、value_or(x) 取值或默认。配合 monadic 操作能让错误处理像流水线一样表达。
std::generator
std::generator 是 C++23 标准化的协程生成器,用于惰性产生值序列。它实现了 std::ranges::input_range,可直接用于范围 for 和 Ranges 算法。在此之前,写惰性序列要么手写迭代器(繁琐)、要么依赖第三方库(如 cppcoro)。
优势:惰性求值(用到一个算一个,不预先生成整个容器)、内存友好(无限序列也能表示)、可组合(生成器可调用其他生成器)。适合流式处理、大数据遍历、按需计算。
1 | |
co_yield 产出单个值并挂起协程,下次迭代时从挂起点恢复。生成器默认是单遍输入范围;要取前 N 项配合 std::views::take(N)。
std::mdspan
std::mdspan 是多维数组的非拥有视图,类比一维的 std::span 推广到 N 维。它把一段连续内存解释成指定维度的多维数组,不拷贝数据、零开销。std::extents 描述维度(可静态固定或动态运行期指定),布局(layout_right/layout_left/layout_stride)控制下标到偏移的映射。
适用场景:科学计算、图像处理、机器学习中的张量操作——在不复制数据的前提下给裸缓冲套上多维语义。配合 C++23 多维 operator[],可写 mat[i, j] 直接访问。
1 | |
submdspan 生成子视图(切片、子块),同样不复制数据。dextents 表示全部维度动态,extents 可混用静态/动态维度。
std::flat_map 和 std::flat_set
std::flat_map / std::flat_set 是 C++23 引入的有序容器适配器:底层用排序的连续容器(默认 vector)存储,对外提供与 std::map/std::set 一致的接口。本质是"有序数组 + 二分查找"。
权衡:查找是 O(log n) 二分但连续内存缓存友好(实际常快于基于节点的 map/set);插入/删除是 O(n) 需要移动元素。因此**适合"大量查找、少量修改"或"批量构建后只读"**的场景——典型如配置表、枚举映射、启动期构建的查找表。修改频繁时仍应选 std::map/std::unordered_map。
1 | |
底层容器可定制(如换 deque),通过模板参数指定。contains、equal_range 等接口与 map/set 对齐,迁移成本低。
std::byteswap
std::byteswap 反转整数的字节序,是 C++23 对 <bit> 的小补充。在此之前,字节序转换要么手写移位、要么用平台相关的 htonl/ntohl(非标准 C++)。std::byteswap 是纯标准、constexpr、类型安全的方案。
主要用途:网络/文件/跨平台数据交换时的大小端转换。注意它只是机械地反转字节,不关心"当前机器是什么端序"——是否需要转换取决于数据来源端序与目标端序是否一致。判断本机端序用 std::endian::native。
1 | |
std::byteswap 是 constexpr,可在编译期使用;对 8 位类型返回原值(无字节可换)。
std::to_underlying
std::to_underlying(e) 把枚举值转换为其底层类型,取代啰嗦且易写错的 static_cast<int>(e)。C++23 之前,从 enum class 取出底层值必须显式转型,写多了既丑又容易把目标类型写错(如误转成 int 而实际底层是 unsigned)。std::to_underlying 自动推导正确底层类型,意图清晰。
适合在与 C API 交互、序列化、日志输出枚举值时使用。配合 enum class 的强类型语义,既安全又简洁。
1 | |
返回类型是枚举的底层类型(由声明决定,此处为 int),无需调用方记忆。
std::string::contains
C++23 给 std::string、std::string_view(以及 std::ranges 子范围)补上了 contains 成员函数,返回 bool 表示是否包含某子串或字符。在此之前,判断包含只能写 str.find("x") != std::string::npos——既冗长又容易忘记 npos 比较、误写成 find 返回 0 被当假值的坑。
contains 直接表达意图,是"小特性大改善"的典型:消除了一个长期存在的样板写法。
1 | |
接受子串(std::string/string_view/const char*)或单个字符,复杂度同 find(最坏 O(n·m)),日常用法足够。
std::unreachable
std::unreachable() 标记"程序执行到此即未定义行为",向编译器声明此点不可达,可用于优化。它与 [[assume]] 的关系:[[assume(false)]] 在语义上等价于 unreachable(),后者是更显式的独立工具。
典型用途:switch 覆盖了枚举所有取值后,default 分支标记不可达——编译器据此消除多余的范围检查,运行期若意外落入则触发 UB(通常 abort)。比手写 __builtin_unreachable() 可移植。务必仅在确实覆盖所有情况时使用,否则会掩盖真实 bug。
1 | |
第二个例子展示了"前置判断后剩余路径不可达"的用法。注意 unreachable() 是 <utility> 中的函数,调用它本身不抛异常——它直接是 UB,编译器可能生成任意代码。
std::stacktrace
<stacktrace> 提供运行期调用栈采集能力,是 C++20 std::source_location(单点位置)的自然延伸——后者只记录"当前这一行",std::stacktrace 则给出"是怎么走到这一行的"完整调用链。每帧是一个 std::stacktrace_entry,可拿到文件名、行号、函数名(与 source_location 字段对应)。
核心接口:std::stacktrace::current() 捕获当前调用栈,返回一个可迭代的 std::stacktrace 容器;entry.description() 给出可读的函数签名/符号名。
1 | |
工程上注意三点:采集调用栈有性能开销(通常毫秒级),不要放进热路径;符号化(函数名、源码位置)依赖调试信息,发布构建里 description() 可能只给地址;编译器/链接器需开启支持(如 GCC 需额外链接 -lstdc++_exp,MSVC 默认可用但符号化需要 PDB),各平台成熟度不一。
常用特性总结
最常用的 C++23 特性:
std::print和std::printlnstd::expectedstd::generatorstd::flat_map和std::flat_set- 标准库模块(
import std) std::byteswapstd::string::containsstd::stacktrace(调试诊断)









