WEBKT

C++20 Ranges 设计思想与实战案例:如何简化集合操作并提升代码质量?

68 0 0 0

Ranges 的设计思想

Ranges 的核心概念

Ranges 的优势

实战案例:使用 Ranges 简化集合操作

案例 1:过滤偶数并求平方

案例 2:查找第一个大于 5 的偶数

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

Ranges 的性能考量

C++20 Ranges 的局限性

总结

C++20 引入的 Ranges 库,是对标准模板库 (STL) 的一次重大升级,它提供了一种更简洁、更高效的方式来处理集合数据。与传统的迭代器相比,Ranges 允许你以一种声明式的方式来表达你的意图,从而减少了冗余代码,提高了代码的可读性和可维护性。本文将深入探讨 Ranges 的设计思想,并通过实际案例,展示如何使用 Ranges 来简化集合操作,并提升代码质量。

Ranges 的设计思想

Ranges 库的核心思想是 组合 (Composition)延迟计算 (Lazy Evaluation)。它将集合操作分解为一系列小的、可组合的 building blocks,然后将它们组合起来以实现更复杂的功能。这种方法具有以下优点:

  • 可读性: 通过将复杂的操作分解为小的、易于理解的步骤,Ranges 提高了代码的可读性。
  • 可维护性: 由于每个 building block 都是独立的,因此可以更容易地进行测试和维护。
  • 可重用性: building blocks 可以被重用于不同的集合操作,从而减少了代码的重复。
  • 效率: 延迟计算允许 Ranges 仅在需要时才执行计算,从而提高了效率。

Ranges 的核心概念

在深入研究 Ranges 的使用之前,我们需要了解几个核心概念:

  • Range: 一个 Range 是一个可以被迭代的对象。它类似于 STL 中的容器,但更加通用。例如,一个数组、一个 std::vector 或一个自定义的迭代器序列都可以被视为一个 Range。
  • View: 一个 View 是一个轻量级的 Range,它提供了一个 Range 的视图,而无需复制底层数据。View 可以被用来过滤、转换或组合 Range。
  • Algorithm: 类似于 STL 中的算法,Ranges 库提供了一系列算法,可以用来操作 Range。不同之处在于,Ranges 算法通常接受 Range 作为输入,并返回一个新的 Range 或 View 作为结果。

Ranges 的优势

相比传统的 STL 迭代器,Ranges 具有以下显著优势:

  • 简洁性: 使用 Ranges 可以用更少的代码实现相同的集合操作。例如,使用 Ranges 可以一行代码实现过滤、转换和排序操作,而使用迭代器可能需要多行代码。
  • 可读性: Ranges 的声明式风格使代码更易于阅读和理解。通过将操作链接在一起,可以清晰地表达你的意图。
  • 安全性: Ranges 可以防止迭代器失效等常见错误。由于 Ranges 算法通常返回新的 Range 或 View,因此可以避免修改原始数据。
  • 性能: Ranges 库经过了优化,可以提供与传统迭代器相当甚至更好的性能。延迟计算可以避免不必要的计算,从而提高效率。

实战案例:使用 Ranges 简化集合操作

为了更好地理解 Ranges 的用法,我们将通过几个实际案例来展示如何使用 Ranges 简化集合操作。

案例 1:过滤偶数并求平方

假设我们有一个整数向量,我们想要过滤出其中的偶数,然后计算它们的平方。使用传统的迭代器,我们需要编写以下代码:

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> even_squares;
for (int number : numbers) {
if (number % 2 == 0) {
even_squares.push_back(number * number);
}
}
for (int square : even_squares) {
std::cout << square << " ";
}
std::cout << std::endl;
return 0;
}

这段代码虽然可以实现目标,但显得比较冗长。使用 Ranges,我们可以用更简洁的方式实现相同的功能:

#include <iostream>
#include <vector>
#include <range/v3/all.hpp> // 引入 ranges-v3 库
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even_squares = numbers | ranges::views::filter([](int n){ return n % 2 == 0; }) | ranges::views::transform([](int n){ return n * n; });
for (int square : even_squares) {
std::cout << square << " ";
}
std::cout << std::endl;
return 0;
}

这段代码使用了 Ranges 的 views::filterviews::transform 来过滤偶数并计算平方。| 运算符用于将这些操作链接在一起,形成一个管道 (Pipeline)。代码的可读性大大提高,并且更加简洁。

案例 2:查找第一个大于 5 的偶数

假设我们想要在一个整数向量中查找第一个大于 5 的偶数。使用传统的迭代器,我们需要编写以下代码:

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 6, 8, 5, 7, 9, 10};
auto it = std::find_if(numbers.begin(), numbers.end(), [](int n){ return n > 5 && n % 2 == 0; });
if (it != numbers.end()) {
std::cout << "Found: " << *it << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}

使用 Ranges,我们可以用更简洁的方式实现相同的功能:

#include <iostream>
#include <vector>
#include <range/v3/all.hpp>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 6, 8, 5, 7, 9, 10};
auto result = numbers | ranges::views::filter([](int n){ return n > 5 && n % 2 == 0; }) | ranges::views::take(1);
if (!ranges::empty(result)) {
std::cout << "Found: " << *result.begin() << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}

这段代码使用了 Ranges 的 views::filterviews::take 来过滤并获取第一个元素。views::take(1) 保证了最多只取一个元素,这在找到第一个符合条件的元素后可以提前结束计算,提高了效率。

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

假设我们有一个字符串,我们想要统计其中每个单词的出现次数。这是一个更复杂的案例,可以更好地展示 Ranges 的强大功能。首先,我们需要一个函数来将字符串分割成单词:

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
std::vector<std::string> split_string(const std::string& str) {
std::vector<std::string> words;
std::stringstream ss(str);
std::string word;
while (ss >> word) {
words.push_back(word);
}
return words;
}

然后,我们可以使用 Ranges 来统计单词的出现次数:

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <range/v3/all.hpp>
std::vector<std::string> split_string(const std::string& str); // 声明
int main() {
std::string text = "This is a test string. This string is a test.";
auto words = split_string(text);
auto word_counts = words
| ranges::views::group_by(std::equal_to<>())
| ranges::views::transform([](auto group){ return std::make_pair(*group.begin(), ranges::distance(group)); })
| ranges::to<std::map<std::string, int>>();
for (const auto& [word, count] : word_counts) {
std::cout << word << ": " << count << std::endl;
}
return 0;
}

这段代码使用了 Ranges 的 views::group_byviews::transform 来对单词进行分组和计数。views::group_by(std::equal_to<>()) 将相同的单词分组在一起,然后 views::transform 将每个组转换为一个键值对,其中键是单词,值是该单词在组中出现的次数。最后,ranges::to<std::map<std::string, int>>(); 将结果转换为一个 std::map,方便后续访问。

Ranges 的性能考量

虽然 Ranges 提供了许多优点,但在性能方面也需要注意一些问题。Ranges 的延迟计算特性可能会导致一些意想不到的性能问题。例如,如果一个 View 被多次迭代,那么它的计算也会被多次执行。为了避免这种情况,我们可以使用 ranges::to 将 View 转换为一个具体的容器,例如 std::vectorstd::list

另外,Ranges 的算法通常需要分配额外的内存来存储中间结果。这可能会导致性能下降,特别是在处理大型数据集时。为了减少内存分配,我们可以尽量使用 in-place 的算法,例如 ranges::actions::sortranges::actions::remove_if

C++20 Ranges 的局限性

虽然 C++20 引入了 Ranges,但标准库中提供的 Ranges 算法和 View 仍然有限。很多常用的算法和 View 还没有被标准化。例如,目前还没有标准的 views::split 来分割字符串。为了弥补这些不足,我们可以使用第三方库,例如 range-v3range-v3 是一个非常流行的 Ranges 库,它提供了比标准库更多的算法和 View。上面示例代码也使用了 range-v3 库。可以通过包管理器安装,例如在 Ubuntu 上使用 sudo apt-get install librange-v3-dev 安装。

总结

C++20 Ranges 库为集合操作提供了一种更简洁、更高效的方式。通过将集合操作分解为小的、可组合的 building blocks,Ranges 提高了代码的可读性、可维护性和可重用性。虽然 Ranges 在性能方面需要注意一些问题,但它仍然是 C++ 开发者的一个非常有用的工具。掌握 Ranges 的使用,可以帮助你编写更清晰、更高效的代码。希望本文能够帮助你理解 Ranges 的设计思想,并开始在你的项目中使用 Ranges。

当然,学习 Ranges 需要一个过程,刚开始可能会觉得比较抽象。建议多阅读 Ranges 的相关文档和示例代码,并尝试在自己的项目中应用 Ranges。相信通过不断的实践,你一定能够掌握 Ranges 的精髓,并将其应用到实际工作中,提高你的代码质量和开发效率。

码农张三 C++20Ranges集合操作

评论点评

打赏赞助
sponsor

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

分享

QRcode

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