📑 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&& selfthis const Self& self),由编译器根据调用对象的值类别与 const 性推导出 self 的确切类型。

这一特性带来三个实际收益:

  1. 消除 const/非 const 成对重载:访问器不再需要写两遍。
  2. 简化 CRTP:派生类不再需要把自己作为模板参数传给基类,基类通过 this auto&& self 直接拿到派生类型。
  3. 统一完美转发成员函数:一个模板签名覆盖左值、右值、const 全部场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Widget {
// self 的类型由调用点推导:左值 → Widget&,右值 → Widget&&,const 左值 → const Widget&
void process(this auto&& self) {
self.doSomething();
}

void doSomething() & {
std::cout << "Lvalue\n";
}

void doSomething() && {
std::cout << "Rvalue\n";
}
};

Widget w;
w.process(); // Lvalue
Widget{}.process(); // Rvalue

注意 self 是对象本身的引用而非指针,调用成员直接用 .;并且它是显式形参,可以加 const&&noexcept 等修饰,比隐式 this 更灵活。

这个显式形参在标准中称为显式对象参数(explicit object parameter),所以该特性也常被叫做 “explicit this”。除了消除重载,它还解锁了几个进阶用法:

  • 递归 lambda:lambda 没有 this,原本无法直接自引用递归;显式对象参数让 lambda 把自己作为形参传入,从而实现递归。
  • 按值传递 self:写 this auto self 可获得对象的副本,适合值语义链式调用或不可变访问。
  • 推导返回类型self 的确切类型已知,配合 decltype(auto) 能正确转发 const/值类别到返回类型。
1
2
3
4
5
// 递归 lambda:计算斐波那契
auto fib = [](this auto& self, int n) -> int {
return n <= 1 ? n : self(n - 1) + self(n - 2);
};
fib(10); // 55

对比 deducing this 出现前 C++ 实现递归 lambda 的两种迂回写法,能看出它为何"最优雅":

1
2
3
4
5
6
7
8
9
10
// 1. 命名引用:必须 std::function,忌 auto(auto 推导时 fib 尚未定义,类型未知)
std::function<int(int)> fib = [&](int n) -> int {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
};

// 2. 自传递(C++14 泛型 lambda,零开销):self 显式作为参数
auto fib = [](auto& self, int n) -> int {
return n <= 1 ? n : self(self, n - 1) + self(self, n - 2);
};
fib(fib, 10);

std::function 方案有类型擦除开销,且依赖 [&] 捕获 fib 自身;自传递方案零开销,但调用点要写 fib(fib, ...),把 self 传来传去。deducing this 让 self 成为隐式对象参数,调用退化成普通的 fib(10)——既零开销又干净。

多语言视角下闭包递归的三种解法(命名引用 / 自传递 / 不动点组合子)见 闭包递归

显式对象参数的引用形式有三种常见写法,差别在于对右值的处理:

显式对象参数普通左值 wconst 左值 cw右值 Widget{}const 右值 std::move(cw)
this auto& selfWidget&const Widget&✗ 推导失败
this auto&& selfWidget&const Widget&Widget&&const Widget&&
this auto self按值拷贝按值拷贝按值拷贝按值拷贝

关键点:auto&&转发引用(forwarding reference),而 auto& 不是。当实参是右值时,auto& 推导出的 Widget& 无法绑定到右值,该重载会被静默剔除(SFINAE 友好)。因此:

  • 需要一个签名覆盖左值/右值/const 全部场景,或配合 std::forward<decltype(self)>(self) 完美转发成员调用 -> 用 auto&&
  • 明确只想处理左值,并希望拒绝右值调用(例如阻止对临时对象执行有副作用的操作)-> 用 auto&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Widget {
void onlyLvalue(this auto& self) { // 仅左值:右值调用会被剔除
/* ... */
}
void anyValueCat(this auto&& self) { // 全值类别:可完美转发
std::forward<decltype(self)>(self).run();
}
};

Widget w;
const Widget cw;
w.onlyLvalue(); // OK,self 为 Widget&
cw.onlyLvalue(); // OK,self 为 const Widget&
Widget{}.onlyLvalue(); // ❌ 编译错误:auto& 无法绑定右值

w.anyValueCat(); // OK,self 为 Widget&
Widget{}.anyValueCat(); // OK,self 为 Widget&&

注意限制:显式对象参数不能用于 virtual 函数(虚派发依赖隐式 this);同一函数也不能同时有显式对象参数版本和隐式 this 重载。

if consteval

C++20 的 std::is_constant_evaluated() 能在函数内判断当前是否处于常量求值上下文,但它是个运行期函数调用,语法上不够直观,且在某些 constexpr 上下文里行为微妙(比如它本身也能在编译期返回 false 时仍要求分支可编译)。C++23 引入 if consteval 作为关键字形式的等价物,语义更清晰:编译期走 if 分支,运行期走 else 分支。

典型用途:同一个 constexpr 函数在编译期用通用但可移植的实现、运行期用平台相关的高效内联(如向量指令、位操作技巧)。两套实现共享同一签名,调用方无感。

1
2
3
4
5
6
7
8
9
10
11
12
constexpr int compute(int x) {
if consteval {
// 编译期:必须可移植、不依赖运行期硬件特性
return x * 2;
} else {
// 运行期:可以用 SIMD、查表、平台内置函数等
return x * 3;
}
}

constexpr int result = compute(5); // 10 (编译期)
int runtime = compute(5); // 15 (运行期)

注意: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
2
3
4
5
6
7
8
9
10
std::vector<int> vec = {1, 2, 3};

// 强制拷贝
auto copy = auto(vec); // vec 的副本

// 强制移动
auto moved = auto(std::move(vec)); // 移动构造

// 括号形式
auto copy2 = auto{vec};

括号形式 auto(x) 允许窄化(与构造语义一致),花括号形式 auto{x} 拒绝窄化——选择哪一种取决于是否需要这种安全网。

静态运算符(Static Operators)

C++23 允许把 operator()operator[] 等小括号运算符声明为 static。对于无状态的可调用对象(不访问任何成员),static operator() 消除了隐式 this 形参:调用时不传对象指针、无虚调用开销,并且能直接转换为普通函数指针,无需 + 技巧。

这使无状态仿函数真正等同于自由函数,同时保留了可被模板推导、可内联的优势。标准库 std::lessstd::plus 等也借此优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Number {
int value;

static Number operator+(Number a, Number b) {
return Number{a.value + b.value};
}

static Number operator-(Number a, Number b) {
return Number{a.value - b.value};
}
};

Number n1{1};
Number n2{2};
Number n3 = n1 + n2; // Number{3}
Number n4 = n1 - n2; // Number{-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C++23 之前
data[0][1][2];

// C++23:支持多维 operator[]
template<typename T>
class Tensor3D {
T* data_;
std::size_t dim1_, dim2_, dim3_;

public:
Tensor3D(T* data, std::size_t d1, std::size_t d2, std::size_t d3)
: data_(data), dim1_(d1), dim2_(d2), dim3_(d3) {}

// C++23 多维 operator[]
auto operator[](std::size_t i) requires std::is_array_v<T> {
return std::mdspan(data_ + i * dim2_ * dim3_, dim2_, dim3_);
}
};

实际中更常见的写法是 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
2
3
4
5
6
7
8
9
10
int divide(int a, int b) {
[[assume(b != 0)]]; // 告诉编译器 b 不为 0,可省略除零检查
return a / b;
}

int process(int value) {
if (value < 0 || value > 100) [[assume(false)]];
// 编译器知道 value 在 [0, 100] 范围内,可优化后续比较
return value * 2;
}

第二个例子展示了"条件分支后假设为假"的用法:进入某分支意味着前置条件不成立,编译器据此收窄 value 的取值范围。仅在确信条件恒成立时使用——错误的假设比没有假设更危险。

size_t 字面量后缀

C++23 为整数字面量新增 uz/UZ 后缀,使其类型为 std::size_t。在此之前,0int0uunsigned int0ulunsigned long——都与 size_t 不完全匹配,在和 size()、容器索引比较时常触发有符号/无符号警告或隐式转换。

uz 后缀让"这是个尺寸/索引"的意图直接体现在字面量上,消除 static_cast<std::size_t>(...) 这类啰嗦写法,也避免跨平台 size_t 宽度不同(32/64 位)带来的移植问题。

1
2
3
4
5
6
7
8
// C++23 字面量后缀
using namespace std::literals;

std::size_t a = 123uz; // size_t
std::size_t b = 0uz; // 空指针安全的 size_t

// 与 sizeof 配合
auto size = sizeof(int)uz;

写循环计数器与容器 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <print>

std::print("Hello, World!\n");
std::println("Hello, World!"); // 自动换行

// 格式化
std::println("Name: {}, Age: {}", "Alice", 30);

// 支持容器(范围自动以 [a, b, c] 形式输出)
std::vector<int> vec = {1, 2, 3};
std::println("Vector: {}", vec); // Vector: [1, 2, 3]

// 支持格式化选项
std::println("Pi: {:.2f}", 3.14159); // Pi: 3.14
std::println("Binary: {:b}", 42); // Binary: 101010

// 输出到文件
std::println(stdout, "To stdout");

常用格式说明符:{} 默认、{:.2f} 定点两位小数、{:b} 二进制、{:x} 十六进制、{:>10} 右对齐宽度 10、{:#x} 带前缀的十六进制。自定义类型可通过特化 std::formatter 接入。

标准库模块

C++23 标准化了标准库的模块接口:import std; 一次性导入整个标准库,import std.compat; 额外导入 C 兼容头(如 <cstdio> 对应的 stdio.h)。这取代了上千次 #include,从根本上解决了头文件重复解析的编译性能问题——模块是预编译的语义实体,导入不重复展开、不泄漏宏、不影响编译顺序。

对大型项目,模块可显著缩短编译时间(头文件解析在 C++ 编译耗时中占比极高),并消除宏污染与头文件循环依赖。代价是构建系统需要支持模块扫描(CMake 有相应支持),工具链集成仍在完善中。

1
2
3
4
5
6
7
8
9
// 导入标准库模块而不是头文件
import std;

// 不再需要 #include <iostream>, <vector>, 等
int main() {
std::println("Hello, World!");
std::vector<int> vec = {1, 2, 3};
return 0;
}

注意:模块与头文件可以混用,但宏无法跨模块边界传递——这是特性而非缺陷,强制了更好的封装。

std::expected

std::expected<T, E> 是类型安全的错误处理类型:要么持有一个成功值 T,要么持有一个错误值 E。它填补了"异常过重、返回码无类型约束、optional 无错误详情"之间的空白——适合可预期、需调用方显式处理的失败(解析失败、文件不存在、网络超时)。

与异常的分工:异常用于真正意外、跨层、罕见的错误;expected 用于业务流程内可预期的失败分支。它强制调用方处理错误(不像返回码可被忽略),又没有异常的运行期开销。

C++23 还为它补齐了 monadic 接口:and_then(成功时映射到新的 expected)、or_else(失败时映射)、transform(变换成功值)、transform_error(变换错误值),可像管道一样串联,避免层层 if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <expected>

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::println("Result: {}", *result);
} else {
std::println("Error: {}", result.error());
}

// 使用 value_or:失败时返回默认值
int value = divide(10, 0).value_or(-1);

// 链式操作:成功值翻倍,失败原样传播
auto final = divide(10, 2).transform([](int x) { return x * 2; });

常用接口:has_value()/operator bool 判成功、value() 取值(失败抛异常)、error() 取错误、value_or(x) 取值或默认。配合 monadic 操作能让错误处理像流水线一样表达。

std::generator

std::generator 是 C++23 标准化的协程生成器,用于惰性产生值序列。它实现了 std::ranges::input_range,可直接用于范围 for 和 Ranges 算法。在此之前,写惰性序列要么手写迭代器(繁琐)、要么依赖第三方库(如 cppcoro)。

优势:惰性求值(用到一个算一个,不预先生成整个容器)、内存友好(无限序列也能表示)、可组合(生成器可调用其他生成器)。适合流式处理、大数据遍历、按需计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <generator>

std::generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}

for (auto x : range(1, 5)) {
std::println("{}", x); // 1 2 3 4
}

// 无限序列:斐波那契,按需取前 N 个
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <mdspan>

int data[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

// 静态维度:3x4 矩阵视图(编译期已知形状)
std::mdspan<int, std::extents<std::size_t, 3, 4>> mat(data);

std::println("mat[1][2] = {}", mat[1][2]); // 7

// 动态维度:运行期指定形状
std::mdspan<int, std::dextents<std::size_t, 2>> dyn(data, 3, 4);

// 切片视图:取第 1 行的一部分
std::mdspan row = std::submdspan(mat, std::slice{1, 1, 2});

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <flat_map>
#include <flat_set>

// 扁平化容器,使用排序的 vector 存储
std::flat_map<std::string, int> map;
map["a"] = 1;
map["b"] = 2;

// 保持排序
map.sort(); // 按键排序

// 快速查找
if (map.contains("a")) {
auto it = map.find("a");
}

std::flat_set<int> set = {3, 1, 4, 1, 5}; // 自动去重和排序

// 范围操作
auto range = map.equal_range("b");

底层容器可定制(如换 deque),通过模板参数指定。containsequal_range 等接口与 map/set 对齐,迁移成本低。

std::byteswap

std::byteswap 反转整数的字节序,是 C++23 对 <bit> 的小补充。在此之前,字节序转换要么手写移位、要么用平台相关的 htonl/ntohl(非标准 C++)。std::byteswap 是纯标准、constexpr、类型安全的方案。

主要用途:网络/文件/跨平台数据交换时的大小端转换。注意它只是机械地反转字节,不关心"当前机器是什么端序"——是否需要转换取决于数据来源端序与目标端序是否一致。判断本机端序用 std::endian::native

1
2
3
4
5
6
7
8
#include <bit>

uint32_t x = 0x12345678;
uint32_t y = std::byteswap(x); // 0x78563412

// 大小端转换
uint16_t a = 0xABCD;
uint16_t swapped = std::byteswap(a); // 0xCDAB

std::byteswapconstexpr,可在编译期使用;对 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
2
3
4
5
6
7
8
9
#include <type_traits>

enum class Color : int { Red = 1, Green = 2, Blue = 3 };

Color c = Color::Red;
int value = std::to_underlying(c); // 1,自动推导底层类型

// 强转等价但更清晰
auto val = std::to_underlying(Color::Green); // 2

返回类型是枚举的底层类型(由声明决定,此处为 int),无需调用方记忆。

std::string::contains

C++23 给 std::stringstd::string_view(以及 std::ranges 子范围)补上了 contains 成员函数,返回 bool 表示是否包含某子串或字符。在此之前,判断包含只能写 str.find("x") != std::string::npos——既冗长又容易忘记 npos 比较、误写成 find 返回 0 被当假值的坑。

contains 直接表达意图,是"小特性大改善"的典型:消除了一个长期存在的样板写法。

1
2
3
4
5
6
7
8
9
10
11
12
std::string str = "Hello, World!";

// C++23 之前
if (str.find("World") != std::string::npos) { /* ... */ }

// C++23
if (str.contains("World")) { /* ... */ }
if (str.contains('o')) { /* ... */ }

// string_view 也有 contains
std::string_view sv = "test.txt";
if (sv.contains("test")) { /* ... */ }

接受子串(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <utility>

int process(int value) {
switch (value) {
case 1:
return 10;
case 2:
return 20;
default:
std::unreachable(); // 调用方约定 value 只能是 1 或 2
}
}

void* allocate(std::size_t size) {
if (size == 0) {
return nullptr;
}
// 编译器知道 size > 0
std::unreachable();
}

第二个例子展示了"前置判断后剩余路径不可达"的用法。注意 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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stacktrace>
#include <iostream>

void dump() {
for (const auto& entry : std::stacktrace::current()) {
std::cout << entry.description() << " at "
<< entry.file_name() << ":" << entry.line() << "\n";
}
}

void bar() { dump(); }
void foo() { bar(); }

int main() { foo(); }

工程上注意三点:采集调用栈有性能开销(通常毫秒级),不要放进热路径;符号化(函数名、源码位置)依赖调试信息,发布构建里 description() 可能只给地址;编译器/链接器需开启支持(如 GCC 需额外链接 -lstdc++_exp,MSVC 默认可用但符号化需要 PDB),各平台成熟度不一。

常用特性总结

最常用的 C++23 特性:

  • std::printstd::println
  • std::expected
  • std::generator
  • std::flat_mapstd::flat_set
  • 标准库模块(import std
  • std::byteswap
  • std::string::contains
  • std::stacktrace(调试诊断)