第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. 构建更智能的元编程库——让模板代码更优雅

代码示例

 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++20C++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关键字是固定的,后面跟着的是对象的类型和参数名。

为什么需要这个特性?

  1. 代码更清晰——明确知道函数要操作哪个对象
  2. 支持更多操作符重载——可以让函数对象也享受成员函数的语法
  3. 统一语法——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;
}

适用场景

  1. 实现链式调用——返回*this以支持fluent API
  2. 泛型成员函数——this auto&让你写出适用于整个类层次结构的代码
  3. 替代完美转发——让函数对象可以被"调用"得像函数一样

💡 小提示:显式对象参数是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. 工具类——提供便捷的静态方法

代码示例

 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++20C++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引入的一个强大但危险的特性。它允许程序员向编译器声明:“我假设这个条件永远为真,你可以基于这个假设做优化。”

这就像和魔鬼签订契约——你给出承诺,编译器给予力量,但如果你的承诺是假的…后果自负!

使用场景

  1. 性能优化——告诉编译器某些不变量
  2. 分支消除——帮助编译器去掉不可能的分支
  3. 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)

为什么需要这些类型?

  1. 机器学习——低精度浮点运算更快更省内存
  2. 嵌入式系统——资源受限,需要节省内存
  3. 科学计算——某些场景需要精确控制精度
  4. 跨平台一致性——以前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一个专属后缀:zZ

代码示例

 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爱好者!

对比各种整数后缀

类型后缀示例说明
int100默认
longL100L
long longLL100LL
unsignedU100U
unsigned longUL100UL
size_tz/Z100zC++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为字符串处理带来了几个小而美的改进:

  1. std::string 视图与操作 —— 提供更方便的字符串操作视图
  2. 行拼接说明明确化 —— 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引入了移动语义:

1
return v;  // 移动而不是拷贝

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++17C++20C++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允许在ifswitchforwhile等语句的初始化部分使用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_assertif 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++20C++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:简单粗暴就是我!

对比各种转义序列

语法范围示例
\xhh1-2位十六进制\x41 = ‘A’
\x{hh...}任意位十六进制(C++23)\x{20AC} = ‘€’
\uhhhh4位十六进制Unicode\u0041 = ‘A’
\Uhhhhhhhh8位十六进制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的改进

  1. 更好的字符集检测——编译器能更好地识别UTF-8编码的源文件
  2. 标准化的编码约定——C++23标准明确了UTF-8作为源文件编码的首选
  3. 改进的字符串字面量——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++20C++23
源文件编码实现定义UTF-8首选
char8_t支持更完善的支持
UTF-8字符串u8前缀更一致的语义
Unicode属性部分支持更完整

💡 小提示:虽然C++23改进了UTF-8支持,但跨平台开发时仍需注意

  1. 确保你的编辑器/IDE使用UTF-8编码保存源文件
  2. 确保终端/控制台支持UTF-8输出
  3. 在Windows上,可能需要设置代码页为65001(UTF-8)

27.22 constexpr扩展:编译期计算的终极形态

constexpr的进化史

  • C++98:没有constexpr
  • C++11constexpr关键字诞生,只能用于非常简单的函数
  • 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的能力:

  1. 允许goto语句——再也不用担心编译报错
  2. 允许union、位域(bitfield)——更丰富的类型支持
  3. 允许staticthread_local变量声明——语法层面更自由
  4. 允许try-catch——错误处理也能在编译期
  5. 更宽松的返回类型和参数类型限制

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

特性constexprconsteval
求值时机编译期或运行期必须编译期
灵活性低(必须编译期)
C++版本C++11C++20
适用场景通用元编程

💡 小提示:C++23的constexpr扩展非常强大,但记住:constexpr不代表一定会在编译期执行。如果你想要强制编译期求值,使用consteval函数!


本章小结

C++23:一场静悄悄的革命

C++23虽然不如C++11那样颠覆性地引入移动语义和Lambda,也不像C++20那样带来概念(Concepts)和协程,但它在细节上的打磨让C++变得更加易用和强大。

核心要点回顾

  1. if consteval:编译期的时间探测器,区分常量上下文和运行时上下文

  2. 显式对象参数(Deducing this):给this起个昵称,让成员函数语法更统一

  3. 多维下标运算符:让obj(i,j,k)语法标准化,矩阵运算更优雅

  4. 静态operator[]和operator():不用实例就能调用的运算符黑魔法

  5. auto(x)auto{x}:decay-copy的显式表达,泛型编程利器

  6. [[assume]]假设属性:与魔鬼的契约,用好了性能飙升,用砸了程序升天

  7. Lambda属性:给匿名函数戴帽子,让元数据标注成为可能

  8. 扩展浮点类型:统一命名float16_tfloat32_t等,AI时代的标配

  9. 新预处理器指令elifdefelifndef#warning,预处理器也现代化

  10. size_t后缀z/Z:终于等到你,不用强制转换了!

  11. 字符串trim()系列:首尾空白一键删除

  12. 隐式移动简化:编译器更聪明,程序员更轻松

  13. 范围for初始化器:数据准备和循环一体化

  14. CTAD与继承构造函数:类型推导和继承的强强联合

  15. 复合语句末尾标签:goto的文艺复兴

  16. 初始化语句中的using声明:一行搞定namespace引入

  17. Lambda可选括号:空参数列表可以写[]{}

  18. 窄化转换检查加强:consteval中的类型检查更严格

  19. 命名通用字符转义:Unicode字符有名字了

  20. 定界转义序列\x{...}:任意Unicode码点,一个语法

  21. UTF-8源文件编码:告别编码噩梦

  22. 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给了你更多的工具,如何使用,由你决定!

最后修改 March 30, 2026: 更新 C++ 教程 (da65b52)