跨越十年的C++演进:C++23新特性全解析
跨越十年的C++演进系列,分为5篇,本文为第5篇~
前4篇如下:
跨越十年的C++演进:C++11新特性全解析
跨越十年的C++演进:C++14新特性全解析
跨越十年的C++演进:C++17新特性全解析
跨越十年的C++演进:C++20新特性全解析
与 C++20 相比,C++23 的变化虽然不那么显著,但在语言的稳定性与可用性方面做出了多项重要改进。C++20 引入了模块、协程、概念等重量级特性,大幅拓展了语言的功能和表达能力;而 C++23 则更侧重于对这些特性的完善与补充,修复细节问题,并引入了一些实用的新工具和编程模式,进一步提升了代码的可读性、安全性和开发效率。
本文将深入解析 C++23 引入的关键新特性,并通过清晰的代码示例帮助开发者快速理解其用法与适用场景,从而更好地在实际项目中应用这些现代 C++ 技术。
一、新增语言特性
1.1、明确的对象参数
C++23 引入了“明确的对象参数”(Explicit object parameter),允许在非静态成员函数中显式声明调用对象的类型。这一特性简化了某些复杂的模板编程模式,尤其是 CRTP 的实现方式。
传统的 CRTP 模式依赖于将派生类类型作为模板参数传递给基类,并通过 static_cast 将 this 转换为派生类指针以访问其成员函数。这种方式虽然有效,但代码冗余且不够直观。
现在,借助 C++23 的明确对象参数机制,可以更清晰地表达对调用对象的引用,提升可读性和安全性。
示例代码:
struct Base {template <typename T>void func(T&& t) {static_cast<T*>(this)->impl(std::forward<T>(t));}
};
struct Derived : Base {void impl(int x) {// Implementation for int}
};
int main() {Derived d;d.func(42); // Calls Derived::impl(int)
}
示例解析:
这段代码展示了传统 CRTP 模式的典型应用方式。其核心机制如下:
定义基类 Base:
Base 类内部声明了一个泛型函数模板 func,接受一个转发引用类型的参数 T。函数体中通过 static_cast 将当前对象指针转换为模板类型 T*,然后调用其 impl 方法。这种设计使基类能够提供统一接口,而具体行为由派生类实现。
定义派生类 Derived:
Derived 继承自 Base,并实现了名为 impl 的成员函数,接受一个整数参数。该方法会在基类的 func 函数中被调用,作为实际的功能执行单元。
main 函数中的使用过程:
在主函数中创建了 Derived 实例 d,并调用其 func 成员函数传入整数 42。此时,func 中的 this 指针被转换为 Derived* 类型,并最终触发 Derived::impl 的执行。这样,基类通过统一接口完成了对派生类方法的调用。
上述示例说明了如何利用明确的对象参数来优化传统的 CRTP 编码方式。新特性使得我们可以在函数签名中清晰地表示出对调用对象的依赖关系,从而提升代码的可维护性和灵活性。C++23 的这一更新让 CRTP 等高级编程技巧更易于理解和使用,降低了模板代码的理解门槛,提升了开发效率。
1.2、if consteval
if consteval 是 C++23 中引入的一项语言特性,用于判断当前是否处于常量求值上下文中。它允许开发者在同一个函数中根据是否在编译期执行而选择不同的代码路径,从而增强编译期计算的能力与灵活性。
示例代码:
constexpr int compute(int x) {if consteval {return x * 2;} else {return x * 3;}
}
int main() {constexpr int result = compute(5); // Result is 10 at compile-time
}
示例解析:
这段代码演示了如何通过 if consteval 在编译时与运行时之间切换不同的实现逻辑。
定义 compute 函数:
该函数被声明为 constexpr,表示可以在编译期执行。函数内部使用了 if consteval 条件判断:如果当前是在常量求值环境中,则返回 x * 2;否则返回 x * 3。
main 函数中的调用:
在 main 函数中,我们以 constexpr 方式调用 compute(5)。由于这是一个编译时常量表达式,因此 if consteval 的条件成立,最终结果为 10。
通过 if consteval,开发者可以更精细地控制编译期与运行期的行为差异,使函数在不同场景下表现得更加灵活。这不仅提升了常量表达式的可编程性,也为编写高效的元编程代码提供了有力支持。尤其适用于需要大量编译期计算或类型推导的场景,如模板库开发、静态断言优化等。
1.3、多维下标运算符
C++23 引入了对多维下标的支持,使得开发者可以直接通过多个参数的形式来访问数组元素,从而提升代码的直观性和可读性。这一改进简化了对多维数据结构的操作,特别是在处理矩阵、图像、张量等结构时非常实用。
示例代码:
#include <iostream>
#include <array>
template <typename T, size_t Rows, size_t Cols>
struct Matrix {std::array<T, Rows * Cols> data;T& operator[](std::pair<size_t, size_t> idx) {return data[idx.first * Cols + idx.second];}const T& operator[](std::pair<size_t, size_t> idx) const {return data[idx.first * Cols + idx.second];}
};
int main() {Matrix<int, 3, 3> mat = { { 1, 2, 3, 4, 5, 6, 7, 8, 9 } };std::cout << mat[{1, 2}] << std::endl; // Output: 6
}
示例解析:
本示例展示了如何利用多维下标运算符来实现一个二维矩阵的访问接口。
定义 Matrix 结构体模板:
Matrix 是一个泛型结构体,使用 std::array 存储数据。其模板参数分别为元素类型 T、行数 Rows 和列数 Cols。成员变量 data 用于存储实际数据。
实现多维下标操作:
通过重载 operator[] 方法,并接受一个 std::pair<size_t, size_t> 类型作为索引参数,实现了类似 mat[{row, col}] 的二维访问方式。函数内部将二维索引转换为一维数组的偏移量进行访问。
提供了一个 const 版本的重载方法,以支持只读访问。
main 函数中的使用:
创建一个 3x3 的整型矩阵并初始化。随后通过 mat[{1, 2}] 访问第二行第三列的数据,输出结果为 6。
多维下标运算符让开发者能够以更自然的方式操作多维数据结构,显著提升了代码的清晰度和安全性。相比传统的嵌套数组或多个独立函数调用,这种统一的下标方式减少了出错的可能性,也更容易理解和维护。C++23 的这项更新为构建高效、易读的数值计算程序提供了良好的语言基础。
1.4、内建衰减复制支持
C++23 在语言层面增强了对“衰减复制”(Decay Copy)机制的支持,使开发者在模板编程和函数参数处理中能够获得更一致、更可预期的类型转换行为。所谓“衰减复制”,是指将一个复杂类型转换为更基本的等效类型的过程,例如数组退化为指针、引用被移除、cv 限定符被剥离等。
示例代码:
#include <type_traits>
template<typename T>
void process(T&& arg) {std::decay_t<T> val = std::forward<T>(arg);// val is now a decayed copy of arg
}
int main() {int x = 42;process(x); // int&process(42); // int&&process("Hello"); // const char[6]
}
该示例演示了如何通过 std::decay_t 来实现一个通用的模板函数,自动完成参数类型的简化处理。
定义模板函数 process:
这个函数模板采用完美转发的方式接收任意类型的参数。函数内部使用 std::decay_t<T> 对传入的参数类型进行“衰减”处理,并将其结果赋值给局部变量 val。这样可以确保无论传入的是左值、右值还是数组类型,最终都得到一个适合使用的副本。
main 函数中的调用场景:
- 当调用 process(x) 时,x 是一个左值 int&,经过 std::decay_t 处理后,val 的类型变为 int。
- 调用 process(42) 时,字面量 42 是一个右值 int&&,同样会被转换为 int 类型。
- 调用 process("Hello") 时,字符串字面量的类型是 const char[6],但经 std::decay_t 处理后,会变成 const char*,即指向字符数组首元素的指针
C++23 中增强的衰减复制机制,使得模板函数在处理不同类型输入时更加统一和稳定。借助 std::decay_t,开发者无需手动编写复杂的类型转换逻辑,就能安全地获取到适合作为副本使用的类型。这一特性在泛型编程、函数封装以及库开发中尤为实用,有助于提升代码的健壮性和可维护性。
1.5、标记不可达代码(std::unreachable)
C++23 引入了 std::unreachable 工具函数,用于显式标记程序中不可能被执行到的代码路径。这一特性不仅有助于提高代码的可读性,也为编译器提供了更明确的优化线索,使其能够执行更激进的优化操作,并减少不必要的警告或误报。
示例代码:
#include <utility>
[[noreturn]] void error() {throw "error";
}
int main() {int x = 0;if (x == 0) {error();}std::unreachable(); // Compiler knows this point is never reached
}
示例解析:
该示例演示了如何利用 std::unreachable 明确标识程序中不会被执行到的代码部分。
定义 error 函数:
函数 error 被标记为 [[noreturn]],表示它在调用后不会正常返回。函数体通过抛出异常中断当前执行流程。
main 函数中的判断逻辑:
在 main 函数中声明并初始化整型变量 x 为 0。随后使用 if 判断其值是否为零,若为零则调用 error() 函数终止执行流。
添加 std::unreachable 声明:
在 if 分支之后调用 std::unreachable(),向编译器表明此行代码永远不会被执行。这有助于消除某些编译器关于“可能未覆盖所有分支”的警告。
std::unreachable 的引入使得开发者可以更清晰地表达控制流意图,同时也为编译器提供更强的优化依据。例如,在断言失败处理、错误分支结束等场景中非常实用。这一工具增强了代码的健壮性和可维护性,是现代 C++ 编程中值得推广的一项新特性。
1.6、平台无关的假设([[assume]])
C++23 引入了属性 [[assume]],允许开发者在代码中声明某个条件始终成立,从而协助编译器进行更深层次的优化。这一机制不同于运行时断言,而是纯粹服务于编译期优化目的,适用于对性能要求较高的关键路径。
示例代码:
#include <cassert>
int divide(int a, int b) {[[assume(b != 0)]];return a / b;
}
int main() {int result = divide(10, 2); // Compiler assumes b is not 0assert(result == 5);
}
示例解析:
本示例展示了如何使用 [[assume]] 来辅助编译器优化除法运算中的边界检查。
定义 divide 函数:
函数 divide 接收两个整数参数 a 和 b,返回 a / b 的结果。在函数体内,通过 [[assume(b != 0)]] 显式告知编译器:b 永远不会等于零。
main 函数中的调用测试:
在 main 函数中调用 divide(10, 2),将结果保存至 result。由于已通过 [[assume]] 告知编译器 b 不会为零,因此可以省去对除以零情况的额外检查。
断言验证结果:
使用 assert(result == 5) 验证除法结果是否正确,确保功能行为不变。
[[assume]] 提供了一种轻量级、平台无关的方式,让开发者可以向编译器传递语义信息,帮助其实现更高效的代码生成。尤其适用于对性能敏感的系统编程、数值计算以及嵌入式开发等领域。需要注意的是,[[assume]] 不具备运行时检查能力,如果假设条件不成立,可能导致未定义行为,因此应谨慎使用。
1.7、命名通用字符转义
C++23 引入了对命名 Unicode 字符转义的支持,显著提升了代码在处理国际化文本时的可读性与表达能力。开发者现在可以使用更具描述性的字符名称来表示 Unicode 字符,而不再依赖于记忆具体的码点值。
示例代码:
#include <iostream>
int main() {char smiley = '\N{WHITE SMILING FACE}';std::cout << smiley << std::endl;
}
示例解析:
该示例演示了如何通过命名字符语法来表示一个 Unicode 表情符号。
使用命名字符转义:
'\N{WHITE SMILING FACE}' 是一种新的字符字面量语法,用于表示 Unicode 中名为“WHITE SMILING FACE”的字符(即 U+263A)。这种写法比直接使用十六进制或八进制码点更具可读性和语义意义。
main 函数中的使用:
声明一个 char 类型变量 smiley,并将其初始化为上述命名字符。随后使用 std::cout 输出该字符,在支持显示表情符号的终端中将看到一个笑脸符号。
命名通用字符转义机制让开发者能够以更直观的方式在代码中嵌入 Unicode 字符,尤其适用于多语言界面开发、文档处理以及富文本操作等场景。这一特性不仅提高了代码的可维护性,也减少了因手动输入码点而导致错误的可能性。
1.8、扩展基于范围的 for 循环中临时变量的生命周期
C++23 对基于范围的 for 循环进行了改进,延长了在初始化器中创建的临时对象的生命周期,使其在整个循环过程中保持有效。这项改动解决了早期版本中由于临时对象频繁构造/析构带来的性能问题和潜在行为不一致的问题。
示例代码:
#include <vector>
#include <iostream>
int main() {for (const auto& num : std::vector<int>{1, 2, 3, 4, 5}) {std::cout << num << ' ';}return 0;
}
示例解析:
本示例展示了如何利用 C++23 的新规则,安全地在基于范围的 for 循环中使用临时容器。
定义临时容器:
在 for 循环的初始化部分,我们使用了一个匿名临时 std::vector<int> 来作为迭代源。该容器仅在循环开始前创建一次。
遍历元素:
循环体内部通过 const auto& 引用每个元素,并使用 std::cout 将其输出到控制台。
生命周期变化说明:
在 C++20 及更早版本中,此类临时变量可能在每次迭代结束后被销毁,导致性能下降甚至悬空引用的风险。而在 C++23 中,该临时对象的生命周期被扩展至整个循环体内,确保其在整个迭代过程中都保持有效。
这一改进使得开发者可以更安全、高效地在基于范围的 for 循环中使用临时对象,无需额外引入命名变量或将构造提前至循环外部。这不仅简化了代码结构,也提升了运行效率,增强了代码的稳定性和可读性。
1.9、constexpr增强
C++23 对 constexpr 的支持进行了进一步扩展,允许更多标准库组件和数学函数在编译期执行。这使得开发者可以在编译时完成更复杂的计算和对象构造,从而提升程序性能并增强代码的安全性。
示例代码:
#include <bitset>
#include <memory>
#include <cmath>
constexpr std::bitset<8> bitset_op() {std::bitset<8> b;b.set(3);return b;
}
constexpr std::unique_ptr<int> unique_ptr_op() {auto ptr = std::make_unique<int>(42);return ptr;
}
constexpr double compute_sqrt(double x) {return std::sqrt(x);
}
int main() {constexpr auto b = bitset_op();static_assert(b[3] == true, "Bit 3 should be set");constexpr auto ptr = unique_ptr_op();// Note: Cannot dereference constexpr unique_ptr in compile-time contextconstexpr double result = compute_sqrt(9.0);static_assert(result == 3.0, "sqrt(9.0) should be 3.0");
}
示例解析:
该示例展示了 C++23 中 constexpr 能力的显著提升。
使用 constexpr 操作 std::bitset:
函数 bitset_op 构建了一个 std::bitset<8> 类型的对象,并设置其第 3 位为 1。由于 std::bitset 的部分接口已支持 constexpr,因此该操作可在编译期完成。在 main 函数中,通过 static_assert 可以验证结果是否符合预期。
使用 constexpr 操作 std::unique_ptr:
函数 unique_ptr_op 返回一个 std::unique_ptr<int>,并且该返回值被声明为 constexpr。虽然不能在编译期解引用指针,但可以验证其构造和移动行为是否能在常量上下文中正确执行。
使用 constexpr 执行 <cmath> 函数:
函数 compute_sqrt 使用了 std::sqrt 来计算平方根,并被标记为 constexpr。在 main 函数中,通过 static_assert 验证了该计算结果是否为 3.0。
C++23 对 constexpr 的增强让开发者能够将更多运行时逻辑前移到编译期处理,不仅提升了程序效率,也增强了类型安全和可验证性。这一改进尤其适用于需要高性能计算和静态检查的系统级编程场景。
1.10、简化的隐式移动
C++23 对局部变量在函数返回时的行为进行了优化,简化了隐式移动机制。这项改进减少了不必要的拷贝构造和移动构造操作,提高了资源管理的效率,使代码更加简洁高效。
示例代码:
#include <iostream>
#include <vector>
std::vector<int> create_vector() {std::vector<int> vec = {1, 2, 3, 4, 5};return vec; // Implicit move occurs here
}
int main() {std::vector<int> vec = create_vector();for (int v : vec) {std::cout << v << ' ';}return 0;
}
示例解析:
本示例演示了如何利用 C++23 的隐式移动优化来提升函数返回局部对象的效率。
定义 create_vector 函数:
函数内部创建了一个局部 std::vector<int> 并初始化了一些元素。随后直接返回该局部变量。
返回值的隐式移动:
在早期标准中,即使启用了移动语义,也可能发生一次显式的移动构造或复制省略(RVO)。而在 C++23 中,这种返回行为被进一步优化,使得局部对象可以直接作为右值传递给接收者,避免了额外的构造步骤。
main 函数中的调用:
函数 create_vector() 被调用后赋值给 vec。由于优化的存在,整个过程没有触发任何拷贝或多余的移动操作。后续使用范围 for 输出向量内容。
简化的隐式移动机制使得开发者无需手动添加 std::move 或关注返回值优化细节,就能获得高效的对象传递。这对于频繁返回大型对象(如容器、字符串等)的函数来说,具有显著的性能优势。
1.11、静态下标运算符 static operator[]
C++23 允许类定义静态版本的下标运算符,使得可以通过类名直接访问静态数组成员,提供了一种更加直观且统一的方式来操作静态数据。
示例代码:
#include <iostream>
class StaticArray {
public:static int data[10];static int& operator[](std::size_t index) {return data[index];}
};
int StaticArray::data[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int main() {StaticArray::data[3] = 42;std::cout << StaticArray::data[3] << std::endl; // Output: 42return 0;
}
示例解析:
本示例展示了如何使用静态下标运算符来访问和修改类中的静态数组。
定义 StaticArray 类:
该类包含一个静态数组 data,并在外部完成了初始化。此外,还定义了一个静态的 operator[] 方法用于访问数组元素。
实现静态下标运算符:
operator[] 是一个静态成员函数,接受一个索引参数并返回对 data[index] 的引用。这样就可以像普通数组一样,通过类名加下标的方式访问静态数据。
main 函数中的使用:
通过 StaticArray::data[3] = 42 修改数组中的某个元素;接着通过 std::cout 输出修改后的值,确认操作有效。
静态下标运算符提供了一种更自然的方式来访问类的静态数组成员,使得代码结构更清晰、语义更明确。它特别适合用于封装全局数据表、配置信息、缓存结构等需要静态访问的场景,有助于提高代码的可读性和可维护性。
1.12、类模板参数推导(从继承构造函数中)
C++23 引入了一项增强功能:允许从继承的构造函数中自动推导类模板参数。这一改进简化了模板类的实例化过程,使开发者无需显式指定模板实参即可创建对象,从而提升了代码的简洁性和可读性。
示例代码:
#include <iostream>
template <typename T>
struct Base {T value;Base(T val) : value(val) {}
};
struct Derived : Base<int> {using Base::Base;
};
int main() {Derived d(42);std::cout << d.value << std::endl; // Output: 42return 0;
}
示例解析:
该示例演示了如何利用 C++23 的新特性,通过继承构造函数实现模板参数的自动推导。
定义模板基类 Base:
Base 是一个模板结构体,接受类型参数 T。它包含一个成员变量 value 和一个构造函数,用于初始化该变量。构造函数接受一个 T 类型的参数,并将其赋值给 value。
定义派生类 Derived:
Derived 继承自 Base<int>,并使用 using Base::Base; 导入基类的构造函数。这使得 Derived 可以直接调用 Base 的构造函数来完成初始化操作。
main 函数中的实例化操作:
在 main 中创建了一个 Derived 对象 d,并传入整数 42。由于构造函数是从 Base<int> 继承而来,编译器能够根据传入的参数自动识别出模板参数为 int,并正确调用相应的构造函数。
输出验证:
通过 std::cout 输出 d.value,结果为 42,说明模板参数被成功推导且构造逻辑执行无误。
C++23 中对类模板参数推导的支持扩展到了继承构造函数的场景,这项改进显著减少了模板类在使用时需要手动指定类型参数的频率。尤其在构建复杂的模板继承体系时,这种隐式的类型推导机制不仅简化了代码编写,还增强了接口的易用性和维护性。
二、标准库增强
2.1、字符串格式化改进
C++23 对标准库中的字符串格式化功能进行了显著增强,引入了更直观且功能丰富的 std::format 接口,并扩展了对范围(range)类型的格式化支持。这一更新使得开发者可以更加简洁高效地处理字符串拼接、类型转换和集合输出等常见任务。
示例代码:
#include <iostream>
#include <format>
#include <vector>
int main() {std::string name = "Alice";int age = 30;std::string output = std::format("Name: {}, Age: {}", name, age);std::cout << output << std::endl;std::vector<int> numbers = {1, 2, 3, 4, 5};std::string formatted_numbers = std::format("Numbers: {}", numbers);std::cout << formatted_numbers << std::endl; // Requires C++23 support
}
示例解析:
本示例演示了如何利用 std::format 进行字符串格式化操作。
基本格式化用法:
声明了两个变量 name 和 age,分别表示姓名和年龄。通过调用 std::format 函数,将它们插入到模板字符串 "Name: {}, Age: {}" 中,生成最终结果 "Name: Alice, Age: 30" 并输出。
格式化范围内容:
定义了一个 std::vector<int> 类型的变量 numbers,并传入 std::format 函数中作为参数。C++23 支持直接格式化整个容器内容,输出形式为 "Numbers: [1, 2, 3, 4, 5]" 或类似结构(具体格式依赖于实现)。
std::format 的引入标志着 C++ 在字符串处理方面迈出了重要一步。它不仅提供了类型安全的格式化机制,还简化了多参数拼接和集合展示的复杂度。对于日志系统、数据导出、UI 文本构建等需要频繁操作字符串的场景来说,这项改进大大提升了开发效率和代码可维护性。
2.2、新容器:flat_map与 flat_set
C++23 引入了两个新的关联容器:flat_map 和 flat_set,它们基于连续内存存储有序元素,提供比传统红黑树实现更高的性能,尤其适用于小型数据集或频繁查找的场景。
示例代码:
#include <flat_map>
#include <iostream>
int main() {std::flat_map<int, std::string> map = {{1, "one"}, {2, "two"}};map[3] = "three";for (const auto& [key, value] : map) {std::cout << key << ": " << value << '\n';}return 0;
}
示例解析:
该示例展示了 flat_map 的基本使用方法。
定义并初始化 flat_map 容器:
声明一个 std::flat_map<int, std::string> 类型的容器 map,并通过初始化列表添加两个键值对 {1, "one"} 和 {2, "two"}。
插入新键值对:
使用下标语法 map[3] = "three" 插入一个新的条目。由于 flat_map 使用排序数组实现,插入时会自动维持顺序。
遍历并输出所有元素:
通过基于范围的 for 循环结合结构化绑定语句 const auto& [key, value] 遍历容器,并逐行输出键值对。
flat_map 和 flat_set 提供了一种在性能和易用性之间取得良好平衡的替代方案。它们相比 std::map 和 std::set 具有更低的内存开销和更快的访问速度,尤其适合用于缓存表、配置项管理、索引映射等场景。这些容器在现代 C++ 编程中具有很高的实用价值,是编写高性能、低延迟程序的重要工具。
2.3、多维视图 mdspan
C++23 引入了 std::experimental::mdspan,提供了一种轻量级的多维数组访问机制,特别适用于科学计算、数值分析、图像处理以及高性能计算等需要频繁操作多维数据的应用场景。该特性不拥有底层数据,仅作为对现有内存的多维封装,从而实现高效且灵活的数据访问方式。
示例代码:
#include <experimental/mdspan>
#include <iostream>
using namespace std::experimental;
int main() {int data[6] = {1, 2, 3, 4, 5, 6};mdspan<int, extents<3, 2>> matrix(data);for (int i = 0; i < 3; ++i) {for (int j = 0; j < 2; ++j) {std::cout << matrix(i, j) << ' ';}std::cout << '\n';}
}
示例解析:
本示例展示了如何利用 mdspan 将一维数组视为二维矩阵,并进行多维索引访问。
定义原始数据:
声明一个长度为 6 的整型数组 data,用于模拟二维矩阵中的元素存储。
创建多维视图:
使用 mdspan<int, extents<3, 2>> 创建名为 matrix 的视图对象,表示一个 3 行 2 列的二维结构。matrix 不复制数据,而是通过指针引用 data 数组,提供了按行列访问的能力。
遍历并输出元素:
通过嵌套的 for 循环,依次访问 matrix(i, j) 中的每个元素,并打印输出,结果如下:
1 2
3 4
5 6
mdspan 提供了一种安全、高效的多维数据访问方式,无需额外内存开销即可将线性存储解释为任意维度的结构化视图。这一特性在涉及大量数值运算或数据变换的领域具有重要意义,例如机器学习、信号处理、图像算法等,极大地提升了开发效率和运行性能。
2.4、标准生成器协程
C++23 对协程的支持进一步标准化,引入了标准生成器协程的概念,使得开发者能够更轻松地构建惰性求值的序列生成器。这种模式非常适合流式数据处理、按需生成值、无限序列迭代等场景。
示例代码:
#include <coroutine>
#include <iostream>
template <typename T>
struct Generator {struct promise_type {T value;std::suspend_always yield_value(T v) {value = v;return {};}std::suspend_always initial_suspend() { return {}; }std::suspend_always final_suspend() noexcept { return {}; }Generator get_return_object() {return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };}void return_void() {}void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> handle;Generator(std::coroutine_handle<promise_type> h) : handle(h) {}~Generator() { if (handle) handle.destroy(); }bool next() {handle.resume();return !handle.done();}T getValue() { return handle.promise().value; }
};
Generator<int> sequence(int start, int step) {for (int i = start;; i += step) {co_yield i;}
}
int main() {auto gen = sequence(0, 5);for (int i = 0; i < 10; ++i) {gen.next();std::cout << gen.getValue() << ' ';}return 0;
}
示例解析:
本示例演示了如何通过协程实现一个简单的整数序列生成器。
定义 Generator 模板结构体:
Generator<T> 是一个通用的协程包装类,包含一个内部 promise_type 结构,负责管理协程的状态和返回值。
实现协程逻辑:
函数 sequence 是一个生成器协程,接收起始值 start 和步长 step,使用 co_yield 关键字逐步返回递增的整数值,形成一个无限序列。
main 函数中使用生成器:
创建一个从 0 开始、每次增加 5 的序列生成器。通过调用 next() 方法恢复协程执行,并通过 getValue() 获取当前生成的值。循环十次后输出结果如下:
0 5 10 15 20 25 30 35 40 45
标准生成器协程为 C++ 带来了类似 Python 中 yield 的强大功能,使开发者可以以声明式的方式编写复杂的序列生成逻辑。相比传统的手动状态机实现,协程方式更加直观、易于维护,并支持惰性求值,有助于优化资源消耗和提升程序响应能力。该特性在处理大数据流、事件驱动系统、状态机逻辑等方面具有广泛的应用前景。
2.5、使用 std::expected的新型错误处理机制
C++23 引入了 std::expected<T, E> 类型,为函数返回值中嵌入成功结果或错误信息提供了一种类型安全且语义清晰的方式。相比传统的异常机制或手工封装的返回结构,std::expected 提供了更直观的接口,并增强了代码的可读性和健壮性。
示例代码:
#include <expected>
#include <iostream>
#include <string>
std::expected<int, std::string> safe_divide(int a, int b) {if (b == 0) {return std::unexpected("Division by zero!");}return a / b;
}
int main() {auto result = safe_divide(10, 2);if (result) {std::cout << "Result: " << result.value() << std::endl;} else {std::cout << "Error: " << result.error() << std::endl;}result = safe_divide(10, 0);if (result) {std::cout << "Result: " << result.value() << std::endl;} else {std::cout << "Error: " << result.error() << std::endl;}return 0;
}
示例解析:
该示例演示了如何利用 std::expected 来统一处理函数调用的成功结果与错误状态。
定义带有预期返回类型的函数:
函数 safe_divide 返回一个 std::expected<int, std::string>,表示它可能返回一个整数结果,也可能返回一个字符串形式的错误信息。如果除数为零,则通过 std::unexpected 构造一个错误值并返回;否则返回计算结果。
main 函数中的使用:
两次调用 safe_divide,分别传入有效和无效参数。每次调用后检查返回对象是否包含有效值(使用 if (result) 判断),并通过 .value() 获取正常结果或 .error() 获取错误描述。
输出结果:
Result: 5
Error: Division by zero!
std::expected 提供了一种无需依赖异常机制即可显式处理错误的方法,使得开发者能够以更清晰、更可控的方式管理函数调用失败的情况。这种模式特别适用于系统级编程、库开发、异步操作以及对性能敏感的应用场景。它不仅提高了代码的可维护性,也降低了因忽略错误而引发运行时问题的风险。
2.6、运行时堆栈跟踪支持
C++23 引入了 <stacktrace> 头文件,为程序提供了获取当前执行路径的能力。这一功能在调试复杂程序、分析崩溃原因或记录诊断信息时非常有用。通过std::stacktrace,开发者可以轻松地打印出当前的调用堆栈,从而快速定位错误源头。
示例代码:
#include <iostream>
#include <stacktrace>
void foo() {std::cout << std::stacktrace::current() << std::endl;
}
int main() {foo();return 0;
}
示例解析:
本示例演示了如何在运行时获取并打印当前的堆栈跟踪信息。
引入头文件:
使用 #include <stacktrace> 引入标准库提供的堆栈跟踪工具。
定义函数 foo:
在函数 foo 中,调用 std::stacktrace::current() 获取当前的调用堆栈信息,并将其输出到控制台。
main 函数中的调用:
在 main 函数中调用 foo(),触发堆栈跟踪的生成与打印。
典型输出格式(取决于平台):
foo() at example.cpp:5
main() at example.cpp:10
运行时堆栈跟踪功能极大地简化了调试流程,尤其是在没有调试器支持的环境中,例如生产服务器或自动化测试系统。开发者可以在关键路径、错误处理逻辑或异常捕获块中插入堆栈打印语句,以便在发生异常行为时快速获取上下文信息。这项新特性提升了 C++ 在大型项目中日志记录和故障排查方面的能力,是构建高可靠性系统的重要补充。
三、多线程与并发增强
3.1、std::jthread—— 自动管理生命周期的线程类型
C++23 引入了 std::jthread 类型,作为对 std::thread 的增强版本,它能够在对象销毁时自动调用 join(),从而有效避免资源泄漏和未定义行为。这一特性使得线程的管理更加安全、简洁,并减少了手动干预的需要。
示例代码:
#include <iostream>
#include <thread>
#include <chrono>
void work() {std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Work done" << std::endl;
}
int main() {std::jthread t(work);// 当 t 超出作用域时,会自动调用 join()return 0;
}
示例解析:
该示例演示了如何使用 std::jthread 创建并自动管理线程的生命周期。
- 定义工作函数 work:
模拟一个耗时操作,线程休眠一秒后输出 "Work done"。 - 创建 std::jthread 实例:
在 main 函数中启动一个 std::jthread,执行 work 函数。 - 自动回收机制:
当 main 函数结束时,局部变量 t 析构,其析构函数会自动调用 join(),确保主线程等待子线程完成后再退出。
std::jthread 提供了一种更安全、更直观的方式来处理线程的生命周期,特别适用于需要在多个作用域中创建线程的场景。通过减少手动调用 join() 或 detach() 的需求,开发者可以编写出更具可维护性和健壮性的并发代码。
3.2、std::stop_token与 std::stop_source—— 标准化的线程停止机制
C++23 引入了 std::stop_token 和 std::stop_source,为线程提供了一种标准的、协作式的停止机制。这种设计允许线程主动检测是否收到终止请求,从而实现更安全、可控的线程关闭过程。
示例代码:
#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token>
void work(std::stop_token st) {while (!st.stop_requested()) {std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "Working..." << std::endl;}std::cout << "Stopped." << std::endl;
}
int main() {std::jthread t(work);std::this_thread::sleep_for(std::chrono::seconds(1));t.request_stop();return 0;
}
示例解析:
本示例展示了如何利用 std::stop_token 实现线程的优雅终止。
- 定义带参数的 work 函数:
接收一个 std::stop_token 参数,用于轮询检查是否有终止请求。只要没有请求,就持续输出 "Working...",否则退出循环并打印 "Stopped."。 - 创建支持停止的线程:
使用 std::jthread 启动 work 函数,它内部支持响应停止信号。 - 发出停止请求:
主线程休眠 1 秒后,调用 request_stop() 方法通知工作线程终止。工作线程检测到信号后安全退出。
通过 std::stop_token,C++ 提供了一种标准化的线程取消机制,使得线程可以在不强制中断的情况下自行决定何时退出。这大大增强了程序的稳定性和资源释放的安全性,是构建高可靠性并发系统的重要工具。
3.3、std::latch与 std::barrier—— 新增同步原语
C++23 引入了两个新的同步机制:std::latch 和 std::barrier,它们分别用于协调多个线程的开始或阶段性同步任务。相比传统的条件变量和互斥锁组合,这些新原语提供了更高效、更易用的同步方式。
示例代码(std::latch):
#include <iostream>
#include <thread>
#include <latch>
#include <vector>
void worker(std::latch& latch) {std::cout << "Worker is working...\n";std::this_thread::sleep_for(std::chrono::seconds(1));latch.count_down();std::cout << "Worker is done.\n";
}
int main() {std::latch latch(3);std::vector<std::thread> threads;for (int i = 0; i < 3; ++i) {threads.emplace_back(worker, std::ref(latch));}latch.wait();std::cout << "All workers are done.\n";for (auto& t : threads) {t.join();}return 0;
}
示例解析(std::latch):
此示例演示了如何使用 std::latch 来等待一组线程全部完成。
- 定义 worker 函数:
每个线程模拟工作负载,完成后调用 count_down() 减少计数器。 - 初始化 std::latch:
设置初始计数为 3,表示等待三个线程完成。 - 主线程等待:
调用 wait() 阻塞主线程,直到所有线程完成任务。
示例代码(std::barrier):
#include <iostream>
#include <thread>
#include <barrier>
#include <vector>
void phase_work(std::barrier<>& sync_point) {std::cout << "Phase 1 working...\n";std::this_thread::sleep_for(std::chrono::seconds(1));sync_point.arrive_and_wait();std::cout << "Phase 2 working...\n";std::this_thread::sleep_for(std::chrono::seconds(1));sync_point.arrive_and_wait();
}
int main() {std::barrier sync_point(3);std::vector<std::thread> threads;for (int i = 0; i < 3; ++i) {threads.emplace_back(phase_work, std::ref(sync_point));}for (auto& t : threads) {t.join();}return 0;
}
示例解析(std::barrier):
该示例演示了如何使用 std::barrier 来同步多个线程的阶段性任务。
- 定义 phase_work 函数:
每个线程分阶段执行任务,在每个阶段末尾调用 arrive_and_wait(),等待其他线程完成当前阶段后再继续下一阶段。 - 初始化 std::barrier:
设置初始参与者数量为 3,表示需要三个线程参与同步。 - 线程间同步:
所有线程在进入下一阶段前必须等待其他线程到达屏障点。
std::latch 和 std::barrier 是 C++23 中引入的关键并发同步工具,分别适用于一次性等待和多阶段同步的场景。它们简化了并发编程中常见的协调问题,提高了代码的可读性和性能表现,是现代 C++ 并发开发中的重要补充。
3.4、任务块与协同任务:并发任务管理的新方式
C++23 在并发编程模型中引入了更高级别的抽象机制,使得开发者可以更灵活地组织和调度多个线程任务。通过结合 std::latch 和 std::barrier 等同步原语,可以实现对任务启动阶段和完成阶段的统一协调,从而提升整体执行效率和资源利用率。
示例代码:
#include <iostream>
#include <thread>
#include <vector>
#include <latch>
#include <barrier>
// 工作函数,模拟并发任务
void work(std::latch& start_latch, std::barrier<>& end_barrier, int id) {start_latch.wait(); // 等待所有线程准备就绪std::cout << "Worker " << id << " is working...\n";std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作负载end_barrier.arrive_and_wait(); // 等待其他任务完成std::cout << "Worker " << id << " has finished.\n";
}
int main() {const int num_threads = 5;std::latch start_latch(num_threads);std::barrier end_barrier(num_threads + 1); // 主线程也参与同步std::vector<std::thread> threads;for (int i = 0; i < num_threads; ++i) {threads.emplace_back(work, std::ref(start_latch), std::ref(end_barrier), i + 1);start_latch.count_down(); // 表示当前线程已准备好}end_barrier.arrive_and_wait(); // 主线程等待所有任务完成std::cout << "All workers have finished.\n";for (auto& t : threads) {t.join();}return 0;
}
示例解析:
本示例展示了如何利用 std::latch 和 std::barrier 实现任务的统一启动和阶段性同步。
- 定义工作函数 work:
每个线程在开始前调用 start_latch.wait(),等待所有线程初始化完成;完成后调用 end_barrier.arrive_and_wait(),等待所有线程一同结束。 - 初始化同步对象:
std::latch 初始值为线程数,用于确保所有线程都启动后才开始执行;std::barrier 初始值为线程数加一,表示主线程也参与最终的同步。 - 创建并启动线程:
使用 std::vector<std::thread> 启动多个线程,每个线程减少一次 latch 的计数,表示自己已就绪。 - 主线程控制流程:
主线程调用 end_barrier.arrive_and_wait(),等待所有工作线程完成任务后再继续执行后续逻辑。 - 等待线程结束:
最后依次调用 join() 方法,确保所有线程安全退出。
通过使用 std::latch 和 std::barrier,开发者可以构建出结构清晰、同步精确的任务块模型。这种模式特别适用于需要多线程协作完成阶段性工作的场景,例如并行计算、分布式任务调度或流水线处理等。它不仅提升了程序的可读性和维护性,还增强了并发任务的可控性和性能表现。
3.5、执行策略增强:并行算法支持
C++23 对标准库中的执行策略进行了增强,允许开发者通过简单的策略选择来启用并行或异步执行,显著提升算法在多核环境下的执行效率。这一特性对于数据密集型应用尤其重要。
示例代码:
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {std::vector<int> vec(1'000'000, 1);std::transform(std::execution::par, vec.begin(), vec.end(), vec.begin(),[](int x) { return x * 2; });std::cout << "Transformation done.\n";return 0;
}
示例解析:
该示例演示了如何使用并行执行策略加速大规模数据处理操作。
- 初始化数据集:
创建一个包含一百万个整数的向量,所有元素初始值为 1。 - 使用并行执行策略:
调用 std::transform 并传入 std::execution::par 执行策略,指示标准库将该操作并行化。每个元素都会被乘以 2,但整个过程由多个线程并发执行。 - 输出完成信息:
当所有线程完成转换操作后,打印 "Transformation done."。
C++23 中的执行策略增强为开发者提供了一种简洁的方式来启用并行化操作,而无需手动编写复杂的线程调度逻辑。只需更改一行代码即可让标准算法自动利用多核处理器的优势,显著缩短执行时间。这在图像处理、数据分析、科学计算等高性能计算领域具有广泛的应用价值。
此外,由于策略是透明的,开发者可以在不同环境下切换策略(如串行、并行、异步)而不影响核心逻辑,大大提高了代码的灵活性和适应性。
四、实用工具与增强功能
4.1、std::print和 std::println—— 更简洁的输出方式
C++23 引入了两个新的标准输出函数:std::print 和 std::println,为开发者提供了一种更加直观、易用的格式化输出机制。相比传统的 std::cout 和 C 风格的 printf,它们在语法上更简洁,并支持现代格式化方式,显著提升了代码可读性和开发效率。
示例代码:
#include <print>
int main() {std::print("Hello, World!\n");std::println("Formatted number: {}", 42);return 0;
}
示例解析:
本示例演示了如何使用 std::print 和 std::println 进行文本输出。
- 引入头文件:
使用 #include <print> 来访问这两个新函数所需的接口。 - 使用 std::print 输出字符串:
调用 std::print("Hello, World!\n") 直接打印字符串内容。这种方式不需要像 std::cout 那样使用多个 << 操作符,也无需手动指定类型。 - 使用 std::println 实现格式化输出:
std::println("Formatted number: {}", 42) 使用 {} 占位符插入变量值,类似于 Python 的 f-string 或 Java 的 String.format()。它会自动将整数 42 转换为字符串并替换占位符。此外,std::println 在输出结束后自动添加换行符。
std::print和 std::println是对传统 I/O 接口的重要补充,尤其适用于快速调试、日志记录和简单输出场景。它们不仅简化了格式化输出的编写,还提高了代码的可维护性和一致性,是现代 C++ 编程中非常实用的新特性。
4.2、Ranges 库更新 —— 更强大的集合操作能力
C++23 对 Ranges 库进行了进一步完善,新增了许多适配器和算法,使得处理容器、序列等数据集合变得更加灵活、高效。通过链式调用和惰性求值的方式,Ranges 提供了更具表现力的集合操作风格。
示例代码:
#include <iostream>
#include <ranges>
#include <vector>
int main() {std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// 使用 filter 和 transform 构建处理链auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });auto squared_numbers = even_numbers | std::views::transform([](int n) { return n * n; });for (int n : squared_numbers) {std::cout << n << ' ';}std::cout << std::endl;// 使用 contains 判断元素是否存在bool contains_five = std::ranges::contains(numbers, 5);std::cout << "Contains 5: " << std::boolalpha << contains_five << std::endl;return 0;
}
示例解析:
该示例展示了如何利用 C++23 Ranges 的新特性进行集合操作。
- 定义初始数据集:
创建一个包含整数 1 到 10 的向量 numbers。 - 构建过滤与转换链:
使用 std::views::filter 保留偶数,然后通过 std::views::transform 将这些偶数平方。这种链式写法清晰地表达了数据流的变换过程。 - 遍历结果并输出:
使用范围 for 循环输出经过处理后的结果,输出为 4 16 36 64 100。 - 使用新算法 std::ranges::contains:
检查原始数据集中是否包含数字 5,返回布尔值后输出结果为 true。
Ranges 库的增强使得开发者可以以声明式的方式编写集合处理逻辑,减少了冗余代码,提高了可读性和可维护性。对于需要频繁操作容器的项目(如数据分析、算法实现、UI 数据绑定等),这些改进带来了实质性的生产力提升。
4.3、单元操作增强 —— std::optional与 std::variant的改进
C++23 对 std::optional 和 std::variant 进行了多项增强,包括新增成员函数、优化操作符重载以及提升错误处理机制,使它们在实际应用中更加灵活和安全。
示例代码:
#include <iostream>
#include <optional>
#include <variant>
int main() {std::optional<int> opt = 42;if (opt) {std::cout << "Optional value: " << *opt << std::endl;}std::variant<int, std::string> var = "Hello, World!";std::cout << "Variant value: " << std::get<std::string>(var) << std::endl;var = 2023;std::cout << "Variant value: " << std::get<int>(var) << std::endl;return 0;
}
示例解析:
此示例演示了 std::optional 和 std::variant 的基本使用及其增强功能。
- 使用 std::optional 表达可选值:
定义 std::optional<int> opt = 42; 表示一个可能为空的整数值。通过 if (opt) 可以直接判断是否有值存在。如果存在,则使用 *opt 解引用获取其值。 - 使用 std::variant 表示多态值:
定义 std::variant<int, std::string> var = "Hello, World!" 表示该变量可以在运行时持有两种不同类型的值。使用 std::get<T>() 可以安全地提取当前存储的值。 - 修改 variant 中的值:
将 var 重新赋值为整数 2023,并通过 std::get<int>(var) 获取并输出。
std::optional 和 std::variant 是现代 C++ 中用于处理“不确定性”和“多态值”的核心工具。C++23 的增强让它们的使用更加自然和安全,尤其适合于 API 设计、状态管理、配置处理、错误传递等场景。这些改进有助于减少空指针解引用和类型不匹配等常见错误,提高程序的健壮性。
五、总结
有人说C++ 23是一个小版本,相对C++11或者C++20而言,它就像站在巨人肩膀之上的小矮子。但实际上C++23版本正式克服了很多困难推出了比C++14规模要大且可以媲美C++17的改进。
C++23 在错误处理、并发编程和实用工具等方面带来了多项重要更新,如 std::expected和 std::stacktrace提升了代码的健壮性和调试能力,std::jthread、std::stop_token和同步原语增强了多线程开发的安全与便捷性,而 std::print、Ranges 库改进以及 std::optional和 std::variant的增强,也让日常开发更加高效和直观。这些特性共同推动了 C++ 向更现代、更易用、更高性能的方向发展,为开发者提供了更强大的表达能力和更稳定的编程体验。
点击下方关注【Linux教程】,获取编程学习路线、项目教程、简历模板、大厂面试题pdf文档、大厂面经、编程交流圈子等等。