CppCon 2023 学习: C++23:An Overview of Almost All New and Updated Features
C++23 Core Language(核心语言特性)——详细理解
1. Explicit Object Parameters(显式对象形参)
这解决什么?
过去成员函数隐式拥有一个 this 指针,不能:
- 指定参数传值或传引用
- 做重载区分
- 控制对象的 cvref 类型
C++23 引入:
你可以显式写出对象参数:
struct X {
void f(this X& self, int a); // self 就是 this,但显式写出来
};
好处
- 可以重载:
this X,this const X,this X&& - 允许制作更合理的 fluent API
- 与函数式/管道式写法兼容
2. if consteval
用于判断当前是否处于编译期求值(constexpr 计算环境)。
对比:
consteval: 函数必须在编译期执行constexpr: 可以编译期或运行期if consteval: 允许写出“如果在编译期,则走编译期逻辑,否则走运行期逻辑”。
示例:
constexpr int f(int x) {
if consteval {
return x * 2; // 编译期逻辑
} else {
return x + 2; // 运行期逻辑
}
}
3. Multidimensional Subscript Operator(多维下标运算符)
C++23 允许自定义:
operator[](size_t i, size_t j)
可写成:
matrix[i, j];
生成更自然的多维数组 API(服务于 mdspan 等新库)。
4. Attributes on Lambda-Expressions(Lambda 上的属性)
可以写:
auto f = [][[nodiscard]]{ return 5; };
过去 Lambda 不能加属性。
5. Literal Suffix for size_t(为 size_t 增加字面量后缀)
新增后缀:uz
写法:
auto n = 123uz; // type = size_t
方便避免写 size_t(123)。
6. auto(x):语言级 decay-copy(衰减复制)
auto y = (x); // 过去可能是引用折叠
auto z = auto(x) // C++23:始终 decay-copy
保证得到一个 值拷贝,不是引用。
7. #elifdef, #elifndef, #warning
更方便的预处理器分支:
#if DEBUG
#elifdef TESTING
#elifndef RELEASE
#endif
#warning:编译期发出警告。
8. Marking Unreachable Code(标记不可达代码)
关键字:[[unreachable]];
switch(x) {
case 0: ...; break;
}
[[unreachable]]; // 告诉编译器这里不会发生
可使优化更积极。
9. Assumptions(假设)
[[assume(cond)]]。告知编译器条件永远为真。
[[assume(x > 0)]]
int y = 10 / x;
让优化器生成更快的代码(风险:程序员必须确保条件真的永远真)。
10. Named Universal Character Escapes(命名字符转义)
支持:
"\N{LATIN SMALL LETTER SHARP S}"
跨语言文字一致化。
11. Trim Whitespace Before Line Splicing(拼接行前裁剪空白)
改进了:
hello \
world
这种行拼接在语义上的处理,变得更一致、更可控。
C++23 Standard Library(标准库)
1. String Formatting Improvements(格式化字符串改善)
std::format 性能更好,支持更多格式选项。
模块版本:std.format。
2. Standard Library Modules(标准库模块)
C++23 初次完整支持模块化标准库:
import std;import std.core;import std.format;
编译速度、可维护性更好。
3. std::flat_map / std::flat_set(平坦容器)
底层使用 排序 vector 而不是红黑树。
特点:
- 内存连续,Cache 友好
- 查找二分,速度快
- 插入删除慢(因为 vector 移动)
适合“读多写少”的场景。
4. std::mdspan(零开销多维视图)
mdspan = 多维数组的通用抽象。
无内存拥有权,只是视图。
用于:
- HPC
- 数值计算
- 线性代数
- 图像处理
5. std::generator(协程生成器)
使用协程实现:
std::generator<int> numbers() {
for (int i=0;i<10;i++)
co_yield i;
}
很像 python 的 yield。
6. basic_string(_view)::contains()
终于可以写:
s.contains("abc");
7. From nullptr 构造 string
以前非法;现在允许:
std::string s = nullptr; // 构造空串
8. resize_and_overwrite()
高性能地构造字符串:
s.resize_and_overwrite(n, [](char* p, size_t n) {
// 写入 p,返回实际写入的大小
return new_size;
});
避免临时 buffer,极快。
9. Optional 的 Monadic Operations
例如:
transformand_thenor_else
让std::optional更像函数式工具。
10. Stacktrace Library(堆栈跟踪)
std::stacktrace
std::cout << std::stacktrace::current();
调试大提升。
11. Ranges & Views 改进
- 合并功能
- 新增视图
- 性能提升
- 适配器可组合性更好
12. std::expected(标准化错误返回)
用于替代异常或 optional + error
expected<int, error> f();
变成标准,巨大提升错误处理表达能力。
13. std::move_only_function<>
对标 std::function(但只能移动)。
性能更好,不需要拷贝语义。
14. std::spanstream
基于 std::span 的 stream,不再使用动态分配。
15. std::byteswap()
统一的字节序转换:
auto x = std::byteswap(0x12345678u);
16. std::to_underlying()
得到枚举的底层整数值:
int x = std::to_underlying(Color::Red);
17. Heterogeneous Erasure(异类型擦除) for associative containers
例如:
std::map<std::string, int> m;
m.erase("abc"s); // 现在支持异类型查找 + 删除
提高性能,避免构造临时 key。
Removed Features(移除特性)
1. Garbage Collection Support(移除了对 GC 的支持)
C++ 从未真正实现过 GC,但以前保留了 GC hooks 接口。
C++23 把这些接口彻底去掉。
C++23:Explicit Object Parameters(显式对象参数)详解
1. 背景:以前的 this 是隐式的
在 C++20/23 之前,成员函数都有一个隐式的 this 指针:
void setValue(double value) {
m_value = value; // 实际是 this->m_value
}
C++23 允许你显式写出 this 所绑定的对象参数:
2. 显式对象参数(Explicit Object Parameter)语法
你可以这样写:
void setValue(this Cell& self, double value) {
self.m_value = value;
}
效果
this不再隐式出现- 第一个参数 明确表示“这个成员函数要绑定哪个对象”
更类似于 Python 的 self 或 Rust 的 &self / &mut self / self。
3. ref-qualified 成员函数的改写
有些类会写多个版本的同名函数,例如:
void f() &; // 左值对象调用
void g() const &; // 左值 const 对象调用
void h() &&; // 右值对象调用
C++23 可以写成:
void f(this Data&);
void g(this const Data&);
void h(this Data&&);
优点
更统一、更清晰:
- 左值:
Data& - 常量左值:
const Data& - 右值:
Data&&
4. 大幅减少代码重复(重要!)
一个常见问题:同一个 getter 要写三份:
std::string& GetName() &;
const std::string& GetName() const &;
std::string&& GetName() &&;
使用 C++23:一份代码搞定!
template <typename Self>
auto&& GetName(this Self&& self) {
return std::forward<Self>(self).m_name;
}
解析:
Self 推导出调用者类型:
- 如果是左值非 const,Self =
Person& - 如果是 const 左值,Self =
const Person& - 如果是右值,Self =
Person&&
返回的语义完全正确(完美转发)。
5. 递归 lambda 终于变简单!
以前 lambda 的 this 是包围对象的 this,不是 lambda 自己的实例!
C++23:
auto fibonacci = [](this auto self, int n) {
if (n < 2) return n;
return self(n - 1) + self(n - 2);
};
这里的关键:
self 就是 lambda 自身,可以递归调用。
以前必须写 std::function 或用 Y Combinator 技巧,非常麻烦。
if consteval 深度解析(C++23 新语法)
1. 语法
if consteval {
/* A */
} else {
/* B */
}
没有条件表达式!
编译期会判断:此语句是否处在 常量求值环境(constant evaluation)中。
2. 行为解释
- 如果当前语境是常量求值,则执行 A
- 否则执行 B
用数学描述:
execute(A) ⟺ during constant evaluation \text{execute}(A) \iff \text{during constant evaluation} execute(A)⟺during constant evaluation
3. 为什么不用 std::is_constant_evaluated()?
你可能认为写这个就行:
if (std::is_constant_evaluated()) {
/*A*/
} else {
/*B*/
}
但 if consteval 更好:
① 不需要包含头文件
因为它属于语言本身。
② A 块内部可以调用 consteval 函数
std::is_constant_evaluated() 的 if 块内 不一定被认为是立即函数上下文,因此有些 consteval 函数不能调用。
③ if consteval 不会被误写成 if constexpr
下面这种是错误的用法:
if constexpr (std::is_constant_evaluated()) {
...
}
因为 constexpr 会导致编译期决定,而不是运行期,让逻辑含义混乱。
4. 示例
consteval int f(int i) { return i; }
constexpr int g(int i) {
if consteval {
return f(i) + 1; // 可以 —— 立即函数上下文
} else {
return 42;
}
}
解释:
- 在编译期执行时(如在 constexpr 上下文),返回
f(i) + 1 - 运行期调用则返回
42
5. 对比 consteval 函数
consteval int h(int i) {
return f(i) + 1; // 必定在常量求值环境
}
h 必须 总是在编译期求值,而 g 是“不一定要编译期”。
小结
Explicit Object Parameters — 关键点
- 允许你显式写出
this参数 - 可以减少大量代码重复
- 统一 ref-qualified/const 成员函数
- 支持递归 lambda
- 更接近 Rust / Python 的对象方法语义
if consteval — 关键点
- 新的语言层级语法
- 用来区分编译期/运行期路径
- 能调用 consteval 函数
- 比
std::is_constant_evaluated()更安全、语义更明确
C++23:Multidimensional Subscript Operator(多维下标运算符)
1⃣ 背景:以前访问多维数据的方式
在 C++20 及以前,如果你想访问三维数组或多维矩阵,通常有两种选择:
选项 A:函数调用风格
data(x, y, z);
- 优点:可以自定义访问逻辑
- 缺点:语法上不像数组,下标访问不直观
选项 B:多级下标访问
data[x][y][z];
- 优点:语法直观,类似数组
- 缺点:如果底层不是严格嵌套数组,需要写很多
operator[]或额外封装 - 语义复杂:每个
operator[]只能返回一维切片(slice),不能一次性访问多维
2⃣ C++23 新特性:多维下标运算符
C++23 允许你在单个下标操作中同时访问多个维度:
data[x, y, z];
- 核心点:在一个
operator[]中传入多个索引 - 语法更清晰,逻辑更直接
3⃣ 示例
假设你有一个三维矩阵类:
template<typename T>
class Matrix3D {
T storage[100][100][100];
public:
// C++23 多维下标运算符
T& operator[](size_t x, size_t y, size_t z) noexcept {
return storage[x][y][z];
}
};
访问元素:
Matrix3D<int> mat;
mat[1, 2, 3] = 42; // 单行表达式直接访问三维数据
4⃣ 数学表达式对应
如果把三维数组看作函数映射:
data:0..X−1×0..Y−1×0..Z−1→T data : {0..X-1} \times {0..Y-1} \times {0..Z-1} \to T data:0..X−1×0..Y−1×0..Z−1→T
- 以前:data(x,y,z)data(x, y, z)data(x,y,z) 或 data[x][y][z]data[x][y][z]data[x][y][z]
- C++23:data[x,y,z]data[x, y, z]data[x,y,z]
语法上更像函数 f(x,y,z)f(x, y, z)f(x,y,z),又保留数组风格。
5⃣ 优点总结
- 语法简洁:一次表达所有维度,不需要多级
operator[] - 实现灵活:可以在单个函数里实现 bounds check、索引计算等逻辑
- 可读性好:直观表达访问多维数组的意图
C++23:Lambda 表达式属性
1⃣ 背景:以前(C++20 及之前)
Lambda 表达式生成一个函数对象(function object,也叫闭包对象)。
在 C++20 及之前,你可以给这个函数对象本身添加属性,例如 [[deprecated]]:
auto a = [] () [[deprecated]] { return 42; };
- 这里的属性
[[deprecated]]是附加到 整个闭包对象 上的 - 但是无法单独控制 闭包对象的
operator()函数 的属性
2⃣ C++23 的改进
C++23 允许你为 生成的函数对象的 call operator 添加属性,同时保留对闭包对象本身的属性:
auto a = [] [[nodiscard]] () [[deprecated]] { return 42; };
解析:
[[nodiscard]]→ 作用在 闭包对象本身[[deprecated]]→ 作用在 call operator,即 operator()
- 这样你可以同时控制两类属性:
- 闭包对象属性:影响对象本身的行为
- 函数调用属性:影响调用
a()时的行为
3⃣ 可视化理解
| 作用对象 | 属性示例 | 说明 |
|---|---|---|
闭包对象 (lambda object) |
[[nodiscard]] |
提醒不要丢弃返回值或对象 |
函数调用操作符 (operator()) |
[[deprecated]] |
调用时提示已废弃 |
4⃣ 示例:结合使用
auto lambda = [] [[nodiscard]] () [[deprecated]] {
return 42;
};
int x = lambda(); // 调用时编译器会警告:deprecated
[[nodiscard]]提示:如果不使用lambda的返回值,编译器会警告[[deprecated]]提示:调用lambda()时编译器会警告
5⃣ 优势
- 更精细的属性控制
- 以前只能标记函数对象
- 现在可以单独标记 call operator
- 提升代码安全性和可维护性
[[nodiscard]]避免丢弃重要返回值[[deprecated]]提醒过时调用
🟦 C++23:Literal Suffix for size_t
1⃣ 背景:以前的整数字面量后缀
在 C++20 及以前,整数字面量有以下后缀:
| 后缀 | 类型说明 |
|---|---|
U |
unsigned int |
L |
long |
UL |
unsigned long |
LL |
long long |
ULL |
unsigned long long |
- 问题:如果你想写
size_t类型的字面量,通常需要手动转换:
auto n = static_cast<size_t>(10);
2⃣ C++23 新增后缀
| 后缀 | 说明 |
|---|---|
uz 或 UZ |
创建 std::size_t 类型字面量 |
z 或 Z |
创建与 std::size_t 对应的有符号整数类型(ssize_t 类似) |
3⃣ 使用场景
假设有一个向量:
std::vector<int> data{11, 22, 33};
循环遍历时:
for (auto i = 0, count = data.size(); i < count; ++i) { /* ... */ }
- 编译器可能报错:
i被推导为int,而count是size_t - 类型不匹配,尤其在 64 位平台上可能导致警告或错误
4⃣ 解决方案:使用 uz 字面量
for (auto i = 0uz, count = data.size(); i < count; ++i) {
/* ... */
}
0uz→ 类型直接为size_t- 避免类型不匹配的问题
- 语法更简洁,读者可以直接看出这是
size_t类型
5⃣ 数学与类型对应
如果用符号表示:
- 以前循环中类型不匹配:
i∈Z,count∈size_t,i<count i \in \mathbb{Z}, \quad count \in \text{size\_t}, \quad i < count i∈Z,count∈size_t,i<count - 使用
uz后:
i∈size_t,count∈size_t,i<count i \in \text{size\_t}, \quad count \in \text{size\_t}, \quad i < count i∈size_t,count∈size_t,i<count
保证类型一致,消除隐式转换或警告。
优点
- 减少类型错误:不用手动 cast
- 代码可读性增强:看到
uz就知道是size_t - 统一风格:循环、数组索引、大小参数等场景自然使用
size_t
C++23:auto(x) / auto{x}
1⃣ 背景:对象拷贝
在 C++20 及以前,拷贝一个对象通常写:
auto c = x;
- 问题:
c是一个 lvalue(左值),即使它是通过拷贝构造得到的- 在某些场景下,需要一个 prvalue(纯右值)来触发移动语义或传递给右值引用参数
2⃣ C++23 新特性
C++23 引入了:
auto(x)
auto{x}
- 效果:产生一个 prvalue 拷贝
- 意味着生成的值是右值,可以传递给右值引用函数,从而触发移动语义
- 避免了因为左值导致的额外拷贝或语义错误
3⃣ 示例
假设有函数:
void process(int&& value) {
std::println("{}", value);
}
void process_all(const std::vector<int>& data) {
for (auto& i : data) {
process(auto(i)); // C++23 decay-copy: i 变成 prvalue
}
}
i本身是data中元素的 左值引用- 直接传递给
process(int&&)会报错(因为不能将 lvalue 绑定到 rvalue 引用) - 使用
auto(i)会生成一个 右值拷贝,可以安全传递
4⃣ 类型和语义理解
- 普通
auto:
auto c = x; // c 是 lvalue
- C++23
auto(x):
auto c = auto(x); // c 是 prvalue
- 数学视角:
x:lvalue→auto(x)x′:prvalue x : \text{lvalue} \quad \xrightarrow{auto(x)} \quad x' : \text{prvalue} x:lvalueauto(x)x′:prvalue - 这保证了:
- 移动语义可以被正确触发
- 避免不必要的拷贝
- 适合遍历容器或处理右值引用参数的场景
5⃣ 小结
auto(x)/auto{x}→ decay-copy,生成 prvalue- 用于需要右值语义的场景
- 适合与 右值引用函数 搭配使用
- 避免了写
std::move(i)的显式操作,使代码更自然
C++23:新预处理指令
1⃣ 背景:以前的预处理指令
在 C++20 及以前,我们常用条件编译指令:
| 指令 | 说明 |
|---|---|
#ifdef id |
相当于 #if defined(id),如果宏 id 已定义,则执行下面的代码块 |
#ifndef id |
相当于 #if !defined(id),如果宏 id 未定义,则执行下面的代码块 |
| 示例: |
#ifdef DEBUG
std::cout << "Debug mode\n";
#endif
2⃣ C++23 新增指令
| 指令 | 说明 |
|---|---|
#elifdef id |
相当于 #elif defined(id),在 #if 或 #elif 后使用,检查宏是否定义 |
#elifndef id |
相当于 #elif !defined(id),在 #if 或 #elif 后使用,检查宏是否未定义 |
#warning "message" |
生成编译时警告,输出指定信息 |
3⃣ 使用示例
#if defined(VERSION1)
// code for VERSION1
#elifdef VERSION2
// code for VERSION2
#elifndef VERSION3
// code if VERSION3 is NOT defined
#else
#warning "No version defined, using default settings"
// default code
#endif
#elifdef VERSION2→ 如果VERSION2被定义,则执行对应代码#elifndef VERSION3→ 如果VERSION3没有被定义,则执行对应代码#warning "..."→ 编译器输出警告信息,但不会停止编译
4⃣ 使用场景
- 简化条件编译
- 避免写长形式
#elif defined(id) - 代码更直观、易读
- 避免写长形式
- 编译期提醒
- 使用
#warning提示开发者某些宏或设置未定义 - 可作为临时代码或兼容性检查的工具
- 使用
5⃣ 小结
#elifdef id=#elif defined(id)#elifndef id=#elif !defined(id)#warning "..."→ 生成编译期警告信息- 优点:提高代码可读性,减少书写错误,便于维护条件编译逻辑
C++23:std::unreachable()
1⃣ 背景
在 C++ 中,有时我们知道某段代码在逻辑上永远不会被执行,例如:
switch语句中枚举值已经覆盖了所有可能情况- 某些函数参数有严格限制
以前,为了安全,编译器仍然可能生成检查代码,确保值落在合法范围内。这样会略微影响性能。
2⃣ 新特性
C++23 引入了:
#include <utility>
std::unreachable();
- 功能:标记当前代码为 不可达(unreachable)
- 效果:调用此函数会触发 未定义行为(UB)
- 优化:编译器可因此省略安全检查,提高性能
3⃣ 示例
void do_something(int number_that_is_only_0_1_2_or_3) {
switch (number_that_is_only_0_1_2_or_3) {
case 0: case 2:
handle_0_or_2();
break;
case 1:
handle_1();
break;
case 3:
handle_3();
break;
default:
std::unreachable(); // 永远不会执行到这里
}
}
- 假设函数参数 只可能是 0、1、2、3
default分支理论上永远不会执行- 使用
std::unreachable()告诉编译器:这里不可达 - 编译器可以省略检查,生成更高效代码
4⃣ 注意事项
- 未定义行为
- 如果实际上执行了
std::unreachable(),程序行为是未定义的 - 必须确保逻辑上确实不可达
- 如果实际上执行了
- 优化潜力
- 对于
switch、if-else、枚举、模板等场景,使用std::unreachable()可以显著减少多余分支检查
- 对于
- 数学理解:
假设参数空间 S=0,1,2,3S = {0,1,2,3}S=0,1,2,3,函数 f:S→行为f : S \to \text{行为}f:S→行为,那么:
∀x∈S,f(x)有定义⇒default 分支不可达 \forall x \in S, f(x) \text{有定义} \quad \Rightarrow \quad \text{default 分支不可达} ∀x∈S,f(x)有定义⇒default 分支不可达
- 此时调用
std::unreachable()对应集合补集为空:
Sc=∅⇒UB不会触发(在逻辑正确下) S^c = \emptyset \quad \Rightarrow \text{UB不会触发(在逻辑正确下)} Sc=∅⇒UB不会触发(在逻辑正确下)
5⃣ 小结
std::unreachable():标记不可达代码- 优点:提升性能、帮助编译器优化
- 风险:如果逻辑有误,触发未定义行为
- 常用场景:
switch枚举、严格限制函数参数、模板元编程等
C++23:Assumptions(假设)
1⃣ 背景
在 C++20 及之前,各大编译器提供了**假设(assume)**的功能,但方式不统一:
| 编译器 | 假设语法 |
|---|---|
| MSVC / ICC | __assume(expr) |
| Clang | __builtin_assume(expr) |
| GCC | 通过 if (expr) {} else { __builtin_unreachable(); } 模拟 |
| 示例(GCC 风格): |
int divide_by_32(int x) {
if (x >= 0) {}
else { __builtin_unreachable(); } // 告诉编译器 x < 0 不可能
return x / 32;
}
2⃣ C++23 标准化
C++23 引入标准化方式:
[[assume(expr)]];
- 功能:告诉编译器 expr 必然为 true
- 优势:不依赖编译器特性,统一语法
- 编译器可据此优化生成更高效代码
3⃣ 示例
int divide_by_32(int x) {
[[assume(x >= 0)]]; // 假设 x >= 0
return x / 32;
}
- 编译器可以在优化阶段忽略 x < 0 的情况
- 提高性能,因为生成的除法操作可以假设输入非负,省略安全检查
4⃣ 数学理解
假设函数:
f(x)=x32,x∈Z f(x) = \frac{x}{32}, \quad x \in \mathbb{Z} f(x)=32x,x∈Z
- 我们有假设:
x≥0 x \ge 0 x≥0 - 编译器可以基于这个假设优化生成的机器码,不再处理 x<0x < 0x<0 的情况
- 换句话说,函数的有效域变为:
Df=x∈Z∣x≥0 D_f = { x \in \mathbb{Z} \mid x \ge 0 } Df=x∈Z∣x≥0 - 对于 x∉Dfx \notin D_fx∈/Df,行为是未定义的(UB),必须保证假设成立
5⃣ 使用注意事项
- 性能导向:
[[assume(expr)]]是用于性能优化,而非逻辑检查 - 未定义行为:如果
expr实际为 false,程序行为未定义 - 适用场景:
- 已知输入约束的函数(如数组索引、非负整数等)
- 循环迭代边界优化
- 数学函数假设域优化
小结
[[assume(expr)]]:C++23 提供统一假设语法- 功能:帮助编译器优化,提升性能
- 风险:违反假设会触发未定义行为
- 类似概念:
std::unreachable()标记不可达代码
C++23:Named Universal Character Escapes
1⃣ 背景
在 C++20 及之前,表示 Unicode 字符通常使用 码点(code point)值:
- 单字符字面量:
\uXXXX或\UXXXXXXXX - 字符串字面量:
\uXXXX或\UXXXXXXXX
问题: - 可读性差,不容易理解 Unicode 字符的含义
- 特别是在包含组合字符或不常用字符时
2⃣ C++23 新特性
C++23 允许使用 Unicode 名称 来代替码点值:
auto a { U'\N{LATIN CAPITAL LETTER A WITH MACRON}' };
auto b { u8"\N{LATIN CAPITAL LETTER A WITH MACRON}\N{COMBINING GRAVE ACCENT}" };
\N{...}:通过 Unicode 分配的名称指定字符- 适用于字符字面量、UTF-8/16/32 字符串字面量
- 使代码更易读、可维护
3⃣ 对比示例
传统写法(C++20 及之前)
// UTF-32 单字符字面量 {LATIN CAPITAL LETTER A WITH MACRON}
auto a { U'\u0100' };
// UTF-8 字符串字面量 {LATIN CAPITAL LETTER A WITH MACRON}{COMBINING GRAVE ACCENT}
auto b { u8"\u0100\u0300" };
- 问题:
\u0100\u0300对不熟悉 Unicode 的开发者来说几乎无法立即识别含义
C++23 写法
// UTF-32 单字符字面量
auto a { U'\N{LATIN CAPITAL LETTER A WITH MACRON}' };
// UTF-8 字符串字面量
auto b { u8"\N{LATIN CAPITAL LETTER A WITH MACRON}\N{COMBINING GRAVE ACCENT}" };
\N{...}清楚表明每个字符的 Unicode 名称- 代码可读性显著提高
- 特别适合文档化字符串、国际化软件、处理复杂 Unicode 数据时
4⃣ 数学/Unicode 表示理解
假设字符码点为 U+0100U+0100U+0100,组合重音码点为 U+0300U+0300U+0300:
- 传统方式表示:
a=U′0˘100′,b=u8"0˘1000˘300" a = U'\u0100', \quad b = u8"\u0100\u0300" a=U′0˘100′,b=u8"0˘1000˘300" - C++23 方式表示:
a=U′NLATIN CAPITAL LETTER A WITH MACRON′,b=u8"NLATIN CAPITAL LETTER A WITH MACRONNCOMBINING GRAVE ACCENT" a = U'\N{LATIN\ CAPITAL\ LETTER\ A\ WITH\ MACRON}', \quad b = u8"\N{LATIN\ CAPITAL\ LETTER\ A\ WITH\ MACRON}\N{COMBINING\ GRAVE\ ACCENT}" a=U′NLATIN CAPITAL LETTER A WITH MACRON′,b=u8"NLATIN CAPITAL LETTER A WITH MACRONNCOMBINING GRAVE ACCENT" - 逻辑上两种方式等价,但可读性大幅提升
5⃣ 使用场景
- 多语言文本处理
- 字符名称明确,便于维护多语言字符串
- 国际化/本地化系统
- 不需要查 Unicode 码点即可理解字符含义
- 文档化或教学用途
- 适合演示 Unicode 字符及组合字符
小结
- C++23 提供了
\N{UNICODE_NAME}来表示 Unicode 字符 - 优点:
- 提高代码可读性
- 减少对 Unicode 码点表的依赖
- 对组合字符和复杂符号支持良好
C++23:Trim Whitespace Before Line Splicing
1⃣ 背景
在 C++ 中,如果你在 代码行末使用反斜杠 \,表示 行继续(line continuation):
int i = 1 \
+ 42;
\告诉编译器,下一行是当前行的延续- 这个机制主要用于宏定义和长表达式分行
2⃣ 历史问题(C++20 及之前)
如果 \ 后面 有额外的空格或制表符:
int i = 1 // 注意这里有空格
\ + 42;
- 标准未定义行为
- 不同编译器表现不一致:
- GCC / Clang 输出:
1 - MSVC 输出:
43
- GCC / Clang 输出:
3⃣ C++23 新规定
C++23 规范化了空白处理:
- 规定: 在行拼接(line splicing)时,
\后面的 所有空白都会被去掉 - 这样保证不同编译器行为一致
效果:
int main()
{
int i = 1 \
+ 42;
std::cout << i;
}
- 输出结果统一为:
i=1 i = 1 i=1 - 也就是说,反斜杠后面多余的空格被自动剔除,不再影响编译结果
4⃣ 总结
优点
- 消除历史兼容性问题
- 在多编译器下行拼接行为一致
- 避免潜在错误
- 无需担心意外空格破坏代码逻辑
- 增强可读性与可维护性
- 开发者可以自由缩进续行而不用担心影响结果
小结
- C++23 行继续规则:
\后的空白在行拼接前会被剔除 - 确保跨平台行为一致,输出符合直观预期
C++23 字符串格式化改进
1⃣ 新增函数
C++23 引入了两个便捷的打印函数,基于 std::format():
| 函数 | 功能 |
|---|---|
std::print() |
直接格式化并输出到标准输出(stdout) |
std::println() |
格式化输出并自动添加换行符 |
2⃣ 基本示例
std::string name { "CppCon" };
// 旧写法:cout + format
std::cout << std::format("Hello {} 2023!\n", name);
// C++23 写法:
std::print("Hello {} 2023!\n", name); // 不自动换行
std::println("Hello {} 2023!", name); // 自动换行
- 以前需要写
std::cout << std::format(...) - C++23 可以直接用
print()或println(),更简洁
3⃣ 容器范围(Ranges)格式化
C++23 支持 直接格式化 STL 容器:
std::vector<std::pair<int, int>> v { {1, 2}, {3, 4} };
std::println("{}", v); // 输出: [(1, 2), (3, 4)]
std::set<std::pair<int, int>> s { {1, 2}, {3, 4} };
std::println("{}", s); // 输出: {(1, 2), (3, 4)}
std::map<int, int> m { {1, 2}, {3, 4} };
std::println("{}", m); // 输出: {1: 2, 3: 4}
- 对
vector,set,map等容器,std::println()可以直接识别元素并格式化输出 - 支持嵌套容器,如
vector<vector<int>>
4⃣ 范围格式化说明(Range Formatting Specifiers)
C++23 提供 范围格式化符,允许定制容器内元素的显示:
std::vector<std::string> strings{ "Hello"s, "World!\t2023"s };
// 默认输出:
std::println("{}", strings);
// 输出: ["Hello", "World!\t2023"]
// 去掉引号并保持换行:
std::println("{::}", strings);
// 输出: [Hello, World!
// 2023]
嵌套容器示例
std::vector<std::vector<int>> vv { {11, 22}, {33, 44, 55} };
std::println("{}", vv); // [[11, 22], [33, 44, 55]]
std::println("{:n}", vv); // [11, 22], [33, 44, 55]
std::println("{:n:n}", vv); // 按元素换行:
// 11, 22
// 33, 44, 55
std::println("{:n:n:*^4}", vv); // 自定义填充和宽度:
// *11*, *22*
// *33*, *44*, *55*
{}:默认格式{:n}:每个嵌套容器换行显示{:n:n}:嵌套元素换行{:n:n:*^4}:自定义填充符、对齐方式、宽度
5⃣ 小结
C++23 字符串格式化改进带来的好处:
- 更简洁:不用再写
std::format+std::cout - 直接支持容器输出,包括嵌套容器
- 灵活的范围格式化符,可以控制元素显示、对齐、填充
- 增强可读性,尤其是调试或日志输出时
C++23 标准库模块
C++23 引入了两个新的命名模块(named modules),用于更方便地导入标准库内容,减少传统 #include 的依赖。
1⃣ 模块列表
| 模块名称 | 功能说明 |
|---|---|
import std; |
导入标准库中 std 命名空间 的所有内容,包括: • C++ 头文件(如 std::sort())• C 风格包装头文件(如 std::fopen()) |
import std.compat; |
导入 std 模块内容 + 全局命名空间版本的 C 包装头文件(如 ::fopen()) |
std模块相当于标准#include的集合std.compat提供对老旧 C 接口的全局命名空间兼容
2⃣ 使用示例:现代 “Hello World”
import std; // 导入 std 模块
int main() {
std::println("Hello World!"); // 直接使用 std 命名空间函数
}
- 不再需要
#include <iostream>或#include <format> std::println直接可用- 模块化导入编译速度更快,且避免宏污染
3⃣ 总结
- 模块化导入:通过
import导入整个标准库模块 - 减少依赖:无需单独包含多个头文件
- 兼容性选择:
std.compat提供对旧 C 接口全局命名空间的支持 - 现代用法:写一个 “Hello World” 只需一句
import std;
C++23 扁平关联容器(Flat Associative Containers)
C++23 提供了一类新的扁平(flat)关联容器,主要用于在顺序容器(如 vector 或 deque)之上实现类似 std::map / std::set 的接口。
1⃣ std::flat_map 与 std::flat_multimap
- 头文件:
#include <flat_map> - 特点:
- 是 顺序容器 的适配器(adaptor)
- 提供 关联容器接口,类似
std::map - 唯一键(
flat_map)或 重复键(flat_multimap) - 键值按键排序
- 键和值分开存储,通常用
std::vector或std::deque
- 作用:
- 基于键快速检索值
- 同时保留顺序容器的高缓存局部性(cache locality),适合大量小型键值对
- 示例:
#include <flat_map>
#include <string>
using namespace std::string_literals;
int main() {
std::flat_map<int, std::string> myMap;
myMap[2023] = "CppCon"s;
// 自定义比较器和底层容器
std::flat_map<int, std::string,
std::less<int>, // 键排序方式
std::deque<int>, // 键存储容器
std::deque<std::string>> myMap2;
}
2⃣ std::flat_set 与 std::flat_multiset
- 头文件:
#include <flat_set> - 特点:
- 类似
flat_map/flat_multimap - 仅存储 键
- 不存储映射值(no mapped values)
- 类似
- 作用:
- 提供快速查找、排序的集合
- 适合大量小键值集合场景
3⃣ 总结
| 容器 | 是否允许重复键 | 存储方式 | 典型用途 |
|---|---|---|---|
std::flat_map |
否 | 键/值分开存储(vector/deque) | 快速查找 + 迭代缓存友好 |
std::flat_multimap |
是 | 同上 | 支持重复键的查找 |
std::flat_set |
否 | 键存储 | 高效唯一键集合 |
std::flat_multiset |
是 | 键存储 | 支持重复键集合 |
- 优点:
- 高缓存局部性,迭代效率高
- 键值分开存储,可自定义底层容器
- 接口类似传统关联容器,容易迁移
1⃣ “Flat” 的含义
- Flat = 扁平化存储
- 键(key)和值(value)直接存储在连续内存块里(通常用
std::vector或std::deque) - 不像
std::map或std::set那样使用 树结构(红黑树、平衡二叉树) - 所以访问元素时,缓存命中率高,迭代速度快
- 键(key)和值(value)直接存储在连续内存块里(通常用
- 对比:
| 容器类型 | 内部结构 | 访问特点 |
| --------------- | ------------------ | ---------------------------- |
|std::map| 红黑树 | 插入/删除对数复杂度,迭代顺序有序,但内存分散 |
|std::flat_map| 连续数组(vector/deque) | 插入/删除需要移动元素,但迭代非常快,内存连续,缓存友好 |
2⃣ “Flat” 带来的好处
- 缓存局部性好(CPU cache friendly)
- 因为数据在内存中是连续存储的
- 迭代性能高
for(auto& kv : myFlatMap)遍历速度比树快
- 内存分配少
- 比每个节点都分配内存的树更节省内存
所以flat_map/flat_set的 flat 可以理解为:
- 比每个节点都分配内存的树更节省内存
“扁平存储的关联容器”,强调 内存连续、迭代高效,而不是树结构。
std::mdspan 理解
1⃣ 定义与用途
std::mdspan定义在<mdspan>- 是 多维数组视图(Multidimensional array view)
- 可以看作是 C++20 的
std::span的多维扩展std::span是对一段连续内存的非拥有性视图mdspan允许直接用多维索引访问数据
- 支持 不同的内存布局策略(layout policies)
- 例如行主序(row-major)、列主序(column-major)等
std::submdspan- 是
mdspan的子视图(slice) - 可以只看已有
mdspan的一部分数据
- 是
2⃣ 示例分析
int* data { /* ... */ };
// 将 data 看作 2 行 2 列的二维数组
auto mySpan { std::mdspan(data, 2, 2) };
// 遍历并赋值
for (size_t i { 0 }; i < mySpan.extents().extent(0); ++i) {
for (size_t j { 0 }; j < mySpan.extents().extent(1); ++j) {
mySpan[i, j] = i * 1000 + j;
}
}
data是一块连续内存std::mdspan(data, 2, 2)将其视为 2×2 的二维数组mySpan[i, j]可以直接用 多维下标访问mySpan.extents().extent(0)获取第 0 维的长度(行数)mySpan.extents().extent(1)获取第 1 维的长度(列数)
3⃣ 特点与优势
- 非拥有性(Non-owning)
- 类似
span,不管理内存 - 可以快速创建对现有数组、vector、C 风格数组的多维视图
- 类似
- 多维索引支持
mySpan[i, j, k]可以访问任意维度的数据
- 灵活布局策略
- 默认是 行主序(row-major)
- 可指定列主序或自定义布局
- 高性能
- 无额外内存分配
- 可直接操作底层连续内存
总结:std::mdspan就像 一个多维的span,提供 高性能、多维访问和灵活布局 的能力,非常适合科学计算、图像处理、矩阵运算等场景。
std::generator 理解
1⃣ 定义与用途
std::generator定义在<generator>- 是 标准协程生成器(coroutine generator)
- 用于 按需生成序列数据,每次生成一个值给调用者,并在下次请求时继续执行
- 相当于 C# 的
yield return或 Python 的yield
2⃣ 示例分析
std::generator<int> getSequenceGenerator(int startValue, int numberOfValues) {
for (int i { startValue }; i < startValue + numberOfValues; ++i) {
// 向调用者生成一个值,并挂起协程
co_yield i;
}
}
int main() {
auto gen { getSequenceGenerator(10, 5) };
for (const auto& value : gen) {
std::print("{} (Press enter for next value)", value);
std::cin.ignore(); // 用户按回车后获取下一个值
}
}
co_yield i;- 生成一个值给调用者
- 挂起协程,等待下一次迭代
auto gen { getSequenceGenerator(10, 5) };- 创建一个生成器对象
gen - 可以按需访问生成的值
- 创建一个生成器对象
for (const auto& value : gen)- 迭代生成器中的值
- 每次迭代会 恢复协程执行,直到遇到下一个
co_yield
3⃣ 特点与优势
- 按需生成(Lazy Evaluation)
- 值只在需要时生成
- 节省内存,不必一次性创建整个序列
- 简化迭代逻辑
- 不需要手动维护迭代器或索引
co_yield自动挂起/恢复
- 协程支持
- 可与
co_await等协程机制配合 - 支持异步生成数据
- 可与
- 类型安全
std::generator<T>明确生成类型为T
总结:std::generator是 C++23 提供的 标准协程生成器,适合序列生成、流处理或按需计算场景。它将协程与迭代器结合,使代码更简洁、高效。
std::to_underlying() 理解
1⃣ 定义与用途
- 定义在
<utility>头文件中 - 作用:将 枚举值 转换为其 底层类型(underlying type)
- 在 C++23 之前,需要手动写:
static_cast<std::underlying_type_t<E>>(enum_value)
- C++23 引入
std::to_underlying(),简化写法,更语义化
2⃣ 示例分析
enum class Color : uint32_t {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff
};
Color r { Color::Red };
// 传统方式
auto value1 { static_cast<std::underlying_type_t<Color>>(r) };
// C++23 简化方式
auto value2 { std::to_underlying(r) };
Color::Red的底层类型为uint32_tstd::to_underlying(r)返回0xff0000,类型为uint32_t- 避免重复写模板语法,更易读
3⃣ 优势
- 简洁
- 不再需要写长模板
static_cast<std::underlying_type_t<E>>(...)
- 不再需要写长模板
- 安全
- 明确意图:将枚举转换为底层类型
- 适用范围
- 适用于 强类型枚举(enum class)
- 对普通
enum也适用,但主要解决 enum class 的类型安全问题
总结:std::to_underlying()是 C++23 提供的语义化工具函数,专门用于获取枚举值的底层数值,让代码更简洁、安全、易读。
std::move_only_function<> 理解(含对比说明)
1⃣ 传统 std::function<> 的限制
int Process(std::function<int()> f) {
return f() * 2;
}
// 普通 lambda 可以调用
std::print("{}", Process([] { return 21; })); // 42
// 捕获 unique_ptr 的 lambda 会失败
std::cout << Process([p = std::make_unique<int>(42)] { return *p; });
- 原因:
std::function内部需要拷贝 lambda 对象 - 如果 lambda 捕获了 不可拷贝的对象(如
std::unique_ptr)- 拷贝构造函数被删除
- 编译器报错
"Attempting to reference a deleted function"
2⃣ C++23 的 std::move_only_function<> 解决方案
int Process(std::move_only_function<int()> f) {
return f() * 2;
}
// 普通 lambda
std::print("{}", Process([] { return 21; })); // 42
// 捕获 unique_ptr 的 lambda 也可以
std::print("{}", Process([p = std::make_unique<int>(42)] { return *p; })); // 84
- 原理:
std::move_only_function<>仅移动 lambda - 内部不需要拷贝,所以允许捕获不可拷贝对象
- 保留
std::function的 type-erasure 特性,同时扩展了适用场景
3⃣ 对比总结
| 特性 | std::function<> |
std::move_only_function<> |
|---|---|---|
| Lambda 捕获可拷贝对象 | 支持 | 支持 |
| Lambda 捕获不可拷贝对象 | 不支持 | 支持 |
| 是否可拷贝 | (仅可移动) | |
| 内存开销 | 较大(拷贝 + type-erasure) | 较小(移动 + type-erasure) |
| 总结: |
std::move_only_function<>是 C++23 为解决 捕获不可拷贝资源的 lambda 调用而引入的- 它允许在函数参数中使用 只移动的 lambda,大大增强了灵活性和性能
std::expected 理解
1⃣ 基本概念
- 定义在
<expected> - 类型:
std::expected<T, E>- T:期望值类型(expected value type)
- E:错误类型(error type)
- 保证 永远不为空,要么有值,要么有错误
- 创建错误值:
std::unexpected()
2⃣ 示例
#include <expected>
#include <string>
#include <iostream>
int main() {
// 包含正常值
std::expected<int, std::string> a { 21 };
// 包含错误值
std::expected<int, std::string> b { std::unexpected("Some error"s) };
// 判断是否有值
if (a.has_value()) {
std::cout << "a 的值: " << a.value() << "\n";
} else {
std::cout << "a 的错误: " << a.error() << "\n";
}
if (b.has_value()) {
std::cout << "b 的值: " << b.value() << "\n";
} else {
std::cout << "b 的错误: " << b.error() << "\n";
}
}
输出:
a 的值: 21
b 的错误: Some error
3⃣ 成员函数
| 成员函数 | 功能说明 |
|---|---|
has_value() |
返回 true 表示包含期望值,false 表示包含错误值 |
value() |
返回期望值引用,如果没有值则抛出 bad_expected_access |
error() |
返回错误值引用 |
4⃣ 使用场景
- 替代传统的 返回错误码 + 输出参数 的方式
- 避免抛异常开销,但依然提供安全的错误处理机制
- 适合 函数可能失败 的场景,例如文件操作、解析、网络请求等
总结: std::expected<T, E>是 C++23 新增的类型安全错误处理工具- 它明确区分了 正常值 与 错误值,可读性和安全性都更高
- 与
std::optional<T>相比,额外提供了 错误信息
C++23 Views 库更新 理解
C++23 对 Ranges/Views 库做了大量扩展,主要是增加了一些方便处理序列和组合序列的视图(view)。下面详细解释各个新特性。
1⃣ views::zip 与 views::zip_transform
- 功能:把多个 view 的第 n 个元素组合成一个 tuple。
- zip:直接组合
- zip_transform:组合的同时应用一个操作(如加、乘等)
std::vector v1 {1, 2};
std::vector v2 {'a', 'b', 'c'};
std::vector v3 {3, 4, 5};
auto r1 = std::views::zip(v1, v2);
// {(1,'a'),(2,'b')}
auto r2 = std::views::zip_transform(std::multiplies(), v1, v3);
// {3, 8} // 1*3=3, 2*4=8
2⃣ views::adjacent 与 views::adjacent_transform
- adjacent:每个元素是原序列中连续 N 个元素的 tuple
- adjacent_transform:每个元素是对连续 N 个元素应用某操作的结果
std::vector v {1,2,3,4};
auto r3 = v | std::views::adjacent<2>;
// {(1,2),(2,3),(3,4)}
auto r4 = v | std::views::adjacent_transform<2>(std::multiplies());
// {2,6,12} // 1*2=2, 2*3=6, 3*4=12
- 别名:
views::pairwise = views::adjacent<2>views::pairwise_transform = views::adjacent_transform<2>
auto r5 = v | std::views::pairwise;
auto r6 = v | std::views::pairwise_transform(std::plus()); // 相邻元素求和
3⃣ views::slide
- 功能:滑动窗口,类似
adjacent,但窗口大小在运行时指定
std::vector v {1,2,3,4,5};
auto r7 = v | std::views::slide(2);
// {(1,2),(2,3),(3,4),(4,5)}
4⃣ views::chunk 与 views::chunk_by
- chunk(N):按固定大小 N 分块
auto r8 = v | std::views::chunk(2);
// {(1,2),(3,4),(5)}
- chunk_by(predicate):按满足条件的块分组
std::vector v {1,2,2,3,0,4,5,2};
auto r9 = v | std::views::chunk_by(std::ranges::less_equal{});
// {(1,2,2,3),(0,4,5),(2)}
5⃣ views::join_with
- 功能:将序列元素用指定分隔符连接
std::vector<std::string> v {"Hello","World","!"};
for(auto c: v | std::views::join_with('\n')) std::print("{}", c);
// 输出:
// Hello
// World
// !
6⃣ views::stride
- 功能:选取等间距元素
std::vector v {1,2,3,4,5};
auto r10 = v | std::views::stride(2); // {1,3,5}
7⃣ views::repeat
- 功能:重复元素
auto r11 = std::views::repeat(2,3); // {2,2,2} 重复3次
auto r12 = std::views::repeat(2); // {2,2,2,...} 无限重复
8⃣ views::cartesian_product
- 功能:生成多个 range 的笛卡尔积
std::vector v {0,1,2};
for(auto&& [a,b] : std::views::cartesian_product(v,v))
std::println("{} {}", a,b);
// 输出:
// 0 0
// 0 1
// 0 2
// 1 0
// 1 1
// 1 2
// ...
9⃣ views::as_rvalue
- 功能:将序列元素视为 rvalue,用于移动操作
std::vector<std::string> words {"Hello","World","2023"};
std::vector<std::string> movedWords;
std::ranges::copy(words | std::views::as_rvalue, std::back_inserter(movedWords));
// words 的元素被移动到 movedWords
总结:
- 这些新视图使 对序列进行组合、分块、滑动窗口、笛卡尔积、重复、步长等操作 更加简洁直观
- 结合 Ranges + Views 可以写出类似 Python/Pandas 风格的链式操作
- 适合处理 大数据序列、算法流水线和懒计算
C++23 Ranges 库更新 理解
C++23 对 Ranges 库(不仅是 Views,还包括对 range 的操作算法)增加了许多新功能,让序列处理更方便、直观。下面详细解释各个新特性。
1⃣ ranges::starts_with / ranges::ends_with
- 功能:检查一个 range 的开始或结尾是否匹配另一个 range
- 示例:
std::vector v1{11, 22, 33, 44};
std::vector v2{11, 22};
std::println("{}", std::ranges::starts_with(v1, v2)); // true
std::println("{}", std::ranges::ends_with(v1, v2)); // false
- 理解:类似字符串的
startswith/endswith,现在可以对任意 range 使用。
2⃣ ranges::shift_left / ranges::shift_right
- 功能:对 range 元素整体左移或右移
- 示例:
std::vector<std::string> v{"a","b","c","d","e"};
std::ranges::shift_left(v, 2);
// {"c","d","e","",""}
std::ranges::shift_right(v, 1);
// {"","c","d","e",""}
- 理解:左移/右移会在空缺位置填充默认构造值(如空字符串)。
3⃣ ranges::to()
- 功能:将一个 range 转换为某种容器
- 示例:
auto ints = std::views::iota(1, 5)
| std::views::transform([](auto v){ return v*2; });
auto vec = std::ranges::to<std::vector>(ints);
std::print("{}", vec); // [2,4,6,8]
- 可选用法:
- 转容器类型不同:
std::vector vec{33,11,22};
auto set1 = std::ranges::to<std::set>(vec); // set<int>
auto set2 = vec | std::ranges::to<std::set<double>>(); // set<double>
std::set<double> set3{std::from_range, vec}; // 使用 from_range 构造
- 结合 Views 使用:
std::string text{"Lorem ipsum dolor sit amet"};
auto words = text
| std::views::split(' ')
| std::views::transform([](auto&& v){ return std::string{std::from_range, v}; })
| std::ranges::to<std::vector>();
// ["Lorem","ipsum","dolor","sit","amet"]
4⃣ 查找与判断
- 查找最后匹配元素:
ranges::find_last(range, value)ranges::find_last_if(range, predicate)ranges::find_last_if_not(range, predicate)- 返回从找到元素到末尾的子 range,如果未找到返回空 range
{last,last}
- 判断包含:
ranges::contains(range, value)→ true/falseranges::contains_subrange(range, subrange)→ true/false
std::vector v1{11,22,33,44};
std::vector v2{33,44};
std::println("{}", std::ranges::contains(v1, 22)); // true
std::println("{}", std::ranges::contains_subrange(v1, v2));// true
5⃣ Folding 算法
- 功能:折叠(累积)range 元素
- 新算法:
fold_left(),fold_left_first()fold_right(),fold_right_last()fold_left_with_iter(),fold_left_first_with_iter()
- 示例:
std::vector v{11,22,33,44};
std::println("{}", std::ranges::fold_left(v, 0, std::plus())); // 110
// 等价于 (((0+11)+22)+33)+44
- 理解:
fold_left:从左到右累加fold_right:从右到左累加- 支持自定义初始值和操作符
- 类似函数式编程中的
reduce/fold
总结:
- C++23 Ranges 库强化了序列处理:
- starts_with/ends_with:检查开头/结尾
- shift_left/right:移动元素
- to():range → 容器
- contains / contains_subrange:判断是否包含
- fold 系列:高阶折叠算法
- 可以结合 Views 做 链式懒计算,极大提高代码简洁性和可读性。
C++23 Stacktrace 库 理解
C++23 新增了 Stacktrace 库(<stacktrace>),用于获取和处理程序运行时的调用栈信息。它可以帮助调试、日志记录以及异常处理。
1⃣ 获取当前调用栈
- 函数:
std::stacktrace::current() - 返回值:
std::stacktrace对象,包含当前线程的调用栈帧信息。
auto trace = std::stacktrace::current();
std::println("{}", trace);
- VC++ 输出示例:
0> D:\test\ConsoleApplication2.cpp(14): ConsoleApplication2!main+0x63
1> D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(79): ConsoleApplication2!invoke_main+0x39
2> ...
- 理解:每一行显示一个调用帧(函数 + 偏移量 + 文件位置)。
2⃣ 异常中自动包含 Stacktrace
- 用法:在异常对象中存储
std::stacktrace,方便捕获时打印。
class MyException : public std::exception {
public:
MyException(std::string message, std::stacktrace st = std::stacktrace::current())
: m_message{std::move(message)}, m_stacktrace{std::move(st)} {}
const char* what() const noexcept override { return m_message.c_str(); }
const std::stacktrace& trace() const noexcept { return m_stacktrace; }
private:
std::string m_message;
std::stacktrace m_stacktrace;
};
void bar() { throw MyException{"Some exception..."}; }
void foo() { bar(); }
int main() {
try { foo(); }
catch (const MyException& e) {
std::println("Exception caught: {}", e.what());
std::println("Stacktrace:\n{}", e.trace());
}
}
- 效果:
- 异常信息 + 调用栈输出
- 可以快速定位问题发生的位置
3⃣ 遍历 stacktrace 的帧
- 用法:通过范围 for 遍历每个
frame
auto trace = std::stacktrace::current();
for (auto&& frame : trace) {
std::print("{}", frame.description());
}
- VC++ 输出示例:
ConsoleApplication2!main+0x63
ConsoleApplication2!invoke_main+0x39
ConsoleApplication2!__scrt_common_main_seh+0x12E
ConsoleApplication2!__scrt_common_main+0xE
ConsoleApplication2!mainCRTStartup+0xE
KERNEL32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
- 理解:
frame.description()返回每个调用帧的详细描述(函数名 + 偏移 + 模块信息)- 可以结合日志系统或调试工具使用
4⃣ 使用场景
- 调试:快速查看异常发生时的调用路径
- 日志记录:捕获错误堆栈信息,方便远程问题定位
- 性能分析:在分析函数调用链时,可以动态生成调用栈信息
总结:
<stacktrace>是 C++23 的标准库特性,不再依赖平台特定 APIstd::stacktrace是不可修改的快照,记录调用栈帧- 可以与异常结合,自动记录堆栈信息
- 提供迭代器接口,可逐帧访问或打印
C++23 basic_string::resize_and_overwrite() 详细理解(示例强化版)
resize_and_overwrite 是 C++23 为 std::basic_string 新增的性能优化函数,它允许在调整字符串大小的同时直接操作底层缓冲区,避免多次重复开销。下面详细解析你的示例。
1⃣ 函数行为回顾
std::size_t resize_and_overwrite(std::size_t count, auto op)
- 功能:
- 将字符串大小调整为
count - 调用操作函数
op(char* data, size_t count)写入内容 - 根据操作函数返回的实际长度
r裁剪多余部分
- 将字符串大小调整为
- 逻辑:
count <= size()→ 删除尾部size() - count个字符count > size()→ 扩展尾部,增加(count - size())个默认字符(通常为'\0')- 调用
r = op(data(), count)写入内容 - 删除多余部分:
erase(begin() + r, end())
2⃣ 常规示例(低效方式)
std::string GeneratePattern(const std::string& pattern, size_t count) {
std::string result;
result.reserve(pattern.size() * count);
for (size_t i = 0; i < count; i++) {
// Not optimal:
// - Writes 'count' nulls
// - Updates size and checks for potential resize 'count' times
result.append(pattern);
}
return result;
}
- 问题:
- 每次
append(pattern)都会修改size()并检查容量 - 如果
count很大,会多次重复写入、调整size(),效率低 - 虽然
reserve()避免了多次 realloc,但仍然存在多次尾部初始化开销
- 每次
3⃣ 使用 resize_and_overwrite 的高效实现
std::string GeneratePattern(const std::string& pattern, size_t count) {
std::string result;
const auto step = pattern.size();
// GOOD: No initialization, single allocation
result.resize_and_overwrite(step * count, [&](char* buf, size_t n) {
for (size_t i = 0; i < count; i++) {
// GOOD: 直接在缓冲区写入,不涉及 size() 更新
memcpy(buf + i * step, pattern.data(), step);
}
return step * count; // 实际写入长度
});
return result;
}
分析:
- 预分配一次空间
resize_and_overwrite(step * count, ...)直接在底层缓冲区扩展到目标大小,无需逐次追加
- 直接写入
memcpy(buf + i*step, pattern.data(), step)将每次模式直接拷贝到缓冲区对应位置- 避免了
append的额外size()更新和边界检查
- 返回实际写入长度
return step * count;- 字符串尾部多余空间(如果有)会被
erase裁剪掉
- 效果:
- 性能提升明显
- 内存操作更集中,减少重复初始化
4⃣ 总结
resize_and_overwrite= “一次性分配 + 直接写入 + 根据返回值裁剪”- 适合高性能字符串构造场景,例如:
- 拼接大量固定模式
- 序列化/反序列化
- 大量二进制数据处理
核心对比:
| 方法 | 操作次数 | 性能 | 内存开销 |
|---|---|---|---|
| append 循环 | 多次写入 & size() 更新 | 低 | 较高 |
| resize_and_overwrite | 单次分配 & 写入 | 高 | 较低 |
C++23 std::optional 的单子(Monadic)操作 详细理解
C++23 为 std::optional 新增了 monadic-style 的成员函数,使得对可选值的操作可以链式调用,而无需每次都判断是否为空。下面详细解析你的示例。
1⃣ 新增成员函数
假设有 std::optional<T> opt:
| 函数 | 功能 |
|---|---|
transform(F) |
如果 opt 有值,返回 optional<F(*opt)>;否则返回空 optional |
and_then(F) |
如果 opt 有值,返回 F(*opt),F 必须返回 optional<U>;否则返回空 optional |
or_else(F) |
如果 opt 有值,返回 *this;否则返回 F() 的结果(必须是 optional<T>) |
这些函数体现了 单子(monad)风格,类似 Haskell 或 Rust 的
Option<T>。
2⃣ 示例解析
std::optional<int> Parse(const std::string& s) {
try { return std::stoi(s); } // 尝试将字符串转为整数
catch (...) { return {}; } // 转换失败返回空 optional
}
int main() {
while (true) {
std::string s;
std::getline(std::cin, s);
auto result = Parse(s)
.and_then([](int value) -> std::optional<int> {
return value * 2;
})
.transform([](int value) {
return std::to_string(value);
})
.or_else([] {
return std::optional<std::string>{"No Integer"};
});
std::println("{}", *result);
}
}
3⃣ 操作链逐步解释
假设输入 "12":
Parse("12")→optional<int>{12}.and_then(...)- 调用 lambda:
12 * 2 = 24 - 返回
optional<int>{24}
- 调用 lambda:
.transform(...)- 调用 lambda:
std::to_string(24)→"24" - 返回
optional<std::string>{"24"}
- 调用 lambda:
.or_else(...)opt有值 → 返回自身"24"
输出:
24
输入 "ee"(非数字):
Parse("ee")→ 空optional<int>{}.and_then(...)→ 空,直接跳过 lambda.transform(...)→ 空,直接跳过 lambda.or_else(...)→ 调用 lambda 返回"No Integer"
输出:
No Integer
4⃣ 总结
- 链式操作:
- 可以把对
optional的操作组合成管道,不需要每次检查has_value()
- 可以把对
- 函数职责分离:
transform→ 转换值类型and_then→ 链接返回optional的操作or_else→ 提供默认值
- 优势:
- 代码简洁,可读性高
- 避免嵌套的
if(opt)判断 - 函数式风格,易于组合
小技巧:
transform和and_then的差别:transform(F)→ F 返回 普通类型,结果会被包成optionaland_then(F)→ F 返回 optional 类型,适合链式传递可能为空的操作
更多推荐


所有评论(0)