C++26 新特性

作为新一代 C++ 标准,C++26 解决了一批开发者期待多年的语言级需求,包括函数契约检查、编译期静态反射以及参数包下标读取。

由于这一版标准非常前沿,主流编译器仍在陆续推进支持,本地尝鲜通常需要安装 GCC 15 或 Clang 19 及以上版本并加上 -std=c++26(或实验性 -std=c++2c)参数。现阶段小伙伴们不需要记忆具体的语法细节,主要看看它解决了哪些实际痛点即可。

C++26 新特性概览

C++26 在语言层面新增了契约检查、静态反射、参数包下标索引与单下划线占位符,并在标准库中补充了栈上动态数组与科学计算模块。常用的代表性特性如下表所示。

C++26 常用特性
代表特性 所属类别 / 头文件 主要变化与作用
契约编程(Contracts) 语言特性 通过 pre、post 和 contract_assert 声明函数前置条件、后置条件与内部断言
静态反射(Static Reflection) 语言特性 / <meta> 引入 ^^ 反射运算符与 std::meta,支持在编译期直接读取类型的结构信息
参数包索引(Pack Indexing) 语言特性 支持写成 pack...[i] 按下标直接取出模板参数包中的第 i 个元素
下划线占位符(_) 语言特性 支持在同一作用域内多次使用单下划线 _ 来忽略不需要的值
std::inplace_vector <inplace_vector> 最大容量在编译期固定、元素直接存放在栈空间内的动态数组容器
SIMD 与线性代数库 <simd> / <linalg> 在标准库中内置数据并行向量运算与基础线性代数计算接口

契约编程(Contracts)

在编写函数时,如果我们要检查调用方传入的参数是否合法(如除数不能为 0),以前只能在函数体内部手写 if 判断,或者借助 <cassert> 头文件中的 assert 断言。

在 C++26 中,语言原生支持了契约编程语法。我们可以直接把输入和输出的检查条件写在函数声明上,就像在函数的入口和出口各安排了一名检查员。

语法:

返回类型 函数名(参数列表)
    pre (前置条件)
    post (返回值名: 后置条件) {
    contract_assert(内部断言条件);
    // 函数体
}

说明:

  • pre(前置条件):写在函数参数列表之后,规定调用该函数前实参必须满足的条件。
  • post(后置条件):跟在函数声明末尾,可为返回值起一个临时名字,规定函数返回时结果必须满足的条件。
  • contract_assert(契约断言):写在函数体内部,用于检查代码执行中途必须成立的条件。

示例 1:声明函数契约条件

#include <iostream>

// 声明前置条件与后置条件
int divideExact(int a, int b)
    pre (b != 0)
    post (r: r * b == a) {
    contract_assert(a % b == 0);    // 断言 a 能被 b 整除
    return a / b;
}

int main() {
    int res = divideExact(20, 4);
    std::cout << "计算结果:" << res << '\n';
    return 0;
}

运行结果如下。

计算结果:5

分析:

在这个例子中,函数声明处的 pre (b != 0) 规定了除数 b 不能为 0。紧随其后的 post (r: r * b == a) 中,冒号左边的 r 是给返回值临时起的名字,冒号右边的 r * b == a 则是返回值必须满足的条件。如果函数本身是 void 无返回值类型,直接写 post (条件) 即可。

一旦运行时发现 pre、post 或函数体内的 contract_assert 条件不成立,程序默认会打印违约位置并终止运行,防止错误数据继续往下传递。此外,pre 和 post 只在函数声明尾部充当上下文关键字,平时代码里已有的同名普通变量不会受到影响。

编译期静态反射

在以前的 C++ 中,源代码一旦编译成机器指令,结构体原本叫什么名字、里面有几个成员变量这些结构信息基本就被抹掉了。如果我们想把结构体的名称或全部成员自动转成文本输出,往往得手写大量重复代码。

C++26 正式引入了编译期静态反射机制,新增了前缀反射运算符(^^)与 <meta> 头文件。写成 ^^类型名 就相当于在编译阶段提取出一张记录该类型结构信息的 “名片”,其类型为 std::meta::info。

拿到这张名片后,我们调用 std::meta::identifier_of() 就能在编译期读取出该类型的名称,返回结果是一个只读的 std::string_view。这个函数名很好记,前面的 identifier 就是英文 “标识符” 的意思。我们来看一个简单例子。

示例 2:反射获取结构体名称

#include <iostream>
#include <meta>
#include <string_view>

struct Student {
    int id;
    double score;
};

int main() {
    // 编译期提取反射信息
    constexpr std::meta::info info = ^^Student;
    constexpr std::string_view name = std::meta::identifier_of(info);

    std::cout << "结构体名称:" << name << '\n';
    return 0;
}

运行结果如下。

结构体名称:Student

分析:

^^Student 表示在编译阶段提取出 Student 结构体的反射信息,并保存在常量 info 中。随后调用 std::meta::identifier_of(info),就能直接拿到该结构体的名称 "Student"。

这里只是借具体的 Student 演示基本语法。在实际编写函数模板时,我们事先并不知道模板参数 T 具体是什么类型,此时写成 ^^T 就能在编译期自动读取任意类型的名称与成员信息,非常适合用来自动生成 JSON 转换和日志打印代码。

参数包索引与下划线占位符

在编写模板代码和解包多成员数据时,C++26 还提供了参数包索引与下划线占位符两项实用的语法改进。

1. 参数包索引(pack...[i])

当我们编写可变参数模板时,形参包 args... 里往往打包了多个参数。以前如果想单独取出其中某一个位置的参数,必须借助 std::tuple 或编写递归函数一层层拆解。

在 C++26 中,我们可以直接写成 args...[i],按编译期下标取出对应位置的参数。和普通数组一样,下标也是从 0 开始计数的,args...[0] 表示第 1 个参数,args...[1] 表示第 2 个参数。

示例 3:按下标读取参数包

#include <iostream>

template <typename... Args>
auto getFirst(Args... args) {
    return args...[0];    // 取出第 1 个实参
}

int main() {
    std::cout << "第 1 个参数:" << getFirst(100, 200, 300) << '\n';
    return 0;
}

运行结果如下。

第 1 个参数:100

分析:

return args...[0]; 这句代码表示直接按编译期下标 0 取出传入的第 1 个实参 100。相比以前绕弯转换成元组再读取,这种下标写法直观了许多。

2. 下划线占位符(_)

在使用结构化绑定解包数据时,有时我们只关心其中一个成员,并不需要另一个成员。在 C++26 之前,如果在同一个作用域里写两次 auto [x, _] = ...;,编译器会把 _ 当成普通变量名,报出 “变量重定义” 错误。

C++26 将单下划线(_)设为了专门的 “未命名占位符”,支持在同一作用域内多次使用 _ 来忽略不需要的值。请看下面例子。

示例 4:重复使用下划线占位符

#include <iostream>
#include <utility>

int main() {
    std::pair<int, int> p1 = {10, 20};
    std::pair<int, int> p2 = {30, 40};

    auto [x, _] = p1;    // 忽略第 2 个成员
    auto [_, y] = p2;    // 再次使用 _ 不报错

    std::cout << "x = " << x << ", y = " << y << '\n';
    return 0;
}

运行结果如下。

x = 10, y = 40

分析:

在这个例子中,auto [x, _] = p1; 和 auto [_, y] = p2; 在 main() 函数内部连续两次使用了单下划线 _。编译器会自动将它们识别为被丢弃的占位符,不会触发重定义报错。

需要注意的是,一旦在同一作用域里多次使用 _ 充当占位符,后续代码就不能再去读取 _ 的值了,它的唯一职责就是帮我们跳过用不上的返回值或成员。

std::inplace_vector 与数值计算库

在标准库方面,C++26 既补充了兼顾栈内存性能与动态增删体验的新容器,也扩充了科学计算与硬件并行工具。

1. 栈上动态数组(std::inplace_vector)

回顾前面学过的两个连续容器:std::array 把元素直接存放在对象自身的栈空间里,读写速度快但长度固定,不能调用 push_back()。而 std::vector 虽然能随时动态增减元素,却需要去堆内存上申请空间。

C++26 在 <inplace_vector> 头文件中新增的 std::inplace_vector<T, N> 把两者的优点结合在了一起:最大容量 N 在编译期固定,元素直接存放在栈空间里,同时支持在 0 到 N 的范围内调用 push_back() 和 pop_back() 动态增减。

示例 5:使用 inplace_vector

#include <inplace_vector>
#include <iostream>

int main() {
    // 最大容量固定为 4
    std::inplace_vector<int, 4> nums;

    nums.push_back(10);
    nums.push_back(20);
    nums.push_back(30);

    std::cout << "元素个数:" << nums.size() << '\n';
    std::cout << "最大容量:" << nums.capacity() << '\n';
    std::cout << "元素内容:";
    for (int n : nums) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
    return 0;
}

运行结果如下。

元素个数:3
最大容量:4
元素内容:10 20 30

分析:

在上面例子中,std::inplace_vector<int, 4> nums; 创建了一个最多可容纳 4 个整数的栈上容器。当我们调用 3 次 push_back() 后,nums.size() 变为 3,而 nums.capacity() 始终固定为 4。

注意 由于 std::inplace_vector 的底层空间在编译期就已经固定,它不支持自动扩容。当元素已满时继续调用 push_back() 会直接抛出 std::bad_alloc 异常。如果不希望抛出异常,小伙伴们可以改用 try_push_back(),容量已满时它会安全返回 nullptr。

2. 数据并行与线性代数库

面向科学计算、图形渲染、物理引擎与人工智能底层开发,C++26 还在标准库中内置了两个专业数值计算头文件:

  • <simd> 头文件:提供跨平台的 SIMD(单指令多数据流)向量化接口,支持通过一条 CPU 指令同时对一组数据进行加减乘除运算。
  • <linalg> 头文件:基于 C++23 的 std::mdspan 多维数组视图,提供标准的向量点积、矩阵乘法与线性方程组求解等线性代数算法。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号