第 11 章 C++23

如果说 C++20 是继 C++11 之后又一次「大版本」式的更新,那么 C++23 更像是一次以巩固与完善为主的增量更新:它补全了 C++20 中一些仓促引入的特性,并加入了大量呼声已久的库设施。本章我们挑选其中较为重要、也较为实用的特性进行介绍。

本章的示例使用了 C++23 的特性,编译时需要使用 -std=c++2b(或 -std=c++23)。此外,受限于各标准库实现的进度,部分库特性(如 std::generatorstd::move_only_function<stacktrace>import std;)可能在你的工具链上尚不可用,本章会在相应位置加以说明。

11.1 语言特性

显式对象形参(Deducing this)

在 C++23 之前,如果我们想让一个成员函数同时适配 const/非 const、左值/右值等多种情形,往往不得不书写多个几乎完全重复的重载。C++23 引入了**显式对象形参 (explicit object parameter)**,允许把「this」写成函数的第一个形参,并通过模板对其 cv 与引用限定进行推导,从而用一个函数模板覆盖所有情形:

struct Counter {
int value = 0;
template <typename Self>
auto&& get(this Self&& self) { // self 即原先的 *this
return self.value;
}
};

这一特性还使得「递归 lambda」等以往十分别扭的写法变得自然。

if consteval

C++23 引入了 if consteval,用于在函数体内区分当前是否处于常量求值的语境,从而为编译期与运行期分别选择不同的实现:

constexpr int compute(int x) {
if consteval {
return x * 2; // 常量求值时走这里
} else {
return x * 3; // 运行期走这里
}
}

多维下标运算符

C++23 允许 operator[] 接受多个下标参数,使得多维容器可以使用直观的 m[i, j] 语法,而不必退而求其次地使用 operator()

struct Matrix2x3 {
int data[6] = {};
int& operator[](std::size_t r, std::size_t c) { return data[r * 3 + c]; }
};

Matrix2x3 m;
m[1, 2] = 7;

auto(x) 与静态 operator()

C++23 提供了 auto(x) / auto{x} 语法来显式地产生一份**退化拷贝 (decay-copy)**,即一个与 x 同类型的纯右值副本,这在需要明确「我要的是一份副本」时非常有用:

std::vector<int> v{1, 2, 3};
auto copy = auto(v); // 显式拷贝,得到一个纯右值

此外,函数对象(包括 lambda)的调用运算符现在可以声明为 static,省去了隐式对象形参,在某些场景下能带来更好的性能:

struct Add {
static int operator()(int a, int b) { return a + b; }
};

[[assume]]

[[assume(expr)]] 是 C++23 标准化的属性,用于向编译器声明某个表达式在此处一定为真,从而允许优化器据此进行优化。需要强调的是,如果该假设在运行期实际并不成立,将导致未定义行为,因此应当谨慎使用:

int divide_by(int x) {
[[assume(x > 0)]]; // 向优化器承诺 x 一定为正
return 100 / x;
}

其他语言层面的小改进

C++23 还包含若干较小的语言改进:

11.2 标准库

std::expected

std::expected<T, E> 表示一个「要么是值 T、要么是错误 E」的结果,为不依赖异常的错误处理提供了类型安全、表达力强的手段,与其他语言中的 Result 类型类似:

#include <expected>

std::expected<int, std::string> parse_positive(int x) {
if (x > 0) return x;
return std::unexpected("not positive");
}

auto r = parse_positive(42);
if (r) {
// 使用 *r
} else {
// 使用 r.error()
}

std::printstd::println

C++23 的 <print> 提供了 std::printstd::println,它们基于 C++20 的 std::format,以类型安全的格式化字符串进行输出,比传统的 iostream 链式 << 更简洁、也更高效:

#include <print>

std::println("Hello, {}!", "C++23");
std::println("{} + {} = {}", 1, 2, 1 + 2);

std::mdspan

std::mdspan 是对一段连续存储的非拥有多维视图。它本身不分配内存,只是按照给定的维度(extents)重新解读底层一维数组的布局,常用于科学计算与高性能计算:

#include <mdspan>

std::array<int, 6> storage{1, 2, 3, 4, 5, 6};
std::mdspan m(storage.data(), 2, 3); // 将其视为 2 x 3
int x = m[1, 2]; // 配合多维下标语法

std::flat_mapstd::flat_set

std::flat_map / std::flat_set 是以有序连续存储(默认由两个 vector 实现)为底层的关联容器。相比基于红黑树的 std::map,它们的插入较慢,但查找与遍历对缓存更友好、内存开销也更小:

#include <flat_map>

std::flat_map<int, const char*> m;
m.insert({3, "three"});
m.insert({1, "one"});
// 遍历时按键有序:1, 3

范围库的增强

C++23 为范围库补充了大量实用的适配器,例如 views::zip(将多个范围按位置「拉链」在一起逐元素并行遍历)、views::enumerate(为元素附上下标)、views::chunkviews::slideviews::join_with 等,以及通用的 std::ranges::to(把一个范围物化为具体容器):

#include <ranges>

std::vector<std::string> names{"a", "b", "c"};
std::vector<int> scores{90, 80, 70};
for (auto&& [name, score] : std::views::zip(names, scores)) {
// name 与 score 一一对应
}

其他改进

C++23 还包含许多零散但实用的小改进,例如:

11.3 关于标准库支持情况

C++23 的语言特性大多已被主流编译器较好地支持,但部分库特性的落地进度因实现而异。例如 std::generator(协程范围)、std::move_only_function<stacktrace> 以及标准库模块 import std; 在某些标准库(尤其是 libc++)中可能尚未实现或仍处于实验阶段。在使用这些特性前,建议先查阅 cppreference 的编译器支持页面 确认你所用工具链的支持情况。

总结

C++23 虽然不像 C++11 或 C++20 那样带来颠覆性的变化,但 std::expectedstd::printstd::mdspan、显式对象形参等特性,实实在在地改善了日常编写 C++ 的体验。结合后续的 C++26,现代 C++ 仍在持续演进。

进一步阅读的参考资料