WEBKT

C++20 Ranges 深度解析:原理、应用与实战技巧,让容器操作更丝滑

60 0 0 0

1. Ranges 的核心概念:从迭代器到 Range

2. Views:数据转换的瑞士军刀

3. Range Adaptors:范围的组合与修改

4. Algorithms:更简洁的算法调用

5. 实战案例:使用 Ranges 解决实际问题

6. Ranges 的注意事项

7. 总结

C++20 引入的 Ranges 库,无疑是现代 C++ 编程的一大利器。它以一种更加简洁、易读的方式处理容器和算法,极大地提高了代码的可维护性和开发效率。如果你已经熟悉 C++ STL 的基本使用,并且渴望了解 C++20 函数式编程的魅力,那么本文将带你深入探索 Ranges 的奥秘,让你在实际项目中游刃有余。

1. Ranges 的核心概念:从迭代器到 Range

在深入细节之前,我们需要理解 Ranges 的核心思想。传统 STL 算法通常接受一对迭代器作为参数,表示操作的范围。而 Ranges 则将这个范围的概念抽象成了一个对象,也就是 range。这个 range 对象封装了起始和结束位置的信息,使得算法可以更加方便地操作整个范围。

为什么需要 Range?

考虑以下代码,使用传统 STL 算法 std::transform 将一个 vector 中的元素平方:

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squares(numbers.size());
std::transform(numbers.begin(), numbers.end(), squares.begin(), [](int x) { return x * x; });
for (int square : squares) {
std::cout << square << " ";
}
std::cout << std::endl; // Output: 1 4 9 16 25
return 0;
}

虽然这段代码功能明确,但存在一些潜在的问题:

  • 冗长: 需要显式地传递 numbers.begin()numbers.end() 两个迭代器。
  • 容易出错: 如果 squares 的大小与 numbers 不一致,可能会导致越界访问。
  • 可读性差: 算法的意图不够清晰,需要仔细阅读代码才能理解其作用。

使用 Ranges,我们可以将代码改写为:

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squares(numbers.size());
std::ranges::transform(numbers, squares.begin(), [](int x) { return x * x; });
for (int square : squares) {
std::cout << square << " ";
}
std::cout << std::endl; // Output: 1 4 9 16 25
return 0;
}

这段代码更加简洁,也更易于理解。std::ranges::transform 直接接受 numbers 作为参数,避免了显式传递迭代器的麻烦。编译器可以自动推导出范围的起始和结束位置,减少了出错的可能性。

Range 的分类

Ranges 可以分为以下几种类型:

  • 视图 (Views): 视图是对现有范围的非拥有式 (non-owning) 转换。它们不会修改原始数据,而是生成一个新的范围,表示原始数据的某种变换。例如,std::views::transform 可以将一个范围中的元素进行变换,std::views::filter 可以过滤掉不满足条件的元素。
  • 范围适配器 (Range Adaptors): 范围适配器是用于组合和修改范围的工具。它们可以链接多个视图,或者将一个范围转换为另一种类型。例如,std::views::take 可以从一个范围中取出前 N 个元素,std::views::drop 可以丢弃前 N 个元素。
  • 算法 (Algorithms): Ranges 库提供了许多新的算法,这些算法可以直接操作 range 对象,而无需显式地传递迭代器。

2. Views:数据转换的瑞士军刀

Views 是 Ranges 库中最强大的工具之一。它们提供了一种延迟计算 (lazy evaluation) 的机制,可以高效地处理大型数据集。这意味着 Views 只在需要时才计算结果,避免了不必要的内存分配和计算开销。

常用的 Views

  • std::views::transform:对范围内的每个元素应用一个函数,生成一个新的范围。例如:

    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    auto squares = numbers | std::views::transform([](int x) { return x * x; });
    for (int square : squares) {
    std::cout << square << " ";
    }
    std::cout << std::endl; // Output: 1 4 9 16 25
    return 0;
    }

    这里使用了管道操作符 |,将 numbers 传递给 std::views::transform。这种写法更加简洁,也更易于阅读。

  • std::views::filter:根据指定的条件过滤范围内的元素,生成一个新的范围。例如:

    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    auto even_numbers = numbers | std::views::filter([](int x) { return x % 2 == 0; });
    for (int number : even_numbers) {
    std::cout << number << " ";
    }
    std::cout << std::endl; // Output: 2 4 6
    return 0;
    }

    这段代码过滤掉了 numbers 中所有的奇数,只保留了偶数。

  • std::views::take:从范围的开头取出指定数量的元素。例如:

    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    auto first_three = numbers | std::views::take(3);
    for (int number : first_three) {
    std::cout << number << " ";
    }
    std::cout << std::endl; // Output: 1 2 3
    return 0;
    }

    这段代码只取出了 numbers 中的前三个元素。

  • std::views::drop:从范围的开头丢弃指定数量的元素。例如:

    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    auto last_two = numbers | std::views::drop(3);
    for (int number : last_two) {
    std::cout << number << " ";
    }
    std::cout << std::endl; // Output: 4 5
    return 0;
    }

    这段代码丢弃了 numbers 中的前三个元素,只保留了最后两个元素。

  • std::views::iota:生成一个递增的整数序列。例如:

    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    auto numbers = std::views::iota(1, 6); // Generates the sequence 1, 2, 3, 4, 5
    for (int number : numbers) {
    std::cout << number << " ";
    }
    std::cout << std::endl; // Output: 1 2 3 4 5
    return 0;
    }

    std::views::iota 可以方便地生成指定范围内的整数序列。

Views 的组合

Views 最强大的地方在于它们可以组合使用,形成复杂的数据处理流水线。例如,我们可以将 std::views::transformstd::views::filter 组合起来,先对数据进行变换,然后再进行过滤:

#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
auto even_squares = numbers
| std::views::transform([](int x) { return x * x; })
| std::views::filter([](int x) { return x % 2 == 0; });
for (int square : even_squares) {
std::cout << square << " ";
}
std::cout << std::endl; // Output: 4 16 36
return 0;
}

这段代码首先将 numbers 中的元素平方,然后过滤掉所有的奇数,只保留偶数的平方。

延迟计算的优势

由于 Views 采用延迟计算的机制,因此只有在实际访问元素时才会进行计算。这意味着我们可以构建非常复杂的 Views 链,而不会产生额外的性能开销。例如,我们可以创建一个无限的整数序列,然后从中取出前 N 个元素:

#include <iostream>
#include <ranges>
int main() {
auto infinite_numbers = std::views::iota(1);
auto first_ten = infinite_numbers | std::views::take(10);
for (int number : first_ten) {
std::cout << number << " ";
}
std::cout << std::endl; // Output: 1 2 3 4 5 6 7 8 9 10
return 0;
}

这段代码创建了一个从 1 开始的无限整数序列,然后取出了前 10 个元素。由于 Views 的延迟计算特性,这段代码不会无限循环下去,而是只计算实际需要的元素。

3. Range Adaptors:范围的组合与修改

Range Adaptors 是用于组合和修改范围的工具。它们可以链接多个视图,或者将一个范围转换为另一种类型。

常用的 Range Adaptors

  • std::ranges::subrange:创建一个范围的子范围。例如:

    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    auto subrange = std::ranges::subrange(numbers.begin() + 1, numbers.begin() + 4); // Creates a subrange from index 1 to 3 (exclusive)
    for (int number : subrange) {
    std::cout << number << " ";
    }
    std::cout << std::endl; // Output: 2 3 4
    return 0;
    }

    std::ranges::subrange 可以方便地创建一个范围的子范围,而无需复制数据。

  • std::ranges::join:将多个范围连接成一个范围。例如:

    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4, 5}, {6}};
    auto joined_range = matrix | std::views::join;
    for (int number : joined_range) {
    std::cout << number << " ";
    }
    std::cout << std::endl; // Output: 1 2 3 4 5 6
    return 0;
    }

    std::ranges::join 可以将一个包含多个范围的范围连接成一个单一的范围。

  • std::ranges::split:将一个范围分割成多个子范围。例如:

    #include <iostream>
    #include <string>
    #include <ranges>
    int main() {
    std::string text = "hello,world,how,are,you";
    auto words = text | std::views::split(',');
    for (auto word_range : words) {
    for (char c : word_range) {
    std::cout << c;
    }
    std::cout << " ";
    }
    std::cout << std::endl; // Output: hello world how are you
    return 0;
    }

    std::ranges::split 可以根据指定的分隔符将一个范围分割成多个子范围。

4. Algorithms:更简洁的算法调用

Ranges 库提供了许多新的算法,这些算法可以直接操作 range 对象,而无需显式地传递迭代器。这些算法都位于 std::ranges 命名空间中。

常用的 Ranges 算法

  • std::ranges::for_each:对范围内的每个元素执行一个函数。例如:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::ranges::for_each(numbers, [](int x) { std::cout << x << " "; });
    std::cout << std::endl; // Output: 1 2 3 4 5
    return 0;
    }

    std::ranges::for_each 可以方便地对范围内的每个元素执行一个函数。

  • std::ranges::count:计算范围内满足指定条件的元素的数量。例如:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};
    int count = std::ranges::count(numbers, 2); // Counts the number of elements equal to 2
    std::cout << "Count: " << count << std::endl; // Output: Count: 3
    return 0;
    }

    std::ranges::count 可以方便地计算范围内满足指定条件的元素的数量。

  • std::ranges::sort:对范围内的元素进行排序。例如:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <ranges>
    int main() {
    std::vector<int> numbers = {5, 2, 1, 4, 3};
    std::ranges::sort(numbers); // Sorts the elements in ascending order
    for (int number : numbers) {
    std::cout << number << " ";
    }
    std::cout << std::endl; // Output: 1 2 3 4 5
    return 0;
    }

    std::ranges::sort 可以方便地对范围内的元素进行排序。

Ranges 算法的优势

与传统的 STL 算法相比,Ranges 算法具有以下优势:

  • 更加简洁: 无需显式地传递迭代器,代码更加简洁易读。
  • 更加安全: 编译器可以自动推导出范围的起始和结束位置,减少了出错的可能性。
  • 更加灵活: 可以与 Views 和 Range Adaptors 组合使用,构建复杂的数据处理流水线。

5. 实战案例:使用 Ranges 解决实际问题

为了更好地理解 Ranges 的应用,我们来看一个实战案例。假设我们需要从一个文本文件中读取所有单词,并统计每个单词出现的次数。使用 Ranges,我们可以很方便地实现这个功能:

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
#include <ranges>
int main() {
std::ifstream file("text.txt");
if (!file.is_open()) {
std::cerr << "Failed to open file!" << std::endl;
return 1;
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
auto words = content
| std::views::split(' ')
| std::views::transform([](auto&& word_range) { return std::string(word_range.begin(), word_range.end()); })
| std::views::filter([](const std::string& word) { return !word.empty(); });
std::map<std::string, int> word_counts;
for (const std::string& word : words) {
word_counts[word]++;
}
for (const auto& pair : word_counts) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}

这段代码首先读取文本文件的内容,然后使用 std::views::split 将文本分割成单词。接着,使用 std::views::transform 将每个单词转换为 std::string 类型,并使用 std::views::filter 过滤掉空单词。最后,使用 std::map 统计每个单词出现的次数。

6. Ranges 的注意事项

虽然 Ranges 提供了很多便利,但在使用时也需要注意一些事项:

  • 编译器支持: Ranges 是 C++20 的新特性,需要使用支持 C++20 的编译器才能编译。目前,主流的编译器(如 GCC、Clang、MSVC)都已经支持 Ranges。
  • 性能: 虽然 Views 采用延迟计算的机制,但在某些情况下,过度使用 Views 可能会导致性能下降。需要根据实际情况进行权衡。
  • 调试: 由于 Views 的延迟计算特性,调试 Ranges 代码可能会比较困难。可以使用调试器逐步执行代码,或者使用日志输出中间结果。

7. 总结

C++20 Ranges 库是一个强大的工具,可以帮助我们更加简洁、高效地处理容器和算法。通过学习 Ranges 的核心概念、Views、Range Adaptors 和 Algorithms,我们可以编写出更加易读、易维护的代码。在实际项目中,可以根据具体情况灵活运用 Ranges,提高开发效率和代码质量。希望本文能够帮助你更好地理解和使用 C++20 Ranges,让你在 C++ 编程的道路上更上一层楼!

代码旅行者 C++20RangesSTL

评论点评

打赏赞助
sponsor

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

分享

QRcode

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