1. 简介
C++23 是由 C++ 标准委员会最新发布的标准,旨在进一步提升 C++ 语言的功能和开发效率。作为一项重要的编程语言标准更新,C++23 引入了多个关键的新特性和改进,使开发者能够编写更高效、简洁和安全的代码。
与 C++20 相比,C++23 的变化虽然没有那么显著,但依然对语言的稳固性和可用性做出了许多重要改进。C++20 引入了大量新特性,如模块、协程、概念等,极大地丰富了 C++ 的语法和功能。而 C++23 则在这些基础上进行了补充和优化,解决了一些细节问题,并引入了新的编程工具和方法。
C++23 的新特性包括明确的对象参数(Deducing this
)、if consteval
、多维下标运算符、内建衰减复制支持、标记不可达代码(std::unreachable
)、平台无关的假设([[assume]]
)、命名通用字符转义、扩展基于范围的 for 循环中临时变量的生命周期、constexpr
增强、简化的隐式移动、静态运算符 static operator[]
以及类模板参数推导(Class Template Argument Deduction from Inherited Constructors)。这些特性旨在提高代码的可读性和可维护性,同时优化编译器的性能和程序的执行效率。
此外,C++23 还对标准库进行了重要更新,增加了新的容器类型如 flat_map
和 flat_set
,引入了多维视图(mdspan
)以及标准生成器协程(Generator Coroutines),并改进了字符串格式化和错误处理机制(如 std::expected
)。这些更新使得 C++ 在处理复杂数据结构和并发编程时更加得心应手。
总体而言,C++23 通过一系列细致入微的改进和新增功能,进一步巩固了 C++ 作为高效、强大编程语言的地位,为开发者提供了更丰富的工具箱,助力他们在各种应用场景中编写出色的代码,本文将详细介绍这些新特性,并通过代码示例帮助开发人员更好地理解和应用这些技术。
2. 新增语言特性
2.1. 明确的对象参数 (Deducing this
)
C++23引入了明确的对象参数,允许在非静态成员函数中明确指定对象参数。这一特性简化了某些复杂的C++编程模式,如Curiously Recurring Template Pattern (CRTP)。
示例代码:
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
。- 在
func
函数内部,使用static_cast
将this
指针转换为T*
类型,并调用T
类的impl
成员函数。 - 这种方式允许在基类中定义通用的接口,而具体的实现则由派生类提供。
- 定义派生类
Derived
: -
Derived
类继承自Base
,并实现了一个impl
成员函数,该函数接受一个int
参数。impl
函数是Derived
类的具体实现,当基类的func
调用时,将会转发给这个函数。
- 在
main
函数中使用: -
- 创建一个
Derived
类的实例d
。 - 调用
d.func(42)
时,func
函数中的static_cast
会将this
指针转换为Derived*
,然后调用Derived::impl
函数。 - 这样,通过基类
Base
中定义的通用接口func
,实现了对派生类Derived
中具体实现impl
的调用。
- 创建一个
这个示例展示了明确的对象参数如何简化 CRTP 模式,使得基类能够定义通用接口,而派生类提供具体实现。这种方式提高了代码的灵活性和可维护性,同时减少了模板代码的复杂性。C++23 的这一新特性,使得编写和使用 CRTP 模式更加直观和高效。
2.2. if consteval
if consteval
关键字用于在编译时判断一个常量表达式是否正在求值,从而增强了编译时计算的能力。通过 if consteval
,开发者可以在编译时与运行时进行不同的代码路径选择,使得编译时计算更加灵活和强大。
示例代码:
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
函数: -
compute
函数是一个constexpr
函数,接受一个整数参数x
。- 使用
if consteval
关键字在编译时判断是否在编译时进行计算。 - 如果是编译时计算,返回
x * 2
。 - 如果不是编译时计算,返回
x * 3
。
- 在
main
函数中调用compute
: -
- 定义
constexpr int result = compute(5);
,在编译时计算compute(5)
的结果。 - 由于
compute
函数在编译时进行计算,if consteval
条件为真,返回5 * 2
,结果为10
。
- 定义
通过这种方式,if consteval
关键字提供了一种灵活的机制,使得开发者可以在编译时与运行时进行不同的代码路径选择。这样不仅增强了编译时计算的能力,还可以根据实际情况优化代码路径,提高程序的性能和效率。这对于需要在编译时进行大量计算的场景,如常量表达式求值、编译时优化等,特别有用。
2.3. 多维下标运算符
多维下标运算符增强了对多维数组的支持,使代码更加直观和简洁。通过定义多维下标运算符,开发者可以更加方便地访问和操作多维数组数据,使代码逻辑更加清晰。
示例代码:
#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
成员变量是一个大小为Rows * Cols
的std::array
,用于存储矩阵数据。
- 定义多维下标运算符:
-
operator[]
重载函数接受一个std::pair<size_t, size_t>
类型的参数idx
,表示要访问的元素位置。- 在
operator[]
函数内部,通过计算idx.first * Cols + idx.second
得到一维数组中的索引,从而访问data
中的相应元素。 - 提供了两个版本的
operator[]
,一个用于非 const 对象,另一个用于 const 对象,确保在不同上下文中都可以正确访问矩阵数据。
- 在
main
函数中使用Matrix
: -
- 创建一个
Matrix<int, 3, 3>
对象mat
,并初始化数据。 - 通过多维下标运算符访问矩阵中的元素。例如,
mat[{1, 2}]
访问矩阵第二行第三列的元素,输出结果为 6。
- 创建一个
通过使用多维下标运算符,可以更加直观和简洁地访问和操作多维数组数据。这种方式不仅提高了代码的可读性,还减少了错误的可能性,使得开发者在处理复杂数据结构时更加得心应手。C++23 中的这一特性为开发者提供了更强大的工具,帮助他们编写高效、清晰的代码。
2.4. 内建衰减复制支持
C++23 改进了某些上下文中衰减复制的处理,提供了更可预测的行为。衰减复制(decay copy)是将参数类型转换为其基本类型的过程,这在模板编程和函数重载中非常有用。C++23 通过增强对衰减复制的支持,使得处理模板参数更加直观和可靠。
示例代码:
#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*
}
示例解析:
在这个示例中,展示了如何使用内建衰减复制支持来处理模板参数。以下是详细解析:
- 模板函数
process
的定义: -
process
函数是一个接受通用引用参数T&& arg
的模板函数。- 在函数内部,使用
std::decay_t<T>
对arg
进行衰减复制,并将结果存储在val
变量中。std::decay_t
是一个类型转换工具,它将T
转换为其基本类型,例如将数组类型转换为指针类型,将函数类型转换为指针类型,移除引用和 cv 限定符等。 std::forward<T>(arg)
用于完美转发arg
参数,确保在转发时保持其左值或右值属性。
- 在
main
函数中调用process
: -
- 调用
process(x)
,其中x
是一个整型变量。x
被传递为int&
类型,在process
函数内部经过std::decay_t
处理后,val
的类型为int
。 - 调用
process(42)
,其中42
是一个整型字面值常量。42
被传递为int&&
类型,在process
函数内部经过std::decay_t
处理后,val
的类型为int
。 - 调用
process("Hello")
,其中"Hello"
是一个字符串字面值常量。"Hello"
被传递为const char(&)[6]
类型,在process
函数内部经过std::decay_t
处理后,val
的类型为const char*
。
- 调用
通过这种方式,C++23 中的内建衰减复制支持使得模板参数的处理更加直观和可靠。在模板函数中使用 std::decay_t
可以确保参数被正确地转换为其基本类型,从而避免类型转换错误和未定义行为。这一特性特别适用于泛型编程和库开发,使得代码更加健壮和易于维护。
2.5. 标记不可达代码 (std::unreachable
)
新增的 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]]
属性标记,表明该函数不会返回。它通过抛出异常来中断程序的正常执行流。- 当
error
函数被调用时,程序会跳出当前执行路径,而不会继续执行后续代码。
- 在
main
函数中调用error
: -
- 在
main
函数中,定义一个整型变量x
并初始化为0
。 - 使用
if
语句检查x
的值。如果x
等于0
,则调用error
函数。
- 在
- 使用
std::unreachable
标记不可达代码: -
- 在
error
函数调用后,程序的执行流被中断,后续代码将不会被执行。 std::unreachable()
用于明确告诉编译器,该位置的代码永远不会被执行。这样,编译器可以优化此部分代码,避免不必要的检查和警告。
- 在
示例输出解释:
由于在 main
函数中调用了 error
函数,程序会在抛出异常后终止,std::unreachable()
所在的代码段永远不会被执行。因此,编译器知道该位置不可达,可以进行相应的优化。通过使用 std::unreachable
,开发者可以帮助编译器更好地理解代码的逻辑结构,优化编译过程中的代码生成和警告检查。
2.6. 平台无关的假设 ([[assume]]
)
[[assume]]
属性允许开发者在代码中声明某些条件总是为真,帮助编译器进行更好的优化,同时提高代码的可移植性。通过使用 [[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 0
assert(result == 5);
}
示例解析:
在这个示例中,展示了如何使用 [[assume]]
属性来帮助编译器进行优化。以下是详细解析:
- 定义
divide
函数: -
divide
函数接受两个整数参数a
和b
,并返回a
除以b
的结果。- 使用
[[assume(b != 0)]]
声明b
永远不会等于 0。这意味着函数内部假设b
不为 0,从而消除除以零的检查。
- 在
main
函数中调用divide
: -
- 在
main
函数中,调用divide(10, 2)
并将结果存储在result
变量中。 - 由于
[[assume(b != 0)]]
声明b
不为 0,编译器可以优化divide
函数,省略除以零的检查。
- 在
- 断言结果:
-
- 使用
assert(result == 5)
断言result
的值为 5,以确保函数返回正确的结果。
- 使用
示例输出解释:
由于 [[assume(b != 0)]]
告诉编译器 b
永远不会为 0,编译器可以优化 divide
函数,去掉除以零的检查,从而生成更高效的代码。通过使用 [[assume]]
,开发者可以提供额外的信息给编译器,帮助其进行更激进的优化,提高程序的性能和可移植性。这对于需要严格性能优化的场景,例如嵌入式系统或高性能计算,非常有用。
2.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
是 Unicode 字符的名字,对应的 Unicode 码点是 U+263A。- 这种命名方式相比直接使用 Unicode 码点更加直观,开发者无需记住具体的码点,只需使用字符的描述性名字即可。
- 在
main
函数中使用: -
- 声明一个字符变量
smiley
,并将其初始化为'\N{WHITE SMILING FACE}'
。此时,smiley
变量中存储的是对应的 Unicode 字符。 - 使用
std::cout
输出smiley
变量,程序将输出一个白色笑脸字符。
- 声明一个字符变量
通过使用命名通用字符转义,开发者可以更加方便地在代码中使用 Unicode 字符,特别是在处理多语言文本、特殊符号或表情符号时。这不仅提高了代码的可读性,还增强了代码的国际化支持,使得编写和维护多语言应用程序更加容易。C++23 的这一特性为开发者提供了一个更加直观和描述性的方法来表示 Unicode 字符,减少了错误的可能性,并且使得代码更加易于理解和维护。
2.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>{1, 2, 3, 4, 5}
来初始化循环。 - 该临时变量是一个包含 5 个整数的
std::vector
,在整个 for 循环中都保持有效。
- 在基于范围的 for 循环中,我们使用了一个临时变量
- 遍历临时变量:
-
for (const auto& num : std::vector<int>{1, 2, 3, 4, 5})
表达式中,num
是对临时变量std::vector<int>
中每个元素的常量引用。- 在 C++23 之前的标准中,每次循环迭代结束时,临时变量会被销毁,然后在下一次迭代时重新创建。这可能导致性能开销和潜在的错误。
- C++23 扩展了临时变量的生命周期,确保其在整个循环过程中保持有效。这样,临时变量只需创建一次,并在整个循环体内保持有效,从而提高了性能并确保了逻辑的一致性。
- 输出结果:
-
- 在循环体内,通过
std::cout
输出num
的值。 - 由于临时变量在整个循环过程中保持有效,每次迭代都可以正确访问和输出
std::vector<int>
中的元素,结果输出为1 2 3 4 5
。
- 在循环体内,通过
通过这种方式,C++23 的这一改进提高了基于范围的 for 循环的效率和可靠性,使得代码更加简洁和稳定。开发者可以更安全地使用临时变量进行遍历操作,而无需担心其生命周期问题。
2.9. constexpr
增强
C++23 进一步放宽了 constexpr
的限制,使得更多的函数和表达式可以在编译时计算,包括 std::type_info::operator==
、std::bitset
、std::unique_ptr
、部分 <cmath>
函数以及 std::to_chars
和 std::from_chars
的整数重载。
示例代码:
#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 context
constexpr 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
函数中,constexpr auto b = bitset_op()
初始化了一个constexpr
的std::bitset
对象b
,并使用static_assert
断言第 3 位为 1。这表明std::bitset
的部分操作现在可以在constexpr
上下文中进行。
constexpr
操作std::unique_ptr
:-
unique_ptr_op
函数创建了一个std::unique_ptr<int>
,并将其指向一个值为 42 的整数。- 返回的
std::unique_ptr
对象是constexpr
,可以在编译时使用。 - 在
main
函数中,constexpr auto ptr = unique_ptr_op()
初始化了一个constexpr
的std::unique_ptr<int>
对象ptr
。虽然在编译时不能解引用constexpr
的std::unique_ptr
,但可以验证其构造和移动操作在编译时是有效的。
constexpr
操作<cmath>
函数:-
compute_sqrt
函数使用了std::sqrt
计算平方根,并被标记为constexpr
。- 在
main
函数中,constexpr double result = compute_sqrt(9.0)
初始化了一个constexpr
的double
值result
,并使用static_assert
断言其值为 3。这表明<cmath>
中的部分数学函数现在可以在constexpr
上下文中使用。
通过这些示例,展示了 C++23 对 constexpr
的增强使得更多标准库组件和数学函数可以在编译时计算,提高了代码的效率和安全性。这些改进使得开发者能够编写更高效、性能更优的代码,并利用编译时计算的优势进行更多优化。
2.10. 简化的隐式移动
C++23简化了对象在函数返回时的隐式移动,使得返回局部对象更加高效。在以前的标准中,返回局部对象可能会触发拷贝构造函数或移动构造函数,而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
: -
create_vector
函数创建并初始化一个局部的std::vector<int>
对象vec
,其中包含了一些整数。- 在函数末尾,
vec
被返回。此时,触发了隐式移动操作。
- 隐式移动操作:
-
- 在以前的标准中,返回局部对象可能会触发拷贝构造函数或移动构造函数,视具体情况而定。尽管移动构造函数已经比拷贝构造函数高效,但仍有改进的空间。
- C++23 通过优化,使得返回局部对象时可以直接移动,不再进行不必要的拷贝或复杂的移动构造。这种优化确保了资源的高效传递,减少了额外的性能开销。
- 在
main
函数中使用create_vector
: -
- 调用
create_vector
函数并将返回的std::vector<int>
对象赋值给变量vec
。 - 由于 C++23 的优化,
vec
的构造变得更加高效,减少了内存分配和数据拷贝的开销。 - 使用范围for循环遍历
vec
,并输出其中的每个元素。
- 调用
通过这种方式,C++23的简化隐式移动特性显著提高了返回局部对象的效率,使代码更高效和简洁。这一特性对需要频繁返回大对象的函数特别有用,能够减少内存和性能开销,提高程序的整体性能。
2.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: 42
return 0;
}
示例解析:
在这个示例中,展示了如何使用静态下标运算符来访问和操作静态数组。以下是详细解析:
- 定义类
StaticArray
: -
StaticArray
类包含一个静态成员数组data
,用于存储 10 个整数。- 该数组在类外部初始化,初始值为
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
。
- 定义静态下标运算符
operator[]
: -
operator[]
是一个静态成员函数,接受一个std::size_t
类型的索引参数,并返回数组data
中对应位置的元素引用。- 通过定义静态下标运算符,可以像访问普通数组一样访问和修改静态数组的元素。
- 在
main
函数中使用StaticArray
: -
- 通过
StaticArray::data[3] = 42
语句,使用静态下标运算符访问并修改data
数组的第四个元素,将其值设置为 42。 - 使用
std::cout << StaticArray::data[3] << std::endl;
语句输出data
数组的第四个元素,结果为 42。
- 通过
通过这种方式,C++23 的静态下标运算符使得类的静态数组可以像普通数组一样方便地进行访问和操作。开发者可以利用这一特性编写更加直观和易于理解的代码,同时保持代码的简洁性和高效性。这一特性在需要频繁访问和修改静态数据的场景中尤为有用,提高了代码的可维护性和可读性。
2.12. 类模板参数推导 (Class Template Argument Deduction from Inherited Constructors)
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: 42
return 0;
}
示例解析:
在这个示例中,展示了如何利用类模板参数推导从继承的构造函数中推导模板参数。以下是详细解析:
- 定义模板类
Base
: -
Base
是一个模板类,带有一个类型参数T
,包含一个成员变量value
。- 构造函数
Base(T val)
接受一个T
类型的参数并初始化成员变量value
。
- 定义继承类
Derived
: -
Derived
类继承自Base<int>
,指定T
为int
类型。- 使用
using Base::Base;
语句继承Base
的构造函数,使得Derived
可以直接使用Base
的构造函数进行初始化。
- 在
main
函数中使用Derived
: -
- 创建一个
Derived
类的实例d
,并传递一个整数值42
进行初始化。 - 由于
Derived
继承了Base
的构造函数,编译器能够从传递的参数42
推导出Base
的模板参数T
为int
,并正确调用Base
的构造函数进行初始化。 - 使用
std::cout
输出d.value
的值,结果为42
。
- 创建一个
通过这种方式,C++23 的类模板参数推导特性简化了模板类的使用,使得从继承的构造函数中推导模板参数成为可能。开发者可以利用这一特性编写更加简洁和易于维护的代码,减少了显式指定模板参数的复杂性,特别是在需要频繁创建模板类实例的场景中,提高了代码的可读性和灵活性。
3. 标准库增强
3.1. 字符串格式化改进
C++23 改进了字符串格式化功能,包括支持格式化整个范围的内容。新的格式化功能使得字符串处理更加直观和强大,简化了格式化操作,增强了代码的可读性和维护性。
示例代码:
#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; // C++23将会支持此特性
return 0;
}
示例解析:
在这个示例中,展示了如何使用 C++23 改进的字符串格式化功能来处理和格式化字符串。以下是详细解析:
- 基本字符串格式化:
-
- 定义了两个变量:
name
和age
,分别表示名字和年龄。 - 使用
std::format
函数进行字符串格式化,将name
和age
的值插入到格式字符串"Name: {}, Age: {}"
中。 - 格式化后的字符串存储在
output
变量中,并通过std::cout
输出,结果为"Name: Alice, Age: 30"
。
- 定义了两个变量:
- 格式化整个范围的内容:
-
- 定义了一个
std::vector<int>
类型的变量numbers
,包含一组整数。 - 使用
std::format
函数格式化整个范围的内容,将numbers
的值插入到格式字符串"Numbers: {}"
中。 - 格式化后的字符串存储在
formatted_numbers
变量中,并通过std::cout
输出。由于完整支持范围格式化是 C++23 的新特性,实际运行环境需要支持 C++23 标准,结果为"Numbers: {1, 2, 3, 4, 5}"
。
- 定义了一个
通过这种方式,C++23 的字符串格式化改进使得处理和格式化字符串变得更加方便和高效。开发者可以利用这一新特性编写更加简洁和易于维护的代码,减少了手动拼接字符串的繁琐操作,并提高了代码的可读性和表达力。这对于需要处理大量字符串操作的应用场景,如日志记录、用户界面生成和数据报告等,特别有用。
3.2. 新容器:flat_map
和 flat_set
C++23 提供了性能优化的平面映射 (flat_map
) 和集合 (flat_set
) 容器,这些容器在某些特定场景下可以比传统的 std::map
和 std::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
,其键类型为int
,值类型为std::string
。- 使用初始化列表
{{1, "one"}, {2, "two"}}
对map
进行初始化,存储了两个键值对。
- 添加新的元素:
-
- 使用下标运算符
map[3] = "three"
添加新的键值对3: "three"
到map
中。 - 由于
flat_map
的底层实现是一个排序的连续内存块,因此插入操作可能涉及移动和排序元素,但整体效率通常高于传统的std::map
。
- 使用下标运算符
- 遍历和输出
flat_map
: -
- 使用基于范围的
for
循环遍历map
中的所有键值对。 - 通过解构绑定
const auto& [key, value]
获取每个键值对,并使用std::cout
输出键和值。
- 使用基于范围的
通过这种方式,flat_map
容器提供了比传统 std::map
更高效的性能,特别适用于小型数据集或需要频繁查找操作的场景。开发者可以利用 flat_map
和 flat_set
容器来编写更加高效和简洁的代码,同时减少内存分配和访问时间,提高程序的整体性能。这些新容器在需要优化内存使用和提高缓存命中率的应用中尤为有用。
3.3. 多维视图 (mdspan
)
mdspan
提供了处理多维数组的视图类型,对于科学计算和性能关键的应用非常重要。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
数组,用于存储二维矩阵的数据。
- 创建一个包含 6 个整数的
- 创建
mdspan
视图: -
- 使用
mdspan<int, extents<3, 2>>
创建一个名为matrix
的视图,表示一个 3 行 2 列的矩阵。 mdspan
使用指向data
数组的指针,因此matrix
不持有数据,而只是对现有数据的视图。
- 使用
- 遍历和访问元素:
-
- 使用嵌套的
for
循环遍历矩阵的行和列,通过matrix(i, j)
访问矩阵的元素。 - 输出矩阵的元素,结果为:
- 使用嵌套的
1 2
3 4
5 6
通过这种方式,mdspan
提供了一个高效的多维数组视图,使得科学计算和性能关键的应用可以更方便地管理和操作多维数据。mdspan
不持有数据,而是通过指针和索引访问现有数据,减少了不必要的数据复制和内存分配,提高了访问效率和性能。这对于需要频繁操作多维数组的应用,如数值计算、图像处理和机器学习等,非常有用。
3.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
结构体,表示协程的承诺类型。
- 定义
promise_type
结构体: -
promise_type
结构体包含了生成器协程的核心逻辑。yield_value
方法用于将值v
返回给调用方,并挂起协程,直到下一次恢复执行。initial_suspend
和final_suspend
方法用于控制协程的初始和最终挂起行为。get_return_object
方法返回一个Generator
对象,该对象持有协程句柄。return_void
和unhandled_exception
方法处理协程的返回和异常情况。
- 定义生成器协程
sequence
: -
sequence
是一个生成器协程,从start
开始,每次生成的值增加step
。- 使用
co_yield
关键字将值逐步生成并返回给调用方。
- 在
main
函数中使用生成器: -
- 创建一个
sequence
生成器,起始值为 0,步长为 5。 - 使用
for
循环调用生成器的next
方法生成值,并使用getValue
方法获取当前值。 - 输出生成的值,结果为:
0 5 10 15 20 25 30 35 40 45
。
- 创建一个
通过这种方式,标准生成器协程使得创建惰性序列变得更加容易和高效,开发者可以利用生成器协程按需生成序列元素,而不必一次性生成所有元素。这对于处理大数据集、流数据或需要逐步产生值的应用场景特别有用。
3.5. std::expected
作为错误处理的新方法
std::expected
提供了一种新的错误处理方式,作为异常的更具表达性和类型安全的替代方案。与传统的异常处理机制不同,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
函数: -
safe_divide
函数接受两个整数参数a
和b
,返回一个std::expected<int, std::string>
类型的值。- 如果
b
为 0,表示除零错误,函数返回std::unexpected("Division by zero!")
,其中包含错误信息。 - 否则,函数返回
a / b
的结果。
- 在
main
函数中调用safe_divide
: -
- 调用
safe_divide(10, 2)
,将结果存储在result
变量中。由于b
不为 0,返回成功结果5
。 - 使用
if (result)
检查返回值是否包含成功结果。如果是,输出结果值result.value()
;否则,输出错误信息result.error()
。 - 再次调用
safe_divide(10, 0)
,此时由于b
为 0,返回错误信息"Division by zero!"
。 - 同样使用
if (result)
检查返回值,输出错误信息result.error()
。
- 调用
通过这种方式,std::expected
提供了一种更直观和类型安全的错误处理方法,使得函数调用者能够明确地检查并处理可能的错误情况,而无需依赖异常机制。使用 std::expected
可以提高代码的可读性和维护性,特别是在需要处理多种错误类型或在性能敏感的应用中非常有用。
3.6. 运行时堆栈跟踪
C++23 引入了运行时堆栈跟踪功能,允许开发者在运行时获取堆栈跟踪信息,从而方便调试和错误诊断。通过 std::stacktrace
,开发者可以轻松地打印当前的堆栈跟踪,了解程序执行路径,快速定位问题。
示例代码:
#include <iostream>
#include <stacktrace>
void foo() {
std::cout << std::stacktrace::current() << std::endl;
}
int main() {
foo();
return 0;
}
示例解析:
在这个示例中,展示了如何使用 std::stacktrace
进行运行时堆栈跟踪。以下是详细解析:
- 引入头文件:
-
#include <stacktrace>
引入了堆栈跟踪所需的头文件。
- 定义函数
foo
: -
- 在
foo
函数中,使用std::stacktrace::current()
获取当前的堆栈跟踪信息。 - 将堆栈跟踪信息通过
std::cout
打印出来,以便查看程序的调用路径。
- 在
- 在
main
函数中调用foo
: -
main
函数调用foo
,从而触发foo
中的堆栈跟踪打印操作。- 运行程序时,堆栈跟踪信息会被打印到标准输出,显示出程序的调用路径。
堆栈跟踪示例输出:
foo() at example.cpp:5
main() at example.cpp:10
(实际输出取决于编译器和平台)
通过这种方式,C++23 的运行时堆栈跟踪功能提供了一种方便的方式来获取和查看程序的堆栈信息。这对于调试复杂应用程序、定位错误和分析程序行为非常有用。开发者可以在程序的关键位置添加堆栈跟踪打印,以便在出现问题时快速诊断和解决。
4. 多线程和并发改进
4.1. std::jthread
std::jthread
是一个更易于使用的线程类,自动管理线程的生命周期。当 std::jthread
对象被销毁时,如果线程仍在运行,会自动调用 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() is automatically called at the end of scope
return 0;
}
示例解析:
在这个示例中,展示了如何使用 std::jthread
创建并管理线程。以下是详细解析:
- 定义工作函数
work
: -
work
函数模拟一个简单的工作负载,通过调用std::this_thread::sleep_for(std::chrono::seconds(1))
使线程休眠1秒钟,然后输出"Work done"
。
- 创建
std::jthread
对象: -
- 在
main
函数中,创建一个std::jthread
对象t
,并将work
函数传递给它作为线程的任务。 - 线程
t
开始执行work
函数,进入休眠状态。
- 在
- 自动管理线程生命周期:
-
- 当
main
函数结束时,std::jthread
对象t
超出作用域,其析构函数被调用。 std::jthread
的析构函数会自动调用join()
方法,确保线程t
在程序退出之前完成其工作。- 这避免了手动调用
join()
的麻烦,并确保没有悬挂线程。
- 当
通过这种方式,std::jthread
提供了一种更简单和安全的线程管理方式,自动处理线程的生命周期,减少了错误和资源泄露的风险。这对于多线程编程特别有用,使代码更加简洁和健壮。
4.2. std::stop_token
和 std::stop_source
std::stop_token
和 std::stop_source
提供了一种标准化的机制来请求停止线程操作。std::jthread
可以与 std::stop_token
一起使用,使得线程可以响应停止请求,从而安全地终止线程。
示例代码:
#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
和 std::stop_source
来实现线程的可控停止。以下是详细解析:
- 定义工作函数
work
: -
work
函数接受一个std::stop_token
参数st
,用于检查停止请求。- 在
while
循环中,使用st.stop_requested()
检查是否有停止请求。 - 如果没有停止请求,线程每 100 毫秒输出一次
"Working..."
并继续工作。 - 一旦接收到停止请求,跳出循环并输出
"Stopped."
。
- 创建
std::jthread
对象: -
- 在
main
函数中,创建一个std::jthread
对象t
,并将work
函数传递给它作为线程的任务。 - 线程
t
开始执行work
函数。
- 在
- 发送停止请求:
-
main
函数中,主线程休眠 1 秒钟,以模拟某种条件下发出停止请求的延迟。- 使用
t.request_stop()
向work
线程发送停止请求。 work
线程在检测到停止请求后,跳出while
循环,执行停止逻辑并输出"Stopped."
。
通过这种方式,std::stop_token
和 std::stop_source
提供了一种安全和标准化的机制来请求和响应线程停止,使得线程可以有序和安全地终止。这在需要可控停止线程的多线程应用中非常有用,避免了强制终止线程带来的资源泄漏和不一致状态问题。
4.3. std::latch
和 std::barrier
新增的同步原语 std::latch
和 std::barrier
允许更高效地管理并发任务。std::latch
用于等待一组线程完成工作,而 std::barrier
用于协调一组线程的并行阶段性进展。
示例代码:
#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
来同步多个线程的执行。以下是详细解析:
- 定义工作函数
worker
: -
worker
函数接受一个std::latch&
参数,用于同步线程。- 每个工作线程输出
"Worker is working..."
,然后休眠 1 秒钟以模拟工作负载。 - 调用
latch.count_down()
减少latch
的计数,表示该线程已完成工作。 - 最后输出
"Worker is done."
。
- 创建
std::latch
对象: -
- 在
main
函数中,创建一个std::latch
对象,初始计数为 3,表示需要等待 3 个线程完成工作。
- 在
- 启动工作线程:
-
- 使用
std::vector<std::thread>
创建 3 个线程,每个线程执行worker
函数,并传递latch
引用。 - 每个线程启动后,开始执行
worker
函数。
- 使用
- 等待所有线程完成:
-
- 调用
latch.wait()
,主线程阻塞,直到latch
的计数减为 0,表示所有工作线程已完成工作。 - 输出
"All workers are done."
。
- 调用
- 等待线程结束:
-
- 使用
for
循环调用每个线程的join()
方法,确保所有线程都已终止。
- 使用
通过这种方式,std::latch
提供了一种简单高效的方式来同步多个线程的工作进度。std::latch
非常适合于等待一组线程完成初始化或特定任务,然后再继续后续操作的场景。
使用 std::barrier
: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
来同步线程的并行阶段。以下是详细解析:
- 定义工作函数
phase_work
: -
phase_work
函数接受一个std::barrier&
参数,用于同步线程的并行阶段。- 第一个阶段,输出
"Phase 1 working..."
并休眠 1 秒钟,然后调用sync_point.arrive_and_wait()
同步,等待所有线程完成第一阶段。 - 第二个阶段,输出
"Phase 2 working..."
并休眠 1 秒钟,然后再次调用sync_point.arrive_and_wait()
同步,等待所有线程完成第二阶段。
- 创建
std::barrier
对象: -
- 在
main
函数中,创建一个std::barrier
对象,初始计数为 3,表示需要等待 3 个线程同步。
- 在
- 启动工作线程:
-
- 使用
std::vector<std::thread>
创建 3 个线程,每个线程执行phase_work
函数,并传递sync_point
引用。 - 每个线程启动后,开始执行
phase_work
函数。
- 使用
- 等待线程结束:
-
- 使用
for
循环调用每个线程的join()
方法,确保所有线程都已终止。
- 使用
通过这种方式,std::barrier
提供了一种高效的机制来管理并行计算中的阶段性同步,使得线程可以在每个阶段结束时等待其他线程,然后同时开始下一阶段。
4.4. 任务块和协同任务
通过改进的任务调度机制,协同任务可以更高效地共享资源和时间片,提升并发执行的性能。在 C++23 中,任务块和协同任务使得开发者能够更灵活地管理并发任务,确保资源的高效利用和任务的协调执行。以下是一个示例代码,展示了如何使用任务块和协同任务来实现并发执行。
示例代码:
#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;
}
示例解析:
在这个示例中,我们创建了五个线程,每个线程都执行相同的 work
函数。std::latch
用于确保所有线程在开始工作之前都已经就绪,而 std::barrier
则用于协调线程在完成工作后的同步。
- 初始化
std::latch
和std::barrier
: -
std::latch
的初始计数为线程数,确保所有线程在开始工作之前都已经就绪。std::barrier
的初始计数为线程数加一,因为主线程也参与同步。
- 创建并启动线程:
-
- 每个线程在启动时,都会调用
start_latch.count_down()
来减少计数,表示自己已经准备就绪。 - 线程开始工作前,会等待
start_latch.wait()
,确保所有线程都已启动。
- 每个线程在启动时,都会调用
- 执行任务并同步:
-
- 每个线程在执行完工作负载后,调用
end_barrier.arrive_and_wait()
通知任务完成,并等待其他线程完成。
- 每个线程在执行完工作负载后,调用
- 主线程等待并同步:
-
- 主线程也调用
end_barrier.arrive_and_wait()
,等待所有工作线程完成任务。
- 主线程也调用
- 等待线程结束:
-
- 使用
join()
确保所有线程都已结束执行。
- 使用
通过这种方式,任务块和协同任务机制可以高效地管理并发执行,确保任务的协调和资源的高效利用。
4.5. 执行策略的改进
C++23 增强了标准库中的执行策略,包括并行和异步执行的支持。这些改进使得开发者能够更高效地利用多核处理器,提升程序的性能。通过使用新的执行策略,开发者可以轻松实现并行算法,大幅减少计算时间。
示例代码:
#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec(1000000, 1);
std::transform(std::execution::par, vec.begin(), vec.end(), vec.begin(),
[](int x) { return x * 2; });
std::cout << "Transformation done.\n";
return 0;
}
示例解析:
在这个示例中,使用了 C++23 增强的标准库执行策略之一:std::execution::par
。这使得 std::transform
算法能够并行地执行,从而显著提高了性能。以下是对示例的详细解析:
- 初始化数据:
-
- 创建一个包含 1,000,000 个元素的向量
vec
,每个元素的值为 1。
- 创建一个包含 1,000,000 个元素的向量
- 并行执行转换:
-
- 使用
std::transform
算法,将vec
中的每个元素乘以 2。这里的关键是使用std::execution::par
执行策略,使得算法能够并行执行。 std::execution::par
指示标准库在多个线程上并行执行该算法,从而利用多核处理器的计算能力,减少总体执行时间。
- 使用
- 输出结果:
-
- 转换完成后,输出 "Transformation done.",表示并行操作已完成。
通过这种方式,C++23 的执行策略改进使得并行和异步执行变得更加简单和高效。开发者可以利用这些特性,在不改变算法逻辑的情况下,显著提升计算密集型任务的性能。这些改进对需要处理大量数据或进行复杂计算的应用程序尤其有用,例如图像处理、数据分析和科学计算等领域。
5. 实用工具和功能
5.1. std::print
和 std::println
C++23 引入了新的输出函数 std::print
和 std::println
,简化了输出操作。这些函数相比于传统的 printf
和 std::cout
更加易用,提供了更简洁的语法和格式化能力。
示例代码:
#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");
直接输出字符串"Hello, World!"
并换行。std::print
提供了类似于printf
的功能,但不需要指定格式化类型。
- 使用
std::println
格式化输出: -
std::println("Formatted number: {}", 42);
使用了大括号{}
作为占位符,输出格式化的字符串。- 这里
42
被格式化为字符串插入到占位符{}
位置,输出结果为"Formatted number: 42"
。 std::println
自动在末尾添加换行符,比std::print
更加方便。
通过这种方式,std::print
和 std::println
提供了一种更简洁和易用的输出操作方法。与传统的 printf
和 std::cout
相比,这些函数减少了代码的复杂性,使得格式化输出变得更加直观和高效,特别适合日常开发中的简单输出需求。这些新函数增强了 C++ 标准库的易用性,帮助开发者编写更清晰和可维护的代码。
5.2. Ranges库的更新
C++23 对 Ranges 库进行了许多更新和增强,使其更加强大和灵活,适用于各种集合操作。新增了许多便捷的适配器和算法,以简化代码编写并提高代码可读性和性能。以下是一个示例代码,展示了如何使用这些新功能。
示例代码:
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用新的 ranges 适配器
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;
// 新的 range 算法
bool contains_five = std::ranges::contains(numbers, 5);
std::cout << "Contains 5: " << std::boolalpha << contains_five << std::endl;
return 0;
}
示例解析:
在这个示例中,展示了如何使用 C++23 Ranges 库的更新功能来进行集合操作。以下是详细解析:
- 定义数据集合:
-
- 创建一个包含 10 个整数的
std::vector<int>
类型的变量numbers
,用于存储数据集合。
- 创建一个包含 10 个整数的
- 使用 ranges 适配器:
-
- 使用
std::views::filter
适配器过滤出所有偶数元素。filter
适配器接受一个谓词(lambda 函数[](int n) { return n % 2 == 0; }
),只保留符合条件的元素。 - 使用
std::views::transform
适配器将偶数元素平方。transform
适配器接受一个转换函数(lambda 函数[](int n) { return n * n; }
),对每个元素应用该函数。
- 使用
- 遍历并输出结果:
-
- 使用范围
for
循环遍历squared_numbers
,并输出每个元素的值。结果为4 16 36 64 100
,即2^2 4^2 6^2 8^2 10^2
。
- 使用范围
- 使用 ranges 算法:
-
- 使用
std::ranges::contains
算法检查numbers
集合中是否包含值5
。contains
算法返回一个布尔值。 - 输出检查结果
Contains 5: true
,表示集合中包含值5
。
- 使用
通过这种方式,C++23 Ranges 库的更新使得集合操作更加简洁和高效。开发者可以利用这些新的适配器和算法编写更具可读性和维护性的代码,特别适用于需要对数据集合进行复杂操作的场景。这些增强功能显著提高了代码的表达能力和执行性能。
5.3. 单元操作与改进
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;
}
示例解析:
在这个示例中,展示了如何使用 C++23 对 std::optional
和 std::variant
的增强功能。以下是详细解析:
std::optional
操作:-
std::optional<int> opt = 42;
定义了一个std::optional
类型的变量opt
,并初始化为42
。- 使用
if (opt)
检查opt
是否包含值。如果opt
包含值,则输出该值。 - C++23 增强了
std::optional
的操作,使得检查和访问可选值更加简洁和直观。
std::variant
操作:-
std::variant<int, std::string> var = "Hello, World!";
定义了一个std::variant
类型的变量var
,并初始化为字符串"Hello, World!"
。- 使用
std::get<std::string>(var)
获取var
中的字符串值,并输出。 - 将
var
赋值为整数2023
,再次使用std::get<int>(var)
获取var
中的整数值,并输出。 - C++23 增强了
std::variant
的操作,使得类型安全的访问和修改更加方便。
通过这种方式,C++23 的单元操作与改进使得 std::optional
和 std::variant
的使用更加高效和易于理解。开发者可以利用这些增强功能编写更加简洁和可维护的代码,特别是在需要处理可选值和多态值的场景中,提高了代码的灵活性和可读性。这些改进对实际应用中的数据处理和错误处理提供了更好的支持。
6. 总结
C++23 通过一系列新的语言特性和标准库增强,提高了 C++ 的可用性和开发效率。虽然其变化不如 C++20 那样显著,但它在完善和优化现有功能方面做出了重要贡献,为开发者提供了更加强大和灵活的工具。这些改进使得开发者能够编写出更简洁、高效和安全的代码,有助于提升软件开发的整体质量和效率。
7. 参考资料
- cppreference: C++23 新特性
- C++ Wikipedia: C++23
- Modernes C++: C++23 核心语言改进
本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“AI与编程之窗”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。