第27章 C++23语言特性(最新正式标准)
第27章 C++23语言特性(最新正式标准)
📅 C++23标准于2024年正式发布,带来了大量让程序员"哇塞"的新特性。如果说C++17是"还凑合",C++20是"有点东西",那C++23绝对是"这也太香了吧"!准备好了吗?让我们一起踏上这场C++的版本升级之旅!
27.1 if consteval:编译器的时间旅行探测器
什么是consteval上下文?
在C++的世界里,有些代码需要在编译期执行(比如模板元编程),有些则在运行期执行。if consteval是C++23引入的一个革命性特性,它能让你在代码中"询问":“嘿,我现在是在常量求值的语境里吗?”
这就像给你的代码装了一个时间探测器——它能告诉你现在是在"编译器的魔法世界"还是在"运行时的现实世界"。
工作原理
if consteval是C++23独有的一种if语句写法,它会在编译期求值:
- 如果条件为true(我们在常量上下文中),执行if分支
- 如果条件为false(我们在运行时),执行else分支
适用场景
这个特性主要用于:
- 区分编译期和运行期行为——比如你想让某个函数只能在编译期被调用
- 优化代码生成——根据上下文选择更高效的算法
- 构建更智能的元编程库——让模板代码更优雅
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <iostream>
// C++23: if consteval - 判断是否在常量求值上下文中
// constexpr函数允许在编译期或运行期求值(灵活版)
// 注意:如果你真的想让函数只能在编译期调用,使用consteval替代constexpr
constexpr void assertCompileTime() {
// if consteval会在编译期判断当前是否处于常量求值上下文
if consteval {
// 如果在编译期求值,走到这里
// 注意:从非constexpr上下文调用constexpr函数,这里不会执行
std::cout << "Running at compile time" << std::endl;
} else {
// 如果在运行时求值,走到这里
std::cout << "Running at runtime" << std::endl;
}
}
int main() {
// 调用constexpr函数,这里会在运行期求值
// 因为是从非constexpr上下文调用的,所以会输出 "Running at runtime"
assertCompileTime();
return 0;
}
|
编译运行:
g++ -std=c++23 -o consteval_test consteval_test.cpp
./consteval_test
// 输出: Running at runtime
幽默解读
想象一下你和编译器对话:
你: "亲爱的编译器,你现在是在编译还是在运行?"
编译器: "编译!正在把你的代码变成01串!"
你: "太好了,那我给你一个只有编译期才能吃的蛋糕!"
if consteval就是那个问话的方式!🎂
进阶用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <iostream>
// 模拟一个"智能"函数
consteval int factorial(int n) {
if consteval {
// 编译期:使用递推(更快)
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
} else {
// 运行期:使用递归(更慢,但更"酷")
return n <= 1 ? 1 : n * factorial(n - 1);
}
}
int main() {
// 编译期计算!
constexpr int fact5 = factorial(5); // 120
std::cout << "5! = " << fact5 << std::endl; // 输出: 5! = 120
return 0;
}
|
对比:if consteval vs std::is_constant_evaluated()
在C++20中,我们有std::is_constant_evaluated()函数可以使用。C++23的if consteval更加优雅:
| 特性 | std::is_constant_evaluated() | if consteval |
|---|
| C++版本 | C++20 | C++23 |
| 用法 | if (std::is_constant_evaluated()) | if consteval |
| 可读性 | 较繁琐 | 简洁明了 |
| 必须在常量上下文 | 否 | consteval函数隐式要求 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| #include <iostream>
#include <type_traits>
// C++20风格
constexpr int oldWay(int x) {
if (std::is_constant_evaluated()) {
return x * 2;
} else {
return x + 2;
}
}
// C++23风格 - 更优雅!
consteval int newWay(int x) {
if consteval {
return x * 2;
} else {
return x + 2;
}
}
int main() {
constexpr int a = oldWay(10); // 编译期: 20
constexpr int b = newWay(10); // 编译期: 20
int c = oldWay(10); // 运行期: 12
std::cout << "oldWay编译期: " << a << std::endl; // 输出: 20
std::cout << "newWay编译期: " << b << std::endl; // 输出: 20
std::cout << "oldWay运行期: " << c << std::endl; // 输出: 12
return 0;
}
|
💡 小提示:consteval函数必须返回可以从常量表达式构造的类型,否则会导致编译错误。把它想象成"编译期超人"——什么都要在编译时搞定!
27.2 显式对象参数(Deducing this):给this指针起个昵称
什么是显式对象参数?
在C++23之前,如果你想在成员函数内部访问this指针,只能通过隐式的this关键字。C++23引入了一个新语法——显式对象参数(Explicit Object Parameter),让你可以"显式地"声明一个代表对象本身的参数。
这就像是给你的this指针起了一个昵称,让代码更加清晰明了!
语法解释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 传统写法
struct Counter {
int value = 0;
void increment() {
++this->value; // 隐式this
}
};
// C++23显式对象参数写法
struct Counter {
int value = 0;
void increment(this Counter& self) { // 显式声明"self"代表this
++self.value; // 直接用self
}
};
|
注意this Counter& self的位置——它在参数列表的最前面,但this关键字是固定的,后面跟着的是对象的类型和参数名。
为什么需要这个特性?
- 代码更清晰——明确知道函数要操作哪个对象
- 支持更多操作符重载——可以让函数对象也享受成员函数的语法
- 统一语法——Lambda可以像函数一样声明,现在成员函数也可以像普通函数一样声明
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <iostream>
struct Counter {
int value = 0;
// C++23: 显式对象参数
// "this Counter& self" 意味着self是一个Counter引用
void increment(this Counter& self) {
++self.value;
}
// 重载版本:const对象调用这个
int get(this const Counter& self) {
return self.value;
}
};
int main() {
Counter c;
c.increment(); // 调用increment,self绑定到c
std::cout << "Counter value: " << c.get() << std::endl; // 输出: 1
return 0;
}
|
编译运行:
g++ -std=c++23 -o counter counter.cpp
./counter
// 输出: Counter value: 1
幽默解读
以前:
你(对着空气喊):"this!你给我把value加1!"
this(黑人问号):"谁是this?你在叫我吗?"
C++23:
你(对着self喊):"self!把value加1!"
self(自信满满):"收到!这就是我的value,我来搞定!"
更多有趣的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #include <iostream>
#include <string>
structPrinter {
// 使用显式对象参数打印任何东西
void print(this auto& self, const std::string& msg) {
std::cout << msg << std::endl;
}
// 链式调用!
auto then(this auto& self, const std::string& msg) {
std::cout << msg << std::endl;
return self; // 返回自身以便继续链式调用
}
};
int main() {
Printer p;
p.print("Hello, C++23!"); // 输出: Hello, C++23!
p.then("Step 1").then("Step 2").then("Step 3");
// 输出: Step 1
// 输出: Step 2
// 输出: Step 3
return 0;
}
|
适用场景
- 实现链式调用——返回
*this以支持fluent API - 泛型成员函数——
this auto&让你写出适用于整个类层次结构的代码 - 替代完美转发——让函数对象可以被"调用"得像函数一样
💡 小提示:显式对象参数是C++23全新的语法形式,void foo()和void foo(this X&)是两个完全不同的函数,它们具有不同的签名。不要被"deducing this"这个名字迷惑——虽然"this"是推导出来的,但函数的签名是崭新的!
27.3 多维下标运算符:矩阵先生的专属电梯
什么是多维下标运算符?
在C++23之前,如果你想用operator(i, j)来访问二维数据,你必须自己实现。C++23引入了多维下标运算符,让obj(i, j, k...)这样的语法成为标准!
这就好比给矩阵先生安装了一部可以直达任意楼层的电梯——以前你得自己爬楼梯,现在一步到位!
适用场景
- 矩阵运算——科学计算、机器学习必备
- 图像处理——像素访问
- 表格数据——Excel-like数据结构
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| #include <iostream>
class Matrix {
// 3x3矩阵,用一维数组存储
int data_[9];
public:
// C++23: 多维下标运算符
// operator()可以接受多个参数
int& operator()(int row, int col) {
return data_[row * 3 + col];
}
const int& operator()(int row, int col) const {
return data_[row * 3 + col];
}
// 打印矩阵(辅助函数)
void print() const {
std::cout << "Matrix:" << std::endl;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << (*this)(i, j) << " ";
}
std::cout << std::endl;
}
}
};
int main() {
Matrix m;
// 赋值
m(0, 0) = 1; m(0, 1) = 2; m(0, 2) = 3;
m(1, 0) = 4; m(1, 1) = 5; m(1, 2) = 6;
m(2, 0) = 7; m(2, 1) = 8; m(2, 2) = 9;
// 读取
std::cout << "m(1,2) = " << m(1, 2) << std::endl; // 输出: 6
m.print();
return 0;
}
|
编译运行:
g++ -std=c++23 -o matrix matrix.cpp
./matrix
// 输出: m(1,2) = 6
// Matrix:
// 1 2 3
// 4 5 6
// 7 8 9
幽默解读
矩阵先生:你好,我是矩阵!我有9个房间!
程序员:我要去第2行第1列的房间!
矩阵先生:好的,请用m(1, 0)来访问!
程序员:等等,我数数是0开始的...
矩阵先生:不客气,这是我专属的电梯,按下就直达!
更复杂的例子:三维矩阵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #include <iostream>
#include <vector>
class Tensor3D {
std::vector<int> data_;
int dim1_, dim2_, dim3_;
public:
Tensor3D(int d1, int d2, int d3)
: dim1_(d1), dim2_(d2), dim3_(d3) {
data_.resize(d1 * d2 * d3, 0);
}
// C++23: 三维下标运算符
int& operator()(int i, int j, int k) {
return data_[i * dim2_ * dim3_ + j * dim3_ + k];
}
const int& operator()(int i, int j, int k) const {
return data_[i * dim2_ * dim3_ + j * dim3_ + k];
}
};
int main() {
Tensor3D t(2, 3, 4); // 2x3x4的三维张量
t(1, 2, 3) = 100; // 设置一个值
std::cout << "t(1,2,3) = " << t(1, 2, 3) << std::endl; // 输出: 100
return 0;
}
|
对比总结
| 特性 | C++20及之前 | C++23 |
|---|
| 二维访问 | m[i * cols + j] | m(i, j) |
| 可读性 | 需要自己计算索引 | 语义清晰 |
| 维度数量 | 需要重载多个operator() | 天然支持任意维度 |
💡 小提示:多维下标运算符只是operator()的扩展,没有任何新关键字。它完全向后兼容——你的旧代码照常工作!
27.4 静态operator[]和静态operator():不用实例就能调用的黑魔法
什么是静态运算符?
在C++23之前,静态成员函数我们见过,运算符重载我们也见过。但你听说过**静态operator[]或静态operator()**吗?
这是C++23引入的一个小而美的特性——让你可以不用创建对象,直接用类名就能调用[]和()运算符!
使用场景
- 命名空间替代品——创建类似函数的静态集合
- 配置管理——集中管理一组配置项
- 工具类——提供便捷的静态方法
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <iostream>
class Registry {
public:
// 静态数据成员
static int data[5];
// C++23: 静态operator[]
// 可以像数组一样用类名访问
static int& operator[](int index) {
return data[index];
}
// C++23: 静态operator()
// 可以像函数一样用类名调用
static int operator()(int x) {
return x * x;
}
};
// 初始化静态成员
int Registry::data[5] = {0, 10, 20, 30, 40};
int main() {
// 用类名调用operator[]
Registry[2] = 99; // 不用创建实例!
std::cout << "Registry[2] = " << Registry[2] << std::endl; // 输出: 99
// 用类名调用operator()
std::cout << "Registry(7) = " << Registry(7) << std::endl; // 输出: 49
return 0;
}
|
编译运行:
g++ -std=c++23 -o registry registry.cpp
./registry
// 输出: Registry[2] = 99
// 输出: Registry(7) = 49
幽默解读
C++23之前的世界:
程序员:我要用一个工具函数!
系统:好的,请先new一个实例...
程序员:我只是想要一个简单的加法!
系统:抱歉,必须先实例化,这是规定。
C++23的世界:
程序员:我要用一个工具函数!
系统:好的,直接Registry(3)就能用!
程序员:哇,这么方便!
系统:欢迎来到C++23,程序员的天堂!
实用例子:数学工具箱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <iostream>
#include <cmath>
class MathUtils {
public:
// 静态三角函数查表(假设已经预先计算好了)
static double sin_table[360];
// 静态operator[] - 查表获取sin值
static double& operator[](int degrees) {
// 简化示例:实际应该处理负数和大于360的情况
int idx = ((degrees % 360) + 360) % 360;
return sin_table[idx];
}
// 静态operator() - 计算sin值(使用实际数学运算)
static double operator()(double radians) {
return std::sin(radians);
}
};
// 初始化sin表(简化版)
double MathUtils::sin_table[360] = {0};
int main() {
MathUtils::sin_table[90] = 1.0; // sin(90°) = 1
MathUtils::sin_table[180] = 0.0; // sin(180°) = 0
std::cout << "sin(90°) via table = " << MathUtils[90] << std::endl; // 输出: 1
std::cout << "sin(PI/2) via func = " << MathUtils(3.14159/2) << std::endl; // 约等于1
return 0;
}
|
对比传统实现
| 方式 | C++20 | C++23 |
|---|
| 静态函数 | MathUtils::compute(x) | MathUtils::compute(x) |
| 静态operator[] | ❌ 不支持 | MathUtils[index] |
| 静态operator() | ❌ 不支持 | MathUtils(x) |
| 可调用对象 | ✅ | ✅ |
💡 小提示:静态运算符只是让你不用创建对象就能调用,但它们仍然是类的成员。这意味着你可以访问类的其他静态成员,也可以访问类的私有静态数据(如果在同一类中)。
27.5 auto(x)和auto{x}:decay-copy的优雅实现
什么是decay-copy?
Decay-copy(衰变复制)是一个听起来很"量子力学"的概念。在C++中,它指的是将一个引用类型的值"复制并衰变"成对应的值类型。
简单来说:
const int& → int(引用变值)std::vector<int>& → std::vector<int>(保持类型,但变成真正的对象)
为什么要decay-copy?
在泛型编程中,我们经常遇到这种情况:
1
2
3
4
5
6
| template<typename T>
void process(T&& arg) {
// arg是一个转发引用,但我想保存一份副本
// 以前:T copy = arg; // 可能不对
// 现在:auto copy = auto(arg); // 完美!
}
|
C++23的auto(x)和auto{x}
C++23引入了两种新的变量声明形式:
auto(x) —— 将x decay-copy,结果是纯右值auto{x} —— 将x copy初始化,结果是纯右值
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #include <iostream>
#include <vector>
#include <type_traits>
int main() {
// C++23: auto(x) 和 auto{x} - decay-copy
const int& ref = 42; // ref是一个const int引用,绑定到临时int(42)
// decay-copy:将引用"衰变"成实际值
// auto(ref) 等价于: decay<decltype(ref)>(ref)
auto copied = auto(ref); // copied是int类型,值为42
std::cout << "copied = " << copied << std::endl; // 输出: copied = 42
std::cout << "type is int: " << std::is_same_v<decltype(copied), int> << std::endl; // 输出: 1
// auto{x} 形式(列表初始化)
auto copied2 = auto{ref}; // 同样是int,值为42
std::cout << "copied2 = " << copied2 << std::endl; // 输出: copied2 = 42
// 对于vector也一样
std::vector<int> vec{1, 2, 3};
const std::vector<int>& vecRef = vec;
// decay-copy成真正的vector
auto vecCopy = auto(vecRef); // vecCopy是std::vector<int>
std::cout << "vecCopy size: " << vecCopy.size() << std::endl; // 输出: 3
return 0;
}
|
编译运行:
g++ -std=c++23 -o decay decay.cpp
./decay
// 输出: copied = 42
// 输出: type is int: 1
// 输出: copied2 = 42
// 输出: vecCopy size: 3
幽默解读
程序员:ref,你知道吗,你其实不是一个真正的int,你只是一个int的"引用"!
ref:什么?!那我是什么?
程序员:你是一个int的"影子",一个"替身"!
ref:我...我不理解...
程序员:没关系,auto(ref)会把你"还原"成真正的int!
ref:(努力decay中...)
ref:哇,我真的变成int了!我感觉...真实了!
auto:欢迎来到值类型的世界,ref!
auto(x) vs auto{x} 的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <iostream>
#include <vector>
int main() {
// auto(x) - 直接初始化,decay
int arr[3] = {1, 2, 3};
const int (&arrRef)[3] = arr;
auto copied1 = auto(arrRef); // const int*(decay-copy保留const)
// 注意:数组会decay成指针,const属性也会保留!
// auto{x} - 列表初始化
// auto copied2 = auto{arrRef}; // 编译错误!不能列表初始化数组
// 但对于类类型...
std::vector<int> vec{1, 2, 3};
const std::vector<int>& vecRef = vec;
auto v1 = auto(vecRef); // std::vector<int>
auto v2 = auto{vecRef}; // std::vector<int>
std::cout << "v1 size: " << v1.size() << std::endl; // 输出: 3
std::cout << "v2 size: " << v2.size() << std::endl; // 输出: 3
return 0;
}
|
实际应用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #include <iostream>
#include <memory>
#include <vector>
// 假设我们有一个返回引用的函数
std::vector<int>& getGlobalVector() {
static std::vector<int> gvec{1, 2, 3};
return gvec;
}
int main() {
// 获取全局vector的引用
std::vector<int>& ref = getGlobalVector();
// 如果我们想创建一个独立的副本
// 用auto(x)可以明确表达"我要复制并decay"
auto copy = auto(ref);
// 修改copy不影响原vector
copy.push_back(4);
std::cout << "Original size: " << ref.size() << std::endl; // 输出: 3
std::cout << "Copy size: " << copy.size() << std::endl; // 输出: 4
return 0;
}
|
💡 小提示:auto(x)和auto{x}的关键在于,它们是显式的decay-copy操作。在泛型代码中,当你想要明确"我要复制一份,但不要引用"时,用它们比用普通的auto更清晰!
27.6 假设属性[[assume]]:与魔鬼的契约
什么是[[assume]]?
假设属性(Assume Attribute)是C++23引入的一个强大但危险的特性。它允许程序员向编译器声明:“我假设这个条件永远为真,你可以基于这个假设做优化。”
这就像和魔鬼签订契约——你给出承诺,编译器给予力量,但如果你的承诺是假的…后果自负!
使用场景
- 性能优化——告诉编译器某些不变量
- 分支消除——帮助编译器去掉不可能的分支
- SIMD优化——告诉编译器数据是对齐的
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <iostream>
int main() {
// C++23: [[assume(expr)]] - 告诉编译器可以假设条件为真
// 如果假设不成立,行为未定义(你死定了!)
int x = 5; // 运行时才知道值
// 告诉编译器:假设x >= 0
[[assume(x >= 0)]];
// 编译器可能会优化掉下面的分支
if (x < 0) {
std::cout << "This should never print" << std::endl;
}
std::cout << "x = " << x << std::endl; // 输出: x = 5
return 0;
}
|
幽默解读
程序员:编译器大人,我有个小小的请求!
编译器:说。
程序员:我保证x永远大于等于0!
编译器:真的?你发誓?
程序员:我发誓!(心虚)
编译器:好!那我把所有x<0的检查都删了!性能提升10倍!
程序员:太好了太好了!(如果x真的>=0的话)
... later ...
程序员:奇怪,程序崩溃了...
编译器:你骗我!你明明说x>=0!
程序员:我...我不知道会有人传负数啊!
编译器:哼!
实际应用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <iostream>
#include <vector>
#include <algorithm>
// 一个计算数组最大值的函数
int findMax(const std::vector<int>& arr) {
// 假设数组不为空(调用者保证)
[[assume(!arr.empty())]];
int maxVal = arr[0]; // 如果数组为空,这行 UB!
for (int i = 1; i < arr.size(); ++i) {
maxVal = std::max(maxVal, arr[i]);
}
return maxVal;
}
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6};
std::cout << "Max: " << findMax(numbers) << std::endl; // 输出: 9
return 0;
}
|
安全使用指南
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| #include <iostream>
// 不安全:假设可能是错的
void unsafeFunction(int* ptr) {
[[assume(ptr != nullptr)]]; // 如果ptr是nullptr,UB!
*ptr = 42; // 如果上面假设错了,程序崩溃
}
// 安全版本:先用assert检查
#include <cassert>
void safeFunction(int* ptr) {
assert(ptr != nullptr && "ptr must not be null");
[[assume(ptr != nullptr)]]; // 现在假设安全了
*ptr = 42;
}
int main() {
int x = 0;
safeFunction(&x);
std::cout << "x = " << x << std::endl; // 输出: 42
return 0;
}
|
GCC和Clang的支持
1
2
3
4
5
6
7
8
9
10
11
12
| // 在支持assume的编译器上
#if defined(__cpp_assume) && __cpp_assume >= 202302L
#define ASSUME(expr) [[assume(expr)]]
#else
// 回退到assert
#define ASSUME(expr) assert(expr)
#endif
int optimizedAbs(int x) {
ASSUME(x != INT_MIN); // 避免INT_MIN取反溢出
return x < 0 ? -x : x;
}
|
与[[likely]]/[[unlikely]]的对比
| 属性 | 用途 | 危险程度 |
|---|
[[likely]] | 提示编译器哪个分支更可能执行 | 安全 |
[[unlikely]] | 提示编译器哪个分支不太可能执行 | 安全 |
[[assume]] | 告诉编译器"这是真的",用于优化 | ⚠️ 危险 |
⚠️ 警告:[[assume]]是一把双刃剑。如果你声明的假设是错误的,程序将进入未定义行为(Undefined Behavior)。轻则程序崩溃,重则…你的电脑会开始播放你未来的视频(开玩笑的,但UB确实很可怕)!
27.7 Lambda属性:给匿名函数戴帽子
Lambda还能有属性?
在C++23之前,Lambda表达式可以有属性吗?答案是"…有点能"。你可以在Lambda前加属性,但那只是给Lambda的类型加属性,而不是Lambda本身。
C++23让Lambda可以像普通函数一样拥有属性了!
什么是Lambda属性?
属性(Attributes)是给代码加的元数据标签,告诉编译器一些额外信息。比如:
[[deprecated]] —— 这个东西过时了[[nodiscard]] —— 返回值别忽略[[likely]] —— 这个分支很可能执行
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #include <iostream>
int main() {
// C++23: Lambda可以有属性
// 一个被标记为过时的Lambda
[[deprecated("Use newLambda instead")]]
auto oldLambda = [](int x) {
return x * 2;
};
// 新的Lambda
auto newLambda = [](int x) {
return x * 3;
};
// 调用新版本
std::cout << "newLambda(5) = " << newLambda(5) << std::endl; // 输出: 15
// 旧版本还能用,但会警告(MSVC特有语法,此处改为GCC/Clang兼容写法)
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated"
#endif
std::cout << "oldLambda(5) = " << oldLambda(5) << std::endl; // 输出: 10
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
return 0;
}
|
幽默解读
Lambda(得意):我是一个匿名函数!我没有名字!
C++23:你现在可以戴帽子了!
Lambda:什么帽子?
C++23:属性帽子!比如[[deprecated]]帽子!
Lambda:戴上deprecated帽子会怎样?
C++23:别人用你的时候,编译器会喊"喂!这个Lambda过时了!"
Lambda:...那我不是很没面子?
C++23:面子不重要,重要的是告诉别人用新的!
实用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <iostream>
#include <vector>
#include <numeric>
int main() {
// nodiscard Lambda - 返回值不应该被忽略
[[nodiscard]]
auto computeSum = [](const std::vector<int>& v) {
return std::accumulate(v.begin(), v.end(), 0);
};
std::vector<int> nums{1, 2, 3, 4, 5};
// 正确用法:捕获返回值
int sum = computeSum(nums); // nodiscard提醒你保存结果
std::cout << "Sum: " << sum << std::endl; // 输出: Sum: 15
// 错误用法(如果有的话):computeSum(nums); // 返回值被忽略,编译器可能警告
return 0;
}
|
在泛型代码中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #include <iostream>
int main() {
// 属性可以放在参数列表后面、函数体前面
// C++23标准允许在Lambda上使用以下属性:
// 1. [[nodiscard]]
[[nodiscard]]
auto createValue = [](int x) -> int {
return x * x;
};
// 2. [[deprecated]]
[[deprecated]]
auto oldFunc = [](int x) {
return x + 1;
};
// 3. [[likely]]
auto checkEven = [](int x) {
if (x % 2 == 0) [[likely]] {
return "even";
}
return "odd";
};
std::cout << createValue(7) << std::endl; // 输出: 49
std::cout << checkEven(4) << std::endl; // 输出: even
return 0;
}
|
Lambda属性位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| int main() {
// C++23中,Lambda属性的位置比较灵活:
// 位置1:Lambda前(给Lambda类型加属性)
[[nodiscard]] auto lambda1 = [](int x) { return x; };
// 位置2:mutable关键字后(给调用运算符加属性)
auto lambda2 = [](int x) [[nodiscard]] { return x; }; // C++23
// 注意:这是给operator()加属性
// 位置3:尾置返回类型后
auto lambda3 = [](int x) -> int [[nodiscard]] { return x; }; // C++23
std::cout << lambda1(1) << lambda2(2) << lambda3(3) << std::endl;
return 0;
}
|
💡 小提示:C++23中Lambda属性最常见的用法是[[nodiscard]]和[[deprecated]]。如果你在写一个库,给重要的Lambda加上这些属性,可以让用户更清楚地知道如何正确使用它们!
27.8 扩展浮点类型(std::float16_t等):数值计算的精确制导
什么是扩展浮点类型?
在C++23之前,标准库只提供了float(32位)、double(64位)和long double(平台相关)。C++23引入了标准化的扩展浮点类型:
std::float16_t —— 16位浮点数(半精度)std::float32_t —— 32位浮点数(单精度)std::float64_t —— 64位浮点数(双精度)std::bfloat16_t —— 16位"脑"浮点数(brain float)
为什么需要这些类型?
- 机器学习——低精度浮点运算更快更省内存
- 嵌入式系统——资源受限,需要节省内存
- 科学计算——某些场景需要精确控制精度
- 跨平台一致性——以前
float在不同平台上可能不一样
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <iostream>
#include <cstdint>
#include <limits>
#include <cmath>
int main() {
// C++23: 扩展浮点类型
// 注意:需要编译器支持才能真正使用
// 使用标准类型别名,明确指定精度
std::float16_t half = 0.5f; // 半精度
std::float32_t single = 3.14f; // 单精度
std::float64_t double_ = 3.14159; // 双精度
std::bfloat16_t brain = 6.28f; // bfloat16
std::cout << "Half precision value: " << half << std::endl;
std::cout << "Single precision value: " << single << std::endl;
std::cout << "Double precision value: " << double_ << std::endl;
std::cout << "Brain float value: " << brain << std::endl;
// 检查类型是否存在
std::cout << "\nType sizes:" << std::endl;
std::cout << "sizeof(float16_t) = " << sizeof(std::float16_t) << " bytes" << std::endl;
std::cout << "sizeof(float32_t) = " << sizeof(std::float32_t) << " bytes" << std::endl;
std::cout << "sizeof(float64_t) = " << sizeof(std::float64_t) << " bytes" << std::endl;
std::cout << "sizeof(bfloat16_t) = " << sizeof(std::bfloat16_t) << " bytes" << std::endl;
return 0;
}
|
幽默解读
float:我有32位!
double:我有64位!
long double:我...我也不知道我有多少位,平台说的算...
C++23:好了好了,别吵了,我给你们都发个"标准身份证"!
float16_t:我只有16位,是不是太少了?
C++23:你虽然位数少,但你快啊!内存省啊!
float16_t:真的吗?那我可以用来做AI推理!
C++23:没错,这就是你的用武之地!
bfloat16_t:我也是16位,但我更"大脑"!
C++23:你这名字确实很"大脑"...
bfloat16_t:我保留了32位的指数范围,只是精度低!
C++23:这让你特别适合深度学习!
float vs bfloat16 对比
┌─────────────────────────────────────────────────────────────┐
│ 32-bit Float (float) │
├─────────────┬───────────────────────────────────────────────┤
│ Sign (1) │ Exponent (8) │ Mantissa (23) │
└─────────────┴───────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 16-bit Brain Float (bfloat16) │
├─────────────┬───────────────────────────────────────────────┤
│ Sign (1) │ Exponent (8) │ Mantissa (7) │
└─────────────┴───────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 16-bit Float (float16_t) │
├─────────────┬───────────────────────────────────────────────┤
│ Sign (1) │ Exponent (5) │ Mantissa (10) │
└─────────────┴───────────────────────────────────────────────┘
💡 小提示:不同的16位浮点格式有不同的用途:
float16_t:IEEE 754半精度,适合图像处理bfloat16_t:适合深度学习,动态范围大- 如果你不确定用哪个,就用
float32_t——它和float完全一样!
27.9 新预处理器指令:#elifdef、#elifndef、#warning
C++的预处理器进化史
预处理器是C/C++编译前的一道"魔法",它在我们代码的基础上生成最终的代码。C++23给这个老古董添加了一些新玩具!
#elifdef 和 #elifndef
在C++23之前,如果你想写"elif if defined",你得这样:
1
2
3
4
5
| #if defined(FOO)
// ...
#elif defined(BAR)
// ...
#endif
|
现在,你可以更直观地写:
1
2
3
4
5
| #ifdef FOO
// ...
#elifdef BAR // C++23: elif if defined
// ...
#endif
|
#warning
这个更直接——让你在编译时发出警告!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| #include <iostream>
// C++23: 新的预处理器指令
// #elifdef: elif if defined (如果前面的#if/#elif不满足,检查这个是否定义)
// #elifndef: elif if not defined (如果前面的不满足,检查这个是否未定义)
// #warning: 产生一个编译警告
#define FEATURE_ENABLED
#define EXPERIMENTAL_MODE
int main() {
// 传统写法
#if defined(DEBUG)
std::cout << "Debug mode" << std::endl;
#elif defined(FEATURE_ENABLED)
std::cout << "Feature enabled" << std::endl; // 这行会被执行
#else
std::cout << "Default mode" << std::endl;
#endif
// C++23新写法 - 更直观!
#ifdef DEBUG
std::cout << "Debug mode" << std::endl;
#elifdef FEATURE_ENABLED // C++23: elifdef
std::cout << "Feature enabled (new syntax)" << std::endl; // 这行会被执行
#endif
#ifndef OBSOLETE_API // C++23: if not defined
std::cout << "Using modern API" << std::endl;
#endif
return 0;
}
|
#warning示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #include <iostream>
#define LEGACY_CODE
int main() {
// 发出警告
#warning "This code is using deprecated patterns!"
#ifdef LEGACY_CODE
#warning "LEGACY_CODE is defined, consider removing it"
#endif
std::cout << "Hello, C++23 preprocessor!" << std::endl;
return 0;
}
|
编译时你会看到:
warning: #warning "This code is using deprecated patterns!" [-Wcpp]
warning: #warning "LEGACY_CODE is defined, consider removing it" [-Wcpp]
Hello, C++23 preprocessor!
幽默解读
预处理器(得意):我虽然"老",但我是C++家族最资深的"魔法变换师"!
C++23:好好好,我知道你厉害,但我给你加了几个新咒语!
程序员:什么咒语?
C++23:#elifdef!#elifndef!#warning!
预处理器(眼睛发光):哦哦哦!我现在可以:
- elifdef = elif defined
- elifndef = elif not defined
- warning = 发出警告吓唬程序员!
程序员:等等,warning是用来吓唬程序员的?
预处理器:哈哈,开玩笑的!是用来提醒他们代码可能要过期了!
实际应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <iostream>
// 检测编译器的C++标准版本
#if __cplusplus >= 202302L
#define IS_CPP23 1
#else
#define IS_CPP23 0
#endif
#if IS_CPP23
#warning "Compiled with C++23! Welcome to the future!"
#else
#warning "Not C++23. Some features may not be available."
#endif
int main() {
std::cout << "C++ standard: " << __cplusplus << std::endl;
#if IS_CPP23
std::cout << "Using C++23 features!" << std::endl;
#endif
return 0;
}
|
💡 小提示:#warning在调试和代码迁移时特别有用。比如你可以用它来标记需要在未来版本中修改的代码,或者提醒团队成员某些编译选项的影响!
27.10 size_t字面量后缀’Z’/‘z’:终于等到你
为什么要给size_t加后缀?
在C++中,size_t是一个非常重要的类型——它是无符号整数,用于表示大小和索引。但长期以来,如果你想写一个size_t类型的字面量,你得这样:
1
2
| size_t len = 100; // 需要强制转换
size_t len = static_cast<size_t>(100); // 或者用static_cast
|
C++23终于给了size_t一个专属后缀:z或Z!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <iostream>
#include <cstddef>
#include <vector>
#include <format>
int main() {
// C++23: size_t字面量后缀
// z或Z后缀表示size_t类型
size_t len1 = 100z; // 使用z后缀
size_t len2 = 200Z; // 使用Z后缀(大写也行)
std::cout << "len1 = " << len1 << std::endl; // 输出: 100
std::cout << "len2 = " << len2 << std::endl; // 输出: 200
// 以前需要这样写:
// size_t len_old = 100; // 可能警告:精度丢失
// size_t len_cast = static_cast<size_t>(100); // 太繁琐
// 数组大小也可以用
int arr[50z]; // 50是size_t类型
std::cout << "Array size: " << (sizeof(arr) / sizeof(arr[0])) << std::endl; // 输出: 50
// 在泛型编程中特别有用
auto vec = std::vector<int>(100z); // 100是size_t
std::cout << "Vector size: " << vec.size() << std::endl; // 输出: 100
return 0;
}
|
幽默解读
size_t(委屈):为什么int有100L,long有100LL,我size_t什么都没有!
程序员:是啊,为什么呢?
size_t:每次我想写一个size_t字面量,都得强制转换!我太难了!
double有100.0,float有100.0f,就我什么都没有!
C++23:z!给你z!
size_t:真的吗?!
C++23:真的!100z,这就是你的专属后缀!
size_t(感动落泪):终于...终于等到了这一天!
C++23:别哭了,以后写size_t直接100z就行了!
size_t:太好了!我要把这个消息告诉所有size_t爱好者!
对比各种整数后缀
| 类型 | 后缀 | 示例 | 说明 |
|---|
| int | 无 | 100 | 默认 |
| long | L | 100L | |
| long long | LL | 100LL | |
| unsigned | U | 100U | |
| unsigned long | UL | 100UL | |
| size_t | z/Z | 100z | C++23新特性 |
实际应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <iostream>
#include <cstddef>
#include <vector>
// 模板函数:创建一个包含n个元素的vector
template<typename T>
std::vector<T> createVector(size_t n, T val) {
return std::vector<T>(n, val);
}
int main() {
// C++23之前:需要静态转换
// auto v1 = createVector(static_cast<size_t>(100), 42);
// C++23:直接写100z
auto v1 = createVector(100z, 42);
auto v2 = createVector(50z, 3.14);
std::cout << "v1.size() = " << v1.size() << std::endl; // 输出: 100
std::cout << "v2.size() = " << v2.size() << std::endl; // 输出: 50
// 用于索引
std::string s = "Hello, C++23!";
size_t idx = 7z; // 明确是size_t类型
std::cout << "s[" << idx << "] = " << s[idx] << std::endl; // 输出: C
return 0;
}
|
💡 小提示:z后缀在模板编程中特别有用。以前当你写T::npos时,返回值可能需要转换,现在你可以直接写100z让它变成size_t类型,编译器会非常感激你的!
27.11 空白字符修剪与行拼接:字符串处理的C++23升级
C++23对字符串做了什么?
C++23为字符串处理带来了几个小而美的改进:
std::string 视图与操作 —— 提供更方便的字符串操作视图- 行拼接说明明确化 —— C++23明确了续行符
\的行为(实际上自C++11起\后即可接空白)
trim() 系列函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| #include <iostream>
#include <string>
#include <string_view>
// C++23: trim系列函数(非成员函数,使用string_view)
int main() {
// C++23: 新的字符串修剪操作(使用std::ranges::trim或手动实现)
std::string s = " Hello, C++23! ";
// C++23中trim是ranges算法,这里演示手动实现的效果
// 实际使用可以用 std::ranges::trim(s) 返回一个视图
auto trimmed = [](std::string_view sv) -> std::string {
size_t start = sv.find_first_not_of(" \t\n\r");
size_t end = sv.find_last_not_of(" \t\n\r");
if (start == std::string_view::npos) return "";
return std::string(sv.substr(start, end - start + 1));
};
std::string_view sv = s;
std::string trimmedStr = trimmed(sv);
std::cout << "Original: '" << s << "'" << std::endl;
std::cout << "Trimmed: '" << trimmedStr << "'" << std::endl;
return 0;
}
|
输出:
Original: ' Hello, C++23! '
Trimmed: 'Hello, C++23!'
行拼接改进
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #include <iostream>
int main() {
// C++23改进了行拼接的说明,实际上自C++11起\后面就可以有空白
auto message = "Hello, " \
"C++23!";
std::cout << message << std::endl; // 输出: Hello, C++23!
// 使用续行符连接多行字符串
auto poem = "床前明月光,\
疑是地上霜。\
举头望明月,\
低头思故乡。";
std::cout << poem << std::endl;
return 0;
}
|
幽默解读
字符串(委屈):我两端有好多空白字符,但没人帮我清理!
C++23:trim来了!我帮你打扫!
字符串:真的吗?太好了!
字符串(trim后):哇!好清爽!感觉轻了好多!
程序员:我写代码的时候换行,\后面不能有空格,很烦!
C++23:那以后可以有空格了!
程序员:真的吗?
C++23:真的!\ 后面随便加空格!
程序员:谢谢!虽然是小改进,但太实用了!
实际应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <iostream>
#include <string>
#include <vector>
int main() {
// 处理用户输入(去除首尾空白)
std::vector<std::string> inputs = {
" Alice",
"Bob ",
" Charlie ",
" Dave "
};
std::cout << "Before and after trim:" << std::endl;
// trim辅助函数(C++23风格的手动实现)
auto trim = [](std::string_view sv) -> std::string {
size_t start = sv.find_first_not_of(" \t\n\r");
size_t end = sv.find_last_not_of(" \t\n\r");
if (start == std::string_view::npos) return "";
return std::string(sv.substr(start, end - start + 1));
};
for (const auto& name : inputs) {
std::string original = name;
std::string trimmed = trim(name);
std::cout << "'" << original << "' -> '" << trimmed << "'" << std::endl;
}
// 处理CSV数据
std::string csvLine = " 123, 456, 789 ";
std::cout << "CSV trimmed: '" << trim(csvLine) << "'" << std::endl;
return 0;
}
|
💡 小提示:trim()默认会去除所有空白字符(包括空格、制表符、换行符)。如果想去除其他字符,可以传入一个自定义的trim集合:
1
2
| std::string s = "xxxHelloxxx";
s.trim("x"); // 去除x字符
|
27.12 隐式移动简化:编译器更聪明了
什么是隐式移动?
在C++11引入移动语义之前,返回一个局部对象总是会触发拷贝:
1
2
3
4
| std::vector<int> createVector() {
std::vector<int> v{1, 2, 3};
return v; // C++98/03:拷贝整个vector
}
|
C++11引入了移动语义:
C++17引入了返回值优化(RVO)和强制拷贝省略,进一步优化。
C++23则进一步简化了隐式移动的规则!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| #include <iostream>
#include <vector>
#include <utility>
// 模拟一个会产生移动的函数
std::vector<int> createVector() {
std::vector<int> v{1, 2, 3};
return v; // C++17:强制拷贝省略(C++17起)
}
class MyClass {
public:
std::vector<int> data;
MyClass() {
std::cout << " MyClass constructed" << std::endl;
}
~MyClass() {
std::cout << " MyClass destroyed" << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
std::cout << " MyClass moved" << std::endl;
}
// 拷贝构造函数
MyClass(const MyClass& other) : data(other.data) {
std::cout << " MyClass copied" << std::endl;
}
};
MyClass createMyClass() {
MyClass m;
return m; // C++17保证不会调用拷贝/移动
}
int main() {
// C++23进一步简化隐式移动规则
std::cout << "Creating vector:" << std::endl;
auto v = createVector();
std::cout << "v.size() = " << v.size() << std::endl; // 输出: 3
std::cout << "\nCreating MyClass:" << std::endl;
auto m = createMyClass(); // C++17保证零拷贝/移动
return 0;
}
|
C++17 vs C++20 vs C++23 隐式移动规则
| 场景 | C++17 | C++20 | C++23 |
|---|
| 返回局部变量 | RVO或移动 | RVO或移动 | 更宽松 |
| 返回参数 | 移动 | 移动 | 更宽松 |
| 结构化绑定返回 | - | N/A | 更宽松 |
幽默解读
C++98:返回局部对象?拷贝!一个字都不会少!
C++11:等等,我给他加个移动语义,能省就省!
C++17:都别吵了,我直接强制省略拷贝,谁都不许拷贝!
C++23:我再放宽一点规则,让编译器更自由!
编译器(累了):你们能不能统一一下规则?!
程序员:别抱怨了,你不是越来越聪明了吗?
编译器:...
实际影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| #include <iostream>
#include <memory>
struct BigObject {
int data[1000];
BigObject() { std::cout << " BigObject constructed" << std::endl; }
~BigObject() { std::cout << " BigObject destroyed" << std::endl; }
};
// C++23更宽松的隐式移动规则影响
BigObject createObject() {
BigObject obj;
// ... 可能有一些操作 ...
return obj; // C++23保证不会有多余的拷贝
}
int main() {
std::cout << "Creating BigObject:" << std::endl;
auto obj = createObject();
std::cout << "Object created successfully" << std::endl;
return 0;
}
|
💡 小提示:虽然C++23放宽了规则,但最好的实践仍然是:避免对返回局部对象使用std::move!编译器比你更聪明,它会自动处理。
27.13 范围for初始化器:让循环更优雅
C++23之前的范围for
1
2
3
4
| std::vector<int> v{1, 2, 3};
for (int x : v) {
std::cout << x << " ";
}
|
但如果你想在循环前初始化这个vector,你得在循环外面先声明。
C++23的初始化器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <iostream>
#include <vector>
int main() {
// C++23: 范围for可以有初始化器
// 初始化器和循环变量可以在同一个for语句中声明
for (auto vec = std::vector{1, 2, 3}; auto& v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
|
编译运行:
g++ -std=c++23 -o rangefor rangefor.cpp
./rangefor
// 输出: 1 2 3
幽默解读
C++98:for循环要先在外面准备好数据,很麻烦!
C++11:没关系,我给你range-based for,优雅!
C++23:我再加一个功能——初始化器!数据准备和循环可以在一起!
程序员:这是要把所有东西都塞进一个for语句的节奏吗?
C++23:没错!让代码更紧凑!(虽然可能太紧凑了)
实际应用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include <iostream>
#include <vector>
#include <map>
int main() {
// 场景1:过滤并处理
std::cout << "Even numbers:" << std::endl;
for (auto vec = std::vector{1, 2, 3, 4, 5, 6}; const auto& v : vec) {
if (v % 2 == 0) {
std::cout << v << " "; // 输出: 2 4 6
}
}
std::cout << std::endl;
// 场景2:与if一起使用
for (auto m = std::map<std::string, int>{{"a", 1}, {"b", 2}};
const auto& [key, value] : m) {
std::cout << key << "=" << value << " "; // 输出: a=1 b=2
}
std::cout << std::endl;
// 场景3:临时对象的生命周期
for (auto p = std::pair{1, 2}; const auto& elem :
{p.first, p.second, p.first + p.second}) {
std::cout << elem << " "; // 输出: 1 2 3
}
std::cout << std::endl;
return 0;
}
|
初始化器的语法细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <iostream>
#include <vector>
int main() {
// 语法:for (初始化器; 循环变量声明; 循环体)
// 初始化器可以是任何表达式
for (auto counter = 0; const auto& x : {1, 2, 3, 4, 5}) {
std::cout << counter++ << ":" << x << " ";
}
std::cout << std::endl;
// 输出: 0:1 1:2 2:3 3:4 4:5
// 初始化器中声明的变量作用域是整个for语句
for (auto result = 0; const auto& x : {10, 20, 30}) {
result += x;
}
// result在这里仍然可见
// std::cout << result << std::endl; // 注释:这里输出60
return 0;
}
|
💡 小提示:初始化器的变量在循环结束后仍然存在!如果你不需要在循环后访问它,可以使用块作用域:
1
2
3
4
5
| {
for (auto vec = std::vector{1, 2, 3}; auto& v : vec) {
// ...
}
} // vec在这里被销毁
|
27.14 CTAD与继承构造函数:类型推导的强强联合
什么是CTAD?
CTAD(Class Template Argument Deduction,类模板参数推导)是从C++17引入的特性。它允许你在创建对象时省略模板参数,让编译器自动推导:
1
| std::vector v{1, 2, 3}; // 编译器推导出std::vector<int>
|
什么是继承构造函数?
继承构造函数是C++11引入的特性。使用using Base::Base;可以让派生类"继承"基类的构造函数。
C++23的改进
C++23让继承构造函数也能享受CTAD的便利!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #include <iostream>
struct Base {
int x, y;
Base(int x, int y) : x(x), y(y) {
std::cout << "Base constructed with (" << x << ", " << y << ")" << std::endl;
}
};
struct Derived : Base {
using Base::Base; // 继承构造函数
};
int main() {
// C++23: 继承构造函数的CTAD
// 以前需要这样写:
Derived d1(1, 2); // 显式指定参数类型
// C++23可以通过CTAD直接推导
Derived d2 = Derived(1, 2); // 自动推导
std::cout << "d2.x = " << d2.x << ", d2.y = " << d2.y << std::endl; // 输出: 1, 2
return 0;
}
|
幽默解读
CTAD(自信):我可以让编译器自动推导出模板参数!
继承构造函数(得意):我可以继承基类的构造函数!
CTAD:那我们合作吧!
继承构造函数:好主意!
C++17:你们俩不能一起玩...
C++23:我批准了!你们可以合作了!
CTAD + 继承构造函数(欢呼):太棒了!
更复杂的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| #include <iostream>
#include <memory>
#include <vector>
template<typename T>
struct Wrapper {
T value;
template<typename U>
Wrapper(U&& v) : value(std::forward<U>(v)) {}
};
struct Base1 {
int a;
Base1(int a) : a(a) {}
};
struct Base2 {
double b;
Base2(double b) : b(b) {}
};
// 多继承情况
struct Derived : Base1, Base2 {
using Base1::Base1;
using Base2::Base2;
};
int main() {
// C++23: 继承构造函数CTAD
// 单继承
Derived d1(42); // 继承自Base1的构造函数
std::cout << "d1.a = " << d1.a << std::endl; // 输出: 42
// Wrapper和继承构造函数结合
struct IntWrapper : Wrapper<int> {
using Wrapper<int>::Wrapper;
};
IntWrapper w(100);
std::cout << "w.value = " << w.value << std::endl; // 输出: 100
return 0;
}
|
注意事项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #include <iostream>
struct Base {
int x;
Base(int x) : x(x) {}
};
struct Derived : Base {
using Base::Base;
// 如果派生类定义了同名成员,可能会有问题
// int x; // 如果取消注释,会有歧义
};
int main() {
// C++23中,继承构造函数的CTAD
Derived d(10);
std::cout << "d.x = " << d.x << std::endl; // 输出: 10
return 0;
}
|
💡 小提示:继承构造函数的CTAD在大多数情况下都能正常工作,但如果你遇到歧义或推导失败的问题,可能需要显式指定模板参数。
27.15 复合语句末尾标签:goto的新玩法
什么是复合语句?
复合语句(Compound Statement)是用{}包围的语句块。在C++中,标签(Label)通常和goto一起使用,但C++23之前只能在语句前放标签。
C++23的改变
C++23允许在复合语句的末尾放置标签!这是一个看似奇怪但意外实用的改进。
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <iostream>
int main() {
// C++23: 复合语句末尾可以有标签
// 这在配合goto使用时特别有用
{
int x = 42;
int y = 100;
// 在块末尾放标签
if (x < 0) {
goto negative;
}
// ... 更多代码 ...
negative:; // 标签在块的末尾!C++23允许这样写
}
std::cout << "Labels at end of compound statements in C++23" << std::endl;
return 0;
}
|
幽默解读
标签(委屈):为什么我只能在语句前面?不能在后面吗?
程序员:语句后面的叫"注释",不是"标签"!
标签:可是有时候我需要在块的最后跳转啊!
C++23:好吧好吧,我允许你在块的最后放标签了!
标签:太好了!终于自由了!
goto(路过):听说标签解放了?
标签:是的!现在你可以跳到块的任何地方了!
goto:我好久没被使用了...大家都不喜欢我...
C++23:别担心,这个新特性可能让你重新就业!
实际应用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| #include <iostream>
#include <vector>
int processData(const std::vector<int>& data) {
int result = 0;
// 使用末尾标签进行状态跳转
{
// 模拟一些处理逻辑
bool error = false;
int errorCode = 0;
if (data.empty()) {
error = true;
errorCode = 1;
goto handle_error; // 跳到块的末尾
}
// 正常处理
for (int v : data) {
result += v;
}
// 成功退出
goto cleanup;
handle_error:; // C++23:标签可以在块末尾
std::cout << "Error code: " << errorCode << std::endl;
result = -1;
cleanup:; // C++23:标签可以在块末尾
std::cout << "Cleanup performed" << std::endl;
}
return result;
}
int main() {
std::vector<int> data{1, 2, 3, 4, 5};
std::cout << "Result: " << processData(data) << std::endl; // 输出: 15
std::cout << "Empty case: " << processData({}) << std::endl; // 输出: -1
return 0;
}
|
另一个例子:状态机
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| #include <iostream>
enum class State { IDLE, RUNNING, PAUSED, STOPPED };
State processState() {
State current = State::IDLE;
// 使用末尾标签模拟状态转换
{
auto transition = [&](State newState) {
current = newState;
};
// 模拟状态转换逻辑
transition(State::RUNNING);
if (current == State::RUNNING) {
goto state_running;
}
state_running:; // C++23
std::cout << "Running state" << std::endl;
transition(State::IDLE);
state_done:; // C++23
std::cout << "State machine completed" << std::endl;
}
return current;
}
int main() {
State finalState = processState();
std::cout << "Final state: " << static_cast<int>(finalState) << std::endl;
return 0;
}
|
💡 小提示:虽然C++23允许在复合语句末尾放置标签,但goto本身仍然被认为是有害的(有害健康.jpg)。在现代C++中,我们更推荐使用异常、RAII或状态模式来替代goto。只有在极少数需要高性能跳转到错误处理或资源清理的场景下,才考虑使用goto。
27.16 初始化语句中的using声明:让代码更紧凑
什么是using声明?
using声明(Using Declaration)允许你将命名空间中的名字引入当前作用域:
1
2
| using std::cout; // 不用写std::cout,直接用cout
using std::endl;
|
C++23的创新
C++23允许在if、switch、for、while等语句的初始化部分使用using声明!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <iostream>
int main() {
// C++23: 初始化语句中可以using声明
// 在if语句的初始化部分使用using
if (using T = int; true) { // 声明T为int的别名
T x = 42; // T就是int
std::cout << "x = " << x << std::endl; // 输出: 42
}
// 更实际的例子
if (using namespace std; string s = "Hello"; !s.empty()) {
cout << "String: " << s << endl; // 不用写std::
}
return 0;
}
|
幽默解读
C++23:我可以在if的初始化部分放using声明了!
程序员:真的吗?这太疯狂了!
C++23:没错!以前你得先写一行using,然后才能用:
using std::cout;
cout << 1;
现在一行搞定:
if (using std::cout; condition) {
cout << 1;
}
程序员:这...这也太紧凑了吧!
C++23:紧凑是C++的浪漫!(虽然可能太浪漫了)
实用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| #include <iostream>
#include <map>
#include <string>
int main() {
// 场景1:简化迭代器处理
std::map<std::string, int> ages{{"Alice", 30}, {"Bob", 25}};
if (auto it = ages.find("Alice"); it != ages.end()) {
std::cout << "Found: " << it->first << " = " << it->second << std::endl;
}
// 场景2:简化string操作
if (std::string s = " trimmed "; !s.empty()) {
auto trimmed = s.substr(s.find_first_not_of(' '));
std::cout << "Trimmed: '" << trimmed << "'" << std::endl;
}
// 场景3:在for循环中使用
for (using std::vector; vector<int> v{1, 2, 3}; const auto& x : v) {
std::cout << x << " "; // 不用写std::
}
std::cout << std::endl;
return 0;
}
|
与switch结合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <iostream>
#include <optional>
int main() {
std::optional<int> opt = 42;
switch (auto val = opt.value_or(0); val) {
case 0:
std::cout << "Value is 0 or optional was empty" << std::endl;
break;
default:
std::cout << "Value: " << val << std::endl;
break;
}
return 0;
}
|
嵌套使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <iostream>
int main() {
// 嵌套的初始化语句
if (int a = 1; a > 0) {
if (int b = 2; b > 0) {
if (using auto c = a + b; c > 0) {
std::cout << "a + b = " << c << std::endl; // 输出: 3
}
}
}
return 0;
}
|
💡 小提示:虽然这个特性让代码更紧凑,但也要注意可读性。不要为了"酷"而过度使用。黄金原则:如果一行能说清楚,就不要用三行;如果三行能说清楚,就不要用一行。
27.17 Lambda可选括号:[]的逆袭
以前的Lambda语法
在C++11到C++20中,Lambda必须这样写:
1
| auto lambda = []() { return 42; }; // 必须写()
|
即使没有参数,也要写空括号()。这让很多Python用户羡慕不已(Python:lambda: 42)。
C++23的改进
C++23允许省略空参数列表的括号!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <iostream>
int main() {
// C++23: 空参数列表的Lambda可以省略()
// 以前的写法
auto oldStyle = []() { return 42; };
// C++23新写法
auto newStyle = [] { return 42; }; // 不需要()
// 两者功能完全一样
std::cout << "oldStyle() = " << oldStyle() << std::endl; // 输出: 42
std::cout << "newStyle() = " << newStyle() << std::endl; // 输出: 42
return 0;
}
|
幽默解读
C++11/14/17/20:
程序员:我想写一个不接受任何参数的Lambda!
Lambda:好的,请写[](){}
程序员:为什么要有()?
Lambda:这是规定!
程序员:我只是想返回一个42而已!
Lambda:规定就是规定!
C++23:
程序员:我想写一个不接受任何参数的Lambda!
Lambda:好的,直接写[]{}就行!
程序员:真的吗?
Lambda:真的!括号可以省略了!
程序员:太好了!这才是我想要的!
什么时候不能省略括号?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <iostream>
int main() {
// 1. 有参数时,括号不能省
auto withParams = [](int x) { return x * 2; };
std::cout << withParams(5) << std::endl; // 输出: 10
// 2. 有mutable时,括号不能省
auto mutableLambda = []() mutable { return 42; };
// auto bad = [] mutable { return 42; }; // 错误!
// 3. 有异常规范时,括号不能省
auto noexceptLambda = []() noexcept { return 42; };
// 4. 属性不能省(虽然C++23允许Lambda有属性)
auto attrLambda = [] [[nodiscard]] () { return 42; };
// 5. 有trailing return type时,括号不能省
auto trailingReturn = []() -> int { return 42; };
// 6. 泛型Lambda(auto参数)时,括号不能省
auto genericLambda = [](auto x) { return x; };
return 0;
}
|
实用例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 以前需要写[]
std::vector<int> v{3, 1, 4, 1, 5, 9, 2, 6};
// 排序
std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) {
return a < b;
});
// 打印
std::for_each(v.begin(), v.end(), [](auto x) {
std::cout << x << " ";
});
std::cout << std::endl;
// C++23:无参数的Lambda可以省略()
std::cout << "([]{ return 42; })() = " << ([]{ return 42; })() << std::endl;
// 输出: 42
// 即时调用Lambda(IIFE)
auto result = []{
int sum = 0;
for (int i = 1; i <= 100; ++i) sum += i;
return sum;
}(); // 定义完立刻调用
std::cout << "Sum 1-100 = " << result << std::endl; // 输出: 5050
return 0;
}
|
💡 小提示:虽然C++23允许省略空括号,但可读性很重要。如果你写的是一个简单的常量返回Lambda,省略括号很优雅;但如果Lambda逻辑复杂,加上括号可能更清晰。不要为了"酷"而牺牲代码的可读性!
27.18 static_assert和if constexpr的窄化转换:更严格的类型检查
什么是窄化转换?
窄化转换(Narrowing Conversion)是指从宽类型到窄类型的隐式转换,可能导致数据丢失:
1
| int x{3.14}; // 窄化!3.14被截断为3
|
在C++11引入的初始化列表(花括号初始化)中,窄化转换是被禁止的。
C++23的新规则
C++23扩展了窄化转换检查的范围,让static_assert和if constexpr也能检测窄化转换!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| #include <iostream>
#include <type_traits>
int main() {
// C++23: static_assert和if constexpr支持窄化转换检查
// 1. consteval + if consteval
consteval int square(int x) {
if consteval {
// 在常量上下文中,窄化转换会被检查
// int y{3.14}; // C++23:如果在这里会报错!
return x * x;
} else {
return x * x;
}
}
std::cout << "square(5) = " << square(5) << std::endl; // 输出: 25
// 2. 在static_assert中检查类型
static_assert(std::is_integral_v<int>, "int should be integral");
static_assert(!std::is_floating_point_v<int>, "int is not floating point");
// 3. if constexpr的窄化检查
if constexpr (std::is_same_v<int, int>) {
// 这里是编译期检查
}
std::cout << "Narrowing conversion checks in C++23" << std::endl;
return 0;
}
|
幽默解读
窄化转换(偷偷摸摸):嘿嘿,我是隐式转换,没人能发现我...
C++23(突然出现):站住!我看到你了!
窄化转换:什么?!你怎么可能...我是隐式的!
C++23:在consteval和if constexpr中,隐式的也是隐式的!我都能检测!
窄化转换:不要啊啊啊!
C++23:数据精度是神圣不可侵犯的!
实际应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include <iostream>
#include <concepts>
// C++23:consteval函数中的窄化检查
consteval int safeAdd(int a, int b) {
if consteval {
// 如果参数类型本身就是int,这里不会有窄化
// 但如果有人传了double类型的常量...
return a + b;
} else {
return a + b;
}
}
// 使用概念约束模板参数
template<std::integral T>
T doubleValue(T x) {
return x * 2;
}
int main() {
std::cout << "safeAdd(1, 2) = " << safeAdd(1, 2) << std::endl; // 输出: 3
std::cout << "doubleValue(5) = " << doubleValue(5) << std::endl; // 输出: 10
// C++23的窄化检查让consteval更安全
constexpr int result = safeAdd(10, 20); // 编译期计算
std::cout << "Compile-time result: " << result << std::endl; // 输出: 30
return 0;
}
|
对比C++20和C++23
| 场景 | C++20 | C++23 |
|---|
int x{3.14} | 编译错误 | 编译错误 |
if consteval { int x{3.14}; } | 可能不报错 | 编译错误 |
consteval { int x{3.14}; } | 可能不报错 | 编译错误 |
💡 小提示:C++23对窄化转换的检查更加严格了。如果你的代码在C++23下编译失败,可能是因为存在窄化转换。修复方法是使用显式转换或使用不同的初始化方式。
27.19 命名通用字符转义(\N{…}):Unicode爱好者的福音
什么是命名通用字符转义?
在C++23之前,如果你想表示Unicode字符,你需要知道它的码点:
1
| char c = '\x41'; // 'A'的十六进制码点
|
C++23引入了命名通用字符转义(Named Universal Character Escape),让你可以用名字来引用Unicode字符!
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <iostream>
int main() {
// C++23: 命名通用字符转义
// 使用\N{...}语法引用Unicode字符名称
// 以前的方式(需要知道码点)
char letterA_old = '\x41'; // 'A'
// C++23新方式(用名字)
// 注意:实际编译需要源文件编码支持
char letterA_new = '\N{LATIN SMALL LETTER A}'; // 'A'
std::cout << "Named universal character escapes in C++23" << std::endl;
// 更多例子
// char euro = '\N{EURO SIGN}'; // €
// char yen = '\N{YEN SIGN}'; // ¥
// char copyright = '\N{COPYRIGHT SIGN}'; // ©
return 0;
}
|
幽默解读
Unicode字符:我是谁?我在哪?我的码点是多少?
程序员:你...你的码点是U+0041...
Unicode字符:什么?太难记了!
C++23:没关系,我给你起个名字!
Unicode字符:真的吗?
C++23:LATIN SMALL LETTER A!这就是你的名字!
Unicode字符:太好了!我终于有名字了!
支持的字符名称
C++23支持ISO/IEC 10646标准中定义的所有字符名称,包括:
- 拉丁字母:
LATIN SMALL LETTER A, LATIN CAPITAL LETTER B - 希腊字母:
GREEK SMALL LETTER ALPHA, GREEK CAPITAL LETTER OMEGA - 数字符号:
DIGIT ONE, NUMBER SIGN - 货币符号:
EURO SIGN, YEN SIGN, POUND SIGN - 标点符号:
COMMA, PERIOD, QUESTION MARK - 箭头:
RIGHTWARDS ARROW, LEFTWARDS ARROW
实际应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| #include <iostream>
#include <string>
int main() {
// 使用命名字符构建字符串
std::string greeting = "Hello, \N{WORLD MAP}"; // 🌍
// 使用命名字符进行字符串字面量操作
char heart = '\N{HEART SUIT}'; // ♥
std::cout << "Heart: " << heart << std::endl;
// 在模板中使用
constexpr char copyright = '\N{COPYRIGHT SIGN}'; // ©
std::cout << "Copyright: " << copyright << std::endl;
return 0;
}
|
💡 小提示:虽然命名通用字符转义很方便,但不是所有编译器都完全支持。在使用前请确认你的编译器支持C++23的这个特性。另外,有些字符名称可能因ISO标准更新而变化。
27.20 定界转义序列:\x{…}的魔法
什么是定界转义序列?
定界转义序列(Delimited Escape Sequence)是C++23引入的新语法,使用\x{...}来表示任意Unicode码点。
\x后面跟{...},大括号内是十六进制的码点值。
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include <iostream>
#include <string>
int main() {
// C++23: 定界转义序列
// \x{...} 用于指定任意Unicode码点
// 简单的十六进制
char c1 = '\x41'; // 'A' (传统语法)
char c2 = '\x{41}'; // 'A' (C++23新语法)
// Unicode码点
char euro = '\x{20AC}'; // € (Euro sign, U+20AC)
char yen = '\x{00A5}'; // ¥ (Yen sign, U+00A5)
char heart = '\x{2665}'; // ♥ (Heart, U+2665)
std::cout << "Characters: " << c2 << " " << euro << " " << yen << " " << heart << std::endl;
// 字符串中使用
std::string msg = "Price: \x{20AC}100";
std::cout << msg << std::endl;
// 可以指定任意长度
char snowman = '\x{2603}'; // ☃ (U+2603)
std::cout << "Snowman: " << snowman << std::endl;
std::cout << "Delimited escape sequences in C++23" << std::endl;
return 0;
}
|
幽默解读
传统转义序列(傲慢):我只能表示0-255的码点!
程序员:那我想要更大的码点呢?
传统转义序列:那就用\u或者\U!
程序员:好麻烦...
C++23:没关系!我给你\x{}!任意码点,一个语法搞定!
传统转义序列:...
C++23:简单粗暴就是我!
对比各种转义序列
| 语法 | 范围 | 示例 |
|---|
\xhh | 1-2位十六进制 | \x41 = ‘A’ |
\x{hh...} | 任意位十六进制(C++23) | \x{20AC} = ‘€’ |
\uhhhh | 4位十六进制Unicode | \u0041 = ‘A’ |
\Uhhhhhhhh | 8位十六进制Unicode | \U00000041 = ‘A’ |
实际应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| #include <iostream>
#include <string>
#include <vector>
int main() {
// 使用定界转义序列构建emoji字符串
std::string emojis =
"\x{1F600}" // 😀 GRINNING FACE
"\x{1F601}" // 😁 BEAMING FACE
"\x{1F602}" // 😂 FACE WITH TEARS OF JOY
"\x{1F603}" // 😃 GRINNING FACE WITH BIG EYES
"\x{1F604}"; // 😄 GRINNING FACE WITH SMILING EYES
std::cout << "Emojis: " << emojis << std::endl;
// 生成UTF-8编码的字符串
std::string chinese =
"\x{E4}\x{B8}\x{AD}" // 中 (U+4E2D)
"\x{E6}\x{96}\x{87}"; // 文 (U+6587)
std::cout << "Chinese: " << chinese << std::endl;
// 可变长度编码
char c1 = '\x{7F}'; // 1 byte: 0x7F
char c2 = '\x{7FF}'; // 2 bytes: 0x7FF
char c3 = '\x{FFFF}'; // 3 bytes: 0xFFFF
std::cout << "Delimited escape sequences in C++23" << std::endl;
return 0;
}
|
💡 小提示:\x{...}的好处是你可以指定任意长度的十六进制数,不像\xhh那样受限于1-2位。这对于表示UTF-8或UTF-16编码的字符特别有用。
27.21 UTF-8源文件编码支持:告别编码噩梦
编码问题:一个程序员的噩梦
你有没有遇到过这种情况?
程序员:为什么我的中文字符串输出是乱码?!
编译器:你的源文件编码是什么?
程序员:啊...我不知道...
编译器:...
C++23对UTF-8源文件编码提供了更好的支持,让跨平台开发更加顺畅。
C++23的改进
- 更好的字符集检测——编译器能更好地识别UTF-8编码的源文件
- 标准化的编码约定——C++23标准明确了UTF-8作为源文件编码的首选
- 改进的字符串字面量——UTF-8字符串字面量的处理更加一致
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <iostream>
#include <string>
int main() {
// C++23: 更好的UTF-8源文件编码支持
// 假设源文件是UTF-8编码
// 字符串字面量中的Unicode字符
std::string greeting = "你好,C++23!"; // 如果文件是UTF-8,这会正确工作
std::cout << greeting << std::endl;
// 使用之前介绍的命名转义
std::string euro = "价格: \x{20AC}100";
std::cout << euro << std::endl;
// UTF-8字符串前缀
// u8"你好" 是UTF-8编码的字符串字面量
const char* utf8str = u8"Hello, \x{4E2D}\x{6587}!"; // UTF-8编码
std::cout << utf8str << std::endl;
return 0;
}
|
幽默解读
程序员(崩溃):为什么同样的代码,在Windows上编译是乱码,在Linux上编译是好的?!
C++23:别慌,我来统一UTF-8支持!
程序员:真的吗?
C++23:真的!现在编译器都默认支持UTF-8了!
程序员:那我要在代码里写中文怎么办?
C++23:直接写就行了!编译器会处理的!
程序员:太好了!我再也不用担心编码问题了!
(实际情况下你还是需要担心一点点,但比以前好多了)
不同平台的编码处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| #include <iostream>
#include <string>
#include <locale>
int main() {
// 设置本地化以正确显示Unicode字符
std::locale::global(std::locale(""));
// 包含各种Unicode字符的字符串
std::string unicode_text =
"English: Hello!\n"
"Chinese: 你好!\n"
"Japanese: こんにちは!\n"
"Korean: 안녕하세요!\n"
"Russian: Привет!\n"
"Arabic: مرحبا!\n"
"Emoji: \x{1F600}\x{1F601}\x{1F602}";
std::cout << "UTF-8 source file encoding in C++23" << std::endl;
std::cout << "========================" << std::endl;
std::cout << unicode_text << std::endl;
return 0;
}
|
C++23 vs 之前版本
| 特性 | C++20 | C++23 |
|---|
| 源文件编码 | 实现定义 | UTF-8首选 |
| char8_t | 支持 | 更完善的支持 |
| UTF-8字符串 | u8前缀 | 更一致的语义 |
| Unicode属性 | 部分支持 | 更完整 |
💡 小提示:虽然C++23改进了UTF-8支持,但跨平台开发时仍需注意:
- 确保你的编辑器/IDE使用UTF-8编码保存源文件
- 确保终端/控制台支持UTF-8输出
- 在Windows上,可能需要设置代码页为65001(UTF-8)
27.22 constexpr扩展:编译期计算的终极形态
constexpr的进化史
- C++98:没有constexpr
- C++11:
constexpr关键字诞生,只能用于非常简单的函数 - C++14:放宽了constexpr的限制
- C++17:可以在constexpr函数中使用
if - C++20:可以在constexpr函数中使用
concepts、虚函数、try-catch等 - C++23:constexpr中支持goto、union、位域、try-catch等新特性!
C++23的constexpr扩展
C++23大幅扩展了constexpr的能力:
- 允许
goto语句——再也不用担心编译报错 - 允许union、位域(bitfield)——更丰富的类型支持
- 允许
static和thread_local变量声明——语法层面更自由 - 允许
try-catch——错误处理也能在编译期 - 更宽松的返回类型和参数类型限制
27.22.1 非字面量变量和goto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <iostream>
// C++23: constexpr函数中可以使用某些非字面量类型和语句
constexpr int compute() {
int x = 10; // int是字面量类型
// C++23允许goto
goto skip;
x = 20;
skip:
return x * 2;
}
int main() {
// 编译期计算!
constexpr int val = compute(); // C++23可以这样用
std::cout << "val = " << val << std::endl; // 输出: 20
// 注意:goto在constexpr中主要用于控制流测试
// 实际编程中请谨慎使用goto
return 0;
}
|
幽默解读
constexpr函数(委屈):我被限制了这么多年...
C++委员会:好了好了,C++23给你自由!
constexpr函数:我可以...可以用意志...
C++23:你可以用意志...不对,你可以用goto了!
constexpr函数:真的吗?!
C++23:真的!
constexpr函数:太好了!那我可以用static变量吗?
C++23:也可以!
constexpr函数:完美!
27.22.2 static和thread_local变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| #include <iostream>
// C++23: constexpr函数中可以声明static和thread_local变量
// 注意:这些变量只能在运行期求值时使用,不能用于真正的编译期常量求值
constexpr int access() {
// static变量在运行期会保持状态,但运行期才有固定地址
static int counter = 0;
return ++counter;
}
// C++23: thread_local变量
constexpr int accessThreadLocal() {
thread_local int localCounter = 0;
return ++localCounter;
}
int main() {
// C++23 constexpr扩展
// 如果你真的需要编译期计算,避免使用static/thread_local
constexpr int compileTimeVal = []{
int sum = 0;
for (int i = 1; i <= 10; ++i) {
sum += i;
}
return sum;
}();
std::cout << "Static and thread_local in constexpr context (C++23)" << std::endl;
std::cout << "Compile-time sum 1-10: " << compileTimeVal << std::endl; // 输出: 55
std::cout << "access() = " << access() << std::endl; // 运行期调用,输出: 1
return 0;
}
|
27.22.3 返回类型和参数类型放宽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| #include <iostream>
#include <vector>
#include <string>
// C++23: constexpr函数的限制进一步放宽
// 注意:动态内存分配(new/delete)在constexpr中仍然受限
// 下面的示例展示了C++23允许的新语法特性,但实际编译期计算能力取决于具体实现
// 可以使用union、位域、try-catch等
// 但std::vector的完整 constexpr 支持需要编译器实现跟上标准
// 演示:使用constexpr lambda(这是真正广泛可用的)
constexpr auto square = [](int x) constexpr { return x * x; };
int main() {
// C++23: constexpr扩展让更多特性可以在constexpr上下文中使用
std::cout << "Relaxed constexpr requirements in C++23" << std::endl;
// constexpr lambda(广泛支持)
constexpr int result = square(5);
static_assert(result == 25);
std::cout << "square(5) = " << result << std::endl; // 输出: 25
// 编译期整数运算完全支持
constexpr int fib10 = []{
int a = 0, b = 1;
for (int i = 0; i < 10; ++i) {
int c = a + b;
a = b;
b = c;
}
return a;
}();
static_assert(fib10 == 55);
std::cout << "fib(10) = " << fib10 << std::endl; // 输出: 55
return 0;
}
|
C++23 constexpr允许的新特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| #include <iostream>
#include <array>
int main() {
// C++23 constexpr新增支持的特性(new/delete在C++20已支持)
// 1. try-catch(现在可以在constexpr中使用)
constexpr int tryCatch = []{
try {
throw 42;
} catch (int x) {
return x;
}
}();
static_assert(tryCatch == 42);
// 2. 位域(bitfield)初始化
struct BitField {
int a : 3;
int b : 5;
};
constexpr BitField bf{5, 17};
static_assert(bf.a == 5 && bf.b == 17);
// 3. goto语句(虽然能用,但 constexpr 中使用 goto 通常意味着设计问题)
constexpr int withGoto = []{
goto skip;
skip:
return 123;
}();
static_assert(withGoto == 123);
std::cout << "constexpr extension demo completed" << std::endl;
std::cout << "tryCatch=" << tryCatch << ", bf.a=" << bf.a << ", withGoto=" << withGoto << std::endl;
return 0;
}
|
⚠️ 注意:动态内存分配(new/delete/std::make_unique等)在C++20的constexpr中已被允许,C++23进一步改善了相关支持。不过实际编译期支持程度仍取决于编译器实现。
constexpr vs consteval
| 特性 | constexpr | consteval |
|---|
| 求值时机 | 编译期或运行期 | 必须编译期 |
| 灵活性 | 高 | 低(必须编译期) |
| C++版本 | C++11 | C++20 |
| 适用场景 | 通用 | 元编程 |
💡 小提示:C++23的constexpr扩展非常强大,但记住:constexpr不代表一定会在编译期执行。如果你想要强制编译期求值,使用consteval函数!
本章小结
C++23:一场静悄悄的革命
C++23虽然不如C++11那样颠覆性地引入移动语义和Lambda,也不像C++20那样带来概念(Concepts)和协程,但它在细节上的打磨让C++变得更加易用和强大。
核心要点回顾
if consteval:编译期的时间探测器,区分常量上下文和运行时上下文
显式对象参数(Deducing this):给this起个昵称,让成员函数语法更统一
多维下标运算符:让obj(i,j,k)语法标准化,矩阵运算更优雅
静态operator[]和operator():不用实例就能调用的运算符黑魔法
auto(x)和auto{x}:decay-copy的显式表达,泛型编程利器
[[assume]]假设属性:与魔鬼的契约,用好了性能飙升,用砸了程序升天
Lambda属性:给匿名函数戴帽子,让元数据标注成为可能
扩展浮点类型:统一命名float16_t、float32_t等,AI时代的标配
新预处理器指令:elifdef、elifndef、#warning,预处理器也现代化
size_t后缀z/Z:终于等到你,不用强制转换了!
字符串trim()系列:首尾空白一键删除
隐式移动简化:编译器更聪明,程序员更轻松
范围for初始化器:数据准备和循环一体化
CTAD与继承构造函数:类型推导和继承的强强联合
复合语句末尾标签:goto的文艺复兴
初始化语句中的using声明:一行搞定namespace引入
Lambda可选括号:空参数列表可以写[]{}了
窄化转换检查加强:consteval中的类型检查更严格
命名通用字符转义:Unicode字符有名字了
定界转义序列\x{...}:任意Unicode码点,一个语法
UTF-8源文件编码:告别编码噩梦
constexpr扩展:goto、try-catch、位域、union都能用了!
学习建议
┌─────────────────────────────────────────────────────────────┐
│ C++23学习路线图 │
├─────────────────────────────────────────────────────────────┤
│ 入门级: │
│ ├── if consteval(理解编译期vs运行期) │
│ ├── Lambda可选括号(简化语法) │
│ ├── size_t后缀z(实用小技巧) │
│ └── 字符串trim()(日常编程必备) │
│ │
│ 进阶级: │
│ ├── 显式对象参数(面向对象新写法) │
│ ├── auto(x) decay-copy(模板元编程) │
│ ├── 范围for初始化器(代码组织) │
│ └── constexpr扩展(编译期计算) │
│ │
│ 骨灰级: │
│ ├── [[assume]]假设属性(性能优化,但危险) │
│ ├── 多维下标运算符(科学计算) │
│ └── 继承构造函数CTAD(库开发) │
└─────────────────────────────────────────────────────────────┘
下一步
- 📖 阅读C++23官方提案和cppreference文档
- 🛠️ 使用支持C++23的编译器(GCC 13+, Clang 16+, MSVC 19.35+)实验新特性
- 📝 尝试在新项目中应用这些特性
- 🎯 关注C++26的提案(已经在讨论中!)
幽默结语
C++委员会:各位程序员,C++23来了!
程序员们(蜂拥而上):我要用if consteval!我要用auto(x)!我要用trim()!
C++23:别急别急,人人都有份!
老程序员(擦泪):终于...终于不用写static_cast<size_t>了...
新程序员(激动):Lambda括号可以省略了!
C++委员会:这就是C++的魅力——我们在保持兼容性的同时不断进化!
恭喜你完成了C++23语言特性的学习! 🎉
记住:最好的代码不是用最新特性的代码,而是用最合适特性的代码。C++23给了你更多的工具,如何使用,由你决定!