C++20新特征的一些总结
Ranges 库是对标准模板库(STL)的一个重要扩展。它重新定义了容器和算法的交互方式,使代码更具可读性和表达力。Ranges 库的设计目标是减少代码中复杂的迭代器操作,使程序员能够以声明式风格操作序列数据。三向比较运算符,俗称“太空飞船运算符”(),是 C++20 引入的一种新型比较运算符。它简化了多重比较运算的实现过程,自动生成 ==、!=、= 等运算符。协程是一种特殊的函数,允许在执行过程
C++20 是 C++ 语言发展中的一个重要里程碑,引入了众多新特性,这些特性不仅提升了语言的功能性和性能,还极大地简化了许多编程任务。以下将详细介绍 C++20 的一些关键新特性,包括语法和实际使用示例。
1.Ranges 库
1.1. 概述
Ranges 库是对标准模板库(STL)的一个重要扩展。它重新定义了容器和算法的交互方式,使代码更具可读性和表达力。Ranges 库的设计目标是减少代码中复杂的迭代器操作,使程序员能够以声明式风格操作序列数据。
1.2. 核心组件
views:提供视图(views)以惰性评估(lazy evaluation)的方式对序列进行操作。
actions:用于直接修改序列的内容,类似于视图,但是立即执行的。
adaptors:可以将多个视图组合在一起,例如 filter 和 transform。
1.3. 使用示例
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 使用 ranges 进行惰性计算:过滤和变换
auto even_squares = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// 遍历输出结果
for (int n : even_squares) {
std::cout << n << " "; // 输出:4 16 36 64
}
}
分析:
views::filter:过滤序列中的元素,仅保留符合条件的元素。
views::transform:将保留的元素进行转换操作,在这里进行了平方运算。
惰性计算:此操作不会立即对所有元素进行过滤和转换,只有在访问元素时才会计算结果,节省了不必要的开销。
2. 三向比较运算符(<=>)
2.1. 概述
三向比较运算符,俗称“太空飞船运算符”(<=>),是 C++20 引入的一种新型比较运算符。它简化了多重比较运算的实现过程,自动生成 ==、!=、<、<=、>、>= 等运算符。
2.2. 使用示例
#include <iostream>
#include <compare>
struct Point {
int x, y;
// 自动生成所有比较运算符
auto operator<=>(const Point&) const = default;
};
int main() {
Point p1 = {1, 2};
Point p2 = {1, 3};
if (p1 < p2) {
std::cout << "p1 is less than p2\n";
}
if (p1 != p2) {
std::cout << "p1 is not equal to p2\n";
}
}
输出:
p1 is less than p2
p1 is not equal to p2
分析:
<=> 运算符:在结构体 Point 中引入此运算符后,编译器会自动生成所有比较运算符的实现,减少了代码冗余。
默认实现:通过 = default 关键字,编译器将生成默认的三向比较逻辑。
3. Coroutines(协程)
3.1. 概述
协程是一种特殊的函数,允许在执行过程中暂停并在稍后恢复。C++20 原生支持协程,使异步编程和生成器的实现更加简洁和高效。与传统的多线程不同,协程通过协作式多任务处理减少了上下文切换的开销。
3.2. 核心组件
co_await:用于等待异步操作的完成。
co_yield:用于生成一个值并暂停协程。
co_return:用于返回结果并结束协程。
3.3. 使用示例
#include <iostream>
#include <coroutine>
struct Generator {
struct promise_type {
int current_value;
auto get_return_object() {
return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
auto yield_value(int value) {
current_value = value;
return std::suspend_always{};
}
void return_void() {}
void unhandled_exception() { std::exit(1); }
};
std::coroutine_handle<promise_type> handle;
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { handle.destroy(); }
bool next() {
handle.resume();
return not handle.done();
}
int value() const { return handle.promise().current_value; }
};
Generator counter(int n) {
for (int i = 1; i <= n; ++i) {
co_yield i;
}
}
int main() {
auto gen = counter(5);
while (gen.next()) {
std::cout << gen.value() << " ";
}
}
输出:
1 2 3 4 5
分析:
co_yield:在每次循环中,co_yield 会暂停协程并返回当前值,直到协程被再次恢复。
promise_type:定义了协程的行为,包括如何暂停和恢复,以及如何处理返回值和异常。
std::coroutine_handle:用于控制协程的执行,如恢复、销毁等。
4. Concepts(概念)
4.1. 概述
Concepts 是 C++20 引入的一种用于约束模板参数的新特性。Concepts 通过编译时检查,确保模板参数满足某些条件,从而使模板代码更加安全和易于调试。它是 C++ 模板编程的重要增强,使得模板的使用更加简洁和清晰。
4.2. 核心组件
概念定义:通过 concept 关键字定义概念,可以用来约束模板参数。
约束模板参数:使用概念来限制模板参数的类型或行为。
4.3. 使用示例
#include <iostream>
#include <concepts>
// 定义一个概念
template<typename T>
concept Integral = std::is_integral_v<T>;
template<Integral T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << std::endl; // 正常编译
// std::cout << add(1.0, 2.0) << std::endl; // 编译错误
}
分析:
Integral 概念:通过 std::is_integral_v 定义,确保模板参数 T 是整型。
概念约束:add 函数只能接受符合 Integral 概念的类型作为模板参数。如果尝试传递不符合条件的类型(如浮点数),编译器会给出清晰的错误信息。
5. 模块(Modules)
5.1. 概述
模块是 C++20 中引入的一种全新的代码组织方式,用于替代传统的头文件。模块提供了一种更加现代和高效的方式来管理项目的依赖关系,减少编译时间,并通过清晰的接口定义提高代码的可维护性。
5.2. 模块的定义和导入
模块定义:使用 export module 关键字定义模块。
模块导入:使用 import 关键字导入模块。
5.3. 使用示例
定义模块(math.ixx 文件):
// math.ixx
export module math;
export int add(int a, int b) {
return a + b;
}
使用模块:
import math;
int main() {
int result = add(2, 3);
std::cout << result << std::endl; // 输出: 5
}
分析:
模块化设计:模块将接口与实现分离,使得代码更易管理,同时避免了传统头文件中可能存在的多次定义问题。
编译优化:模块只需编译一次,能够显著减少大型项目的编译时间。
6. constexpr 动态内存分配
6.1. 概述
C++20 扩展了 constexpr 的功能,使其可以在编译时进行动态内存分配。这一特性增强了编译时计算的能力,允许开发者在编译期构建复杂的数据结构。
6.2. 使用示例
#include <iostream>
constexpr int* create_array(int size) {
return new int[size]; // 允许在 constexpr 函数中使用动态分配
}
constexpr int array_sum(int* arr, int size) {
int sum = 0;
for (int i = 0; i < size; ++i) {
sum += arr[i];
}
return sum;
}
int main() {
constexpr int size = 5;
constexpr int* arr = create_array(size);
for (int i = 0; i < size; ++i) {
arr[i] = i + 1;
}
constexpr int sum = array_sum(arr, size);
std::cout << "Sum: " << sum << std::endl; // 输出: Sum: 15
delete[] arr;
}
分析:
编译时内存分配:create_array 函数在编译期分配内存,并将结果作为 constexpr 值。
编译时计算:array_sum 函数在编译期计算数组元素的和,极大地提高了代码的执行效率。
7. std::span
7.1. 概述
std::span 是一个轻量级的视图类型,表示一段连续内存的子集。它类似于指针和数组,但更安全、更易用。std::span 提供了与数组或容器类似的接口,但不负责管理内存,因此可以用于多个不同的数据源。
7.2. 使用示例
#include <iostream>
#include <span>
#include <vector>
void print_span(std::span<int> s) {
for (int n : s) {
std::cout << n << " ";
}
std::cout << std::endl;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> vec = {6, 7, 8, 9, 10};
print_span(arr); // 使用数组初始化 std::span
print_span(vec); // 使用 vector 初始化 std::span
}
输出:
1 2 3 4 5
6 7 8 9 10
分析:
安全性:std::span 提供了类似指针的接口,但不会产生指针的安全问题(如越界访问)。
灵活性:std::span 可以用于数组、std::vector 等不同的数据结构,而无需拷贝数据。
8. constinit 关键字
8.1. 概述
constinit 关键字用于保证变量在程序启动时是静态初始化的。与 constexpr 和 const 关键字不同,constinit 用于确保变量在编译期是静态初始化的,避免在多线程环境中可能发生的重复初始化问题。
8.2. 使用示例
#include <iostream>
constinit int x = 42;
int main() {
std::cout << x << std::endl; // 输出: 42
}
分析:
静态初始化保证:constinit 确保变量在程序启动时被静态初始化,从而避免了在多线程环境中可能出现的重复初始化问题。
编译期检查:如果 constinit 变量没有被静态初始化,编译器会给出错误提示。
9. Lambda 表达式的增强
9.1. 概述
C++20 对 Lambda 表达式进行了若干增强,使其更加灵活和强大。主要包括捕获列表中的 this 和模板 Lambda。
9.2. 使用示例
模板 Lambda:
#include <iostream>
#include <vector>
int main() {
auto add = [](auto a, auto b) { return a + b; };
std::cout << add(1, 2) << std::endl; // 输出: 3
std::cout << add(1.5, 2.3) << std::endl; // 输出: 3.8
}
捕获 this:
#include <iostream>
struct S {
int value = 42;
void print() {
auto lambda = [*this] { std::cout << value << std::endl; };
lambda();
}
};
int main() {
S s;
s.print(); // 输出: 42
}
分析:
模板 Lambda:允许 Lambda 表达式接受不同类型的参数,极大地增强了 Lambda 的通用性。
捕获 this:使用 [*this] 捕获当前对象的拷贝,而不是引用,避免了 Lambda 中出现悬空引用的问题。
10. std::jthread
10.1. 概述
std::jthread 是 C++20 引入的新线程类,它与 std::thread 类似,但自动管理线程的生命周期。std::jthread 提供了一种更安全的方式来处理线程,当 std::jthread 对象销毁时,它会自动加入(join)线程。
10.2. 使用示例
#include <iostream>
#include <thread>
#include <chrono>
void task() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Task finished\n";
}
int main() {
std::jthread t(task); // std::jthread 会自动管理线程的生命周期
std::cout << "Main thread\n";
} // t 作用域结束时自动 join
输出:
Main thread
Task finished
分析:
生命周期管理:std::jthread 在作用域结束时自动调用 join,避免了可能导致程序崩溃的未 join 线程。
可取消性:std::jthread 还支持通过传递 stop_token 来取消正在执行的线程操作。
总结
C++20 是 C++ 标准的一次重大升级,它引入了诸如 Ranges 库、协程、概念、模块和三向比较运算符等众多新特性。这些特性极大地增强了语言的表达能力,使代码更简洁、更高效、更易于维护。通过合理利用这些新特性,开发者可以编写出更具现代感的 C++ 代码,同时提高程序的性能和安全性。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)