C++20 中最优雅的那个小特性 - Ranges
大家好,今天我们来聊聊 C++20 的一项非常重要的新特性——Ranges,可以让你的代码更优雅、更高效、更炫酷,如果你是一个对代码有所追求的小伙伴,那么这个特性你绝对值得拥有!
啥是 Ranges和std::views ?
首先,我们来说说什么是 Range。简单来说,Range 就是一种可以遍历的序列。你可以把它想象成更智能、更灵活的数组或者容器。C++20 引入了 Ranges 这个概念,让我们可以更方便地操作这些序列。
再来说说 std::views,这其实是 C++20 里提供的一系列工具函数,用来对序列进行各种变换。std::views 可以帮我们过滤、转换、拼接等等,让我们以一种非常直观的方式对序列进行操作。
Range和std::views啥关系?
其实,Range和std::views就像是给你一盘水果(Range),然后你拿着各种刀子和工具(std::views)把这些水果处理成你想要的样子。
- Range:数据的集合。
- std::views:处理数据的各种工具,比如过滤、变换、切片等等。
举个例子
假如我们有一个数组,我们想要过滤出其中的偶数,然后再把这些偶数加倍。这在以前需要写挺多代码,但有了 Ranges 和 std::views 后,变得非常简单明了:
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = numbers | std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; });
for (int n : result) {
std::cout << n << ' ';
}
return 0;
}
通过这段代码,我们可以看到,std::views::filter 和 std::views::transform 让我们能一步步地对序列进行处理,代码不仅简洁,而且非常直观,整个过程编码体验真叫一个舒畅啊。
Ranges 与函数式编程
接下来聊聊 Ranges 是怎么有助于函数式编程的。函数式编程的一个核心理念是通过一系列函数的组合来处理数据,而不是通过改变变量的状态。Ranges 恰好和这个理念非常契合。
首先,Ranges 提供了惰性计算的特性,这意味着序列的变换操作在真正需要用到的时候才会执行,不会提前执行。这样做不仅效率高,而且能避免很多不必要的计算。其次,Ranges 让我们可以很自然地把一连串操作用管道的方式串起来。这种方式让代码看起来像流水线一样,每一步都在变换数据。而且每一步的操作都是独立的函数,没有副作用,这和函数式编程的“纯函数”理念非常吻合。
以下将详细探讨 Ranges 和函数式编程之间的关系,以及如何在 C++20 中利用这些新特性来写出更具函数式风格的代码。
1. 声明式代码
在函数式编程中,开发者通常使用声明式代码来描述“做什么”,而不是“怎么做”。C++20 的 Ranges 库通过视图适配器和算法,使代码更具声明性。
示例
传统迭代器代码:
#include <vector>
#include <iostream>
void traditional_example(const std::vector<int>& numbers) {
std::vector<int> result;
for (auto n : numbers) {
if (n % 2 == 0) {
result.push_back(n * n);
}
}
for (auto n : result) {
std::cout << n << ' ';
}
std::cout << std::endl;
}
使用 Ranges 的声明式代码:
#include <vector>
#include <iostream>
#include <ranges>
void ranges_example(const std::vector<int>& numbers) {
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (auto n : result) {
std::cout << n << ' ';
}
std::cout << std::endl;
}
通过 Ranges,代码变得更简洁、更具声明性,且易读性和维护性大大提高。
2. 惰性求值
函数式编程的一个重要概念是惰性求值(Lazy Evaluation)。惰性求值只有在需要时才进行计算,避免不必要的计算和内存消耗。
示例
C++20 的 Ranges 通过视图实现了惰性求值:
#include <iostream>
#include <ranges>
int main() {
auto numbers = std::views::iota(1, 1000000)
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(5); // 仅获取前5个结果
for (auto n : numbers) {
std::cout << n << ' ';
}
std::cout << std::endl;
return 0;
}
在这个例子中,将 iota
生成的一个范围进行筛选、转换和截取,仅在实际使用时才执行这些操作,实现了惰性求值。
4 16 36 64 100
...Program finished with exit code 0
Press ENTER to exit console.
3. 高阶函数
函数式编程中,高阶函数是指可以接受函数作为参数或返回函数的函数。在 C++20 的 Ranges 库中,视图适配器和算法本质上可以看作高阶函数。
示例
#include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
auto even_filter = [](int n) { return n % 2 == 0; };
auto square_transform = [](int n) { return n * n; };
auto result = numbers
| std::views::filter(even_filter)
| std::views::transform(square_transform);
for (auto n : result) {
std::cout << n << ' ';
}
std::cout << std::endl;
return 0;
}
在这个例子中,filter
和 transform
高阶函数接受筛选和转换的函数,并将它们应用于范围上。
4 16 36
...Program finished with exit code 0
Press ENTER to exit console.
4. 不可变性
函数式编程强调数据的不可变性(Immutability)。虽然 C++ 默认是一个可变性较强的语言,但在使用 Ranges 时,通过惰性求值和组合操作,我们可以更接近数据的不可变性。
示例
传统方式:
#include <vector>
#include <algorithm>
#include <iostream>
void traditional_example(std::vector<int>& numbers) {
std::vector<int> result;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(result), [](int n) { return n % 2 == 0; });
std::transform(result.begin(), result.end(), result.begin(), [](int n) { return n * n; });
for (auto n : result) {
std::cout << n << ' ';
}
std::cout << std::endl;
}
使用 Ranges:
#include <vector>
#include <ranges>
#include <iostream>
void ranges_example(const std::vector<int>& numbers) {
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (auto n : result) {
std::cout << n << ' ';
}
std::cout << std::endl;
}
在 Ranges 示例中,输入的 numbers
是不可变的。我们创建了一个新的视图,而不是改变原始数据。
5. 管道操作
函数式编程中一个流行的概念是管道(Pipeline)。它允许将一系列操作按顺序组合起来,形成一个数据处理流水线。
示例
通过 |
管道操作符,Ranges 实现了函数式编程中的管道概念:
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (auto n : result) {
std::cout << n << ' ';
}
std::cout << std::endl;
return 0;
}
这种方式使得代码语义更加清晰,数据流的处理步骤一目了然。讲到这儿,估计很多朋友会好奇了,这个 |
运算符是怎么做到这般神奇的?这就得说到运算符重载了。C++20 的 Pipeline 运算符 |
通过运算符重载,实现了高效、简洁的链式操作。这非常有助于函数式编程,让代码简洁易读,同时让数据变换变得更加纯粹、透明。
标准库支持的视图
C++20 Ranges 库提供了多种视图,可以满足各种序列操作需求,比如:
- std::views::iota:生成一个从某个值开始的无限序列。
- std::views::take:获取视图中的前几个元素。
- std::views::drop:跳过视图中的前几个元素。
- std::views::split:根据特定的分隔符将序列分割成多个子序列。
- std::views::reverse:将序列逆序输出。
- std::views::join:将嵌套范围扁平化。
再来一个扁平化的例子
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<std::vector<int>> nested = { {1, 2}, {3, 4}, {5, 6} };
auto flat_view = nested | std::views::join;
for (auto n : flat_view) {
std::cout << n << ' ';
}
std::cout << std::endl;
}
1 2 3 4 5 6
...Program finished with exit code 0
Press ENTER to exit console.
结语
C++20 的 Ranges 库为 C++ 提供了许多函数式编程的特性,使得代码更加声明性、易读、易维护。通过 Ranges 和 std::views
,开发者可以利用高阶函数、惰性求值、管道操作等函数式编程概念来处理数据流和集合,编写出更高效和优雅的代码。
这些特性不仅增强了 C++ 的表达能力,也让开发者能够以更简洁和自然的方式来进行复杂的数据操作和变换。希望上述内容能帮助你理解和应用 C++20 的 Ranges 库,更好地结合函数式编程理念进行开发。