WEBKT

C++20 Ranges? 优势、劣势与高效代码之道

42 0 0 0

Ranges 究竟是什么?

Ranges 的优势

Ranges 的劣势

如何使用 Ranges 编写更高效的代码?

Ranges 实战案例

总结

作为一名老 C++ 选手,我最初听到 “Ranges” 这个概念时,内心是抗拒的。STL 已经用了这么多年,迭代器也算是老朋友了,突然冒出来个 Ranges,还要改变我的编码习惯?但深入了解后,我发现 Ranges 并非单纯的新概念,而是对 STL 的一次重大升级,它确实能让代码更简洁、更高效,甚至更安全。

Ranges 究竟是什么?

简单来说,Ranges 是对 STL 迭代器概念的泛化和增强。它将算法操作的关注点从迭代器对(begin 和 end)提升到了范围(range)本身。这个范围可以是 STL 容器的一部分,也可以是自定义的数据集合。

想象一下,你有一个 std::vector<int> numbers = {1, 2, 3, 4, 5};,你想筛选出其中所有的偶数。使用传统的 STL 方式,你需要这样写:

std::vector<int> evens;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(evens),
[](int n){ return n % 2 == 0; });

而使用 Ranges,你可以这样写:

#include <iostream>
#include <vector>
#include <algorithm>
#include <range/v3/all.hpp> // 引入 Ranges 库,这里使用的是 range-v3
using namespace ranges;
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto evens = numbers | view::filter([](int n){ return n % 2 == 0; }) | to<std::vector<int>>();
for (int even : evens) {
std::cout << even << " "; // 输出 2 4
}
std::cout << std::endl;
return 0;
}

可以看到,Ranges 使用了一种链式调用的方式,更加直观地表达了数据处理的流程:先从 numbersfilter (筛选)出偶数,然后将结果 to (转换)成 std::vector<int>

核心概念:

  • View: View 是 Ranges 的核心概念,它是一种轻量级的、可组合的范围适配器。View 不拥有数据,而是对现有范围进行转换或过滤,并且这些操作是延迟执行的(lazy evaluation)。上面例子中的 view::filter 就是一个 View。
  • Action: Action 是执行实际操作的函数,例如 to<std::vector<int>>(),它会将 View 的结果物化(materialize)到一个容器中。

Ranges 的优势

  1. 代码更简洁易读: Ranges 使用链式调用,将数据处理的流程以更自然的方式表达出来,避免了传统 STL 中大量的迭代器操作,代码可读性大大提高。
  2. 更高的效率: Ranges 的 View 是延迟执行的,这意味着只有在真正需要结果时才会进行计算。这可以避免不必要的计算,提高程序效率。此外,Ranges 库通常会对算法进行优化,例如使用 SIMD 指令等。
  3. 更强的组合性: Ranges 的 View 可以自由组合,构建复杂的数据处理流水线。例如,你可以先 filter,再 transform,再 sort,最后 take 前几个元素。这种组合性使得 Ranges 能够灵活应对各种数据处理需求。
  4. 更安全的代码: Ranges 避免了手动管理迭代器,减少了迭代器失效的风险。此外,Ranges 库通常会对输入范围进行检查,防止越界访问等错误。
  5. 编译期优化潜力: Ranges 的设计允许编译器进行更多的优化,例如函数内联、循环展开等。在某些情况下,Ranges 代码的性能甚至可以超过手写的 C++ 代码。

Ranges 的劣势

  1. 学习曲线: Ranges 引入了一些新的概念,例如 View、Action 等,需要一定的学习成本。特别是对于已经习惯了传统 STL 的开发者来说,需要改变编码习惯。
  2. 编译时间: Ranges 库通常使用大量的模板,这会导致编译时间变长。特别是对于大型项目来说,编译时间可能会成为一个问题。
  3. 调试难度: Ranges 的链式调用虽然简洁,但也可能增加调试难度。当程序出现问题时,需要逐个检查 View 的执行结果,才能找到问题的根源。
  4. 库的依赖: 虽然 Ranges 已经纳入 C++20 标准,但目前各个编译器的支持程度不一。在实际项目中,可能需要引入第三方 Ranges 库,例如 range-v3。这会增加项目的依赖。
  5. 标准库支持的完善程度: C++20 标准库中的 Ranges 支持目前还不够完善,很多常用的算法和 View 还没有提供。这可能需要在第三方库中寻找解决方案。

如何使用 Ranges 编写更高效的代码?

  1. 选择合适的 View: Ranges 提供了大量的 View,可以用于各种数据处理任务。选择合适的 View 可以简化代码,提高效率。例如,可以使用 view::filter 筛选数据,使用 view::transform 转换数据,使用 view::take 获取前几个元素,使用 view::drop 丢弃前几个元素等等。
  2. 避免不必要的拷贝: Ranges 的 View 是轻量级的,不会拷贝数据。但是,如果将 View 的结果物化到一个容器中,就会发生拷贝。为了避免不必要的拷贝,可以使用 view::all 将容器转换为 View,然后直接在 View 上进行操作。
  3. 利用延迟执行: Ranges 的 View 是延迟执行的,这意味着只有在真正需要结果时才会进行计算。可以利用这个特性,将多个 View 组合在一起,形成一个复杂的数据处理流水线。只有在流水线的末端需要结果时,才会触发整个流水线的执行。
  4. 使用 Ranges 提供的算法: Ranges 提供了很多算法,可以用于各种数据处理任务。这些算法通常都经过优化,性能比手写的 C++ 代码更好。例如,可以使用 ranges::sort 排序数据,使用 ranges::copy 拷贝数据,使用 ranges::for_each 遍历数据等等。
  5. 自定义 View: 如果 Ranges 提供的 View 不能满足需求,可以自定义 View。自定义 View 需要实现 rangeview 两个概念,并提供相应的迭代器。自定义 View 可以将复杂的数据处理逻辑封装起来,提高代码的可重用性。
  6. 充分利用编译期优化: Ranges 的设计允许编译器进行更多的优化。为了充分利用编译期优化,可以使用 constexprconsteval 等关键字,将数据处理逻辑放到编译期执行。这可以提高程序的性能,减少运行时的开销。

Ranges 实战案例

案例 1:统计字符串中每个字符出现的次数

#include <iostream>
#include <string>
#include <map>
#include <range/v3/all.hpp>
using namespace ranges;
int main() {
std::string text = "hello world";
auto char_counts = text
| view::group_by(std::equal_to<>{})
| view::transform([](auto group){ return std::make_pair(ranges::front(group), ranges::distance(group)); })
| to<std::map<char, int>>();
for (auto const& [c, count] : char_counts) {
std::cout << c << ": " << count << std::endl;
}
return 0;
}

代码解释:

  1. text | view::group_by(std::equal_to<>{}): 将字符串按照字符进行分组,相同的字符会被分到同一组。
  2. view::transform([](auto group){ return std::make_pair(ranges::front(group), ranges::distance(group)); }): 将每个分组转换为一个 std::pair,其中 first 是字符,second 是该字符出现的次数。
  3. to<std::map<char, int>>: 将结果转换为 std::map<char, int>,方便后续的访问。

案例 2:从文件中读取数据,并计算平均值

#include <iostream>
#include <fstream>
#include <string>
#include <numeric>
#include <range/v3/all.hpp>
using namespace ranges;
int main() {
std::ifstream file("data.txt");
if (!file.is_open()) {
std::cerr << "Failed to open file" << std::endl;
return 1;
}
auto numbers = istream<int>(file) | to<std::vector<int>>();
if (numbers.empty()) {
std::cerr << "No numbers found in file" << std::endl;
return 1;
}
double average = std::accumulate(numbers.begin(), numbers.end(), 0.0) / numbers.size();
std::cout << "Average: " << average << std::endl;
return 0;
}

代码解释:

  1. istream<int>(file): 创建一个 istream_range,用于从文件中读取整数。
  2. to<std::vector<int>>: 将读取到的整数转换为 std::vector<int>
  3. std::accumulate(numbers.begin(), numbers.end(), 0.0) / numbers.size(): 计算平均值。

注意: 这个例子使用了传统的 std::accumulate 来计算平均值,因为 Ranges 并没有提供直接计算平均值的算法。但你可以自定义一个 View 来实现这个功能。

案例 3:过滤掉字符串向量中的空字符串并转换为大写

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <range/v3/all.hpp>
using namespace ranges;
int main() {
std::vector<std::string> strings = {"hello", "", "world", "", "c++"};
auto upper_case_strings = strings
| view::filter([](const std::string& s){ return !s.empty(); })
| view::transform([](std::string s) {
std::transform(s.begin(), s.end(), s.begin(), ::toupper);
return s;
})
| to<std::vector<std::string>>();
for (const auto& str : upper_case_strings) {
std::cout << str << " "; // 输出 HELLO WORLD C++
}
std::cout << std::endl;
return 0;
}

代码解释:

  1. view::filter([](const std::string& s){ return !s.empty(); }): 过滤掉空字符串。
  2. view::transform([](std::string s) { ... }): 将剩余的字符串转换为大写。
  3. to<std::vector<std::string>>: 将结果收集到新的字符串向量中。

总结

C++20 Ranges 是一项强大的新特性,它可以让代码更简洁、更高效、更安全。虽然学习 Ranges 需要一定的成本,但它带来的好处是显而易见的。在实际项目中,可以根据具体情况选择是否使用 Ranges。对于一些复杂的数据处理任务,Ranges 可以大大提高开发效率。而对于一些性能要求极高的任务,可以考虑使用 Ranges 提供的算法和编译期优化技术。

作为一名 C++ 开发者,我强烈建议你学习和掌握 Ranges。相信在不久的将来,Ranges 将会成为 C++ 开发的标配。拥抱 Ranges,让你的 C++ 代码焕发新的活力!

最后的建议:

  • 从简单的例子开始,逐步掌握 Ranges 的基本概念和用法。
  • 多阅读 Ranges 相关的文档和代码,了解 Ranges 的设计思想和实现细节。
  • 在实际项目中尝试使用 Ranges,积累经验,发现问题,解决问题。
  • 关注 Ranges 的最新发展,及时了解新的特性和改进。

希望这篇文章能够帮助你更好地理解和使用 C++20 Ranges。Happy coding!

代码老司机 C++20RangesSTL

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9276