C++20 Ranges库实战?告别繁琐循环,代码优雅升级!
1. Ranges库的设计哲学:告别迭代器,拥抱变换
2. Ranges库的核心概念:Range、View、Action
3. Ranges库的使用方法:从入门到精通
3.1 基础:ranges::view_interface
3.2 View的创建与组合
3.3 自定义View
3.4 Action的使用
4. Ranges库的实战案例:提升代码表达力
4.1 案例一:查找字符串中最长的单词
4.2 案例二:计算列表中所有正数的平方和
4.3 案例三:从文件中读取所有邮箱地址
5. Ranges库的优势与局限
5.1 优势
5.2 局限
6. 总结与展望
各位C++的同僚们,是否还在为处理各种集合操作时,写出一堆又臭又长的循环而烦恼?是否渴望代码更加简洁、易读、易维护?C++20引入的Ranges库,正是解决这些问题的利器。它不仅是对STL的现代升级,更是编程思维的一次革新。本文将带你深入Ranges的世界,从设计思想、使用方法到实战案例,助你掌握这一强大工具,让你的C++代码焕然一新。
1. Ranges库的设计哲学:告别迭代器,拥抱变换
在深入Ranges库的细节之前,我们先来理解其背后的设计哲学。传统STL基于迭代器进行操作,虽然功能强大,但使用起来略显繁琐,尤其是在进行链式操作时,代码可读性会大大降低。Ranges库则试图通过引入Range的概念,将数据集合和操作算法解耦,从而实现更简洁、更易于组合的代码。
简单来说,Range就是一个可以像容器一样被遍历的东西。但与容器不同的是,Range并不一定拥有实际存储数据的空间,它可以是对现有容器的一个视图(View),也可以是动态生成的序列。
Ranges库的核心思想可以概括为以下几点:
- 组合性(Composability):Ranges库鼓励将小的、功能单一的操作组合成复杂的操作流水线。这类似于函数式编程中的函数组合,可以大大提高代码的表达能力。
- 延迟计算(Lazy Evaluation):Ranges库中的很多操作都是延迟执行的,只有在真正需要结果时才会进行计算。这可以避免不必要的计算,提高程序效率。
- 视图(Views):Ranges库提供了丰富的视图,可以对现有容器进行各种变换,而无需修改原始数据。这使得我们可以以不同的方式观察和操作同一个数据集合。
2. Ranges库的核心概念:Range、View、Action
要掌握Ranges库,首先需要理解几个核心概念:
- Range(范围):表示一个可以被迭代的元素序列。任何支持
begin()
和end()
函数的类型都可以被视为一个Range。例如,std::vector
、std::list
、std::array
等都是Range。 - View(视图):是一种轻量级的Range,它不拥有数据,而是对现有Range的一个变换。View可以进行过滤、转换、排序等操作,而不会修改原始数据。
- Action(动作):是一种可以修改原始Range的操作。例如,排序、删除元素等。
可以将Range理解为数据的来源,View理解为数据的变形器,Action理解为数据的修改器。通过将这三个概念分离,Ranges库实现了更高的灵活性和可组合性。
3. Ranges库的使用方法:从入门到精通
3.1 基础:ranges::view_interface
所有自定义的View都应该继承自ranges::view_interface
,它提供了一些默认实现,例如empty()
、size()
等,可以简化View的编写。
3.2 View的创建与组合
Ranges库提供了丰富的预定义View,可以满足各种常见的需求。例如:
ranges::views::filter
:过滤Range中的元素,只保留满足特定条件的元素。ranges::views::transform
:将Range中的每个元素转换为另一个值。ranges::views::take
:从Range中取出前N个元素。ranges::views::drop
:从Range中丢弃前N个元素。ranges::views::reverse
:反转Range中的元素顺序。
这些View可以通过管道操作符|
进行组合,形成复杂的数据处理流水线。例如,以下代码将一个vector中的偶数平方后取出前5个:
#include <iostream> #include <vector> #include <range/v3/all.hpp> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto result = numbers | ranges::views::filter([](int n){ return n % 2 == 0; }) | ranges::views::transform([](int n){ return n * n; }) | ranges::views::take(5); for (int n : result) { std::cout << n << " "; // 输出:4 16 36 64 100 } std::cout << std::endl; return 0; }
这段代码的可读性非常高,即使不熟悉Ranges库,也能很容易理解其功能。
3.3 自定义View
除了使用预定义的View,我们还可以根据自己的需求创建自定义View。自定义View需要继承自ranges::view_interface
,并实现begin()
和end()
函数,返回迭代器。
例如,以下代码定义了一个slice_view
,可以从Range中截取一段子序列:
#include <iostream> #include <vector> #include <range/v3/all.hpp> namespace my_views { template <typename Range> class slice_view : public ranges::view_interface<slice_view<Range>> { private: Range base_; size_t start_; size_t count_; public: slice_view() = default; slice_view(Range base, size_t start, size_t count) : base_(std::move(base)), start_(start), count_(count) {} auto begin() const { return ranges::begin(base_) + start_; } auto end() const { auto end_pos = ranges::begin(base_) + start_ + count_; return end_pos > ranges::end(base_) ? ranges::end(base_) : end_pos; } }; template <typename Range> slice_view(Range, size_t, size_t) -> slice_view<Range>; inline constexpr auto slice = ranges::make_view_adaptor([](auto&& range, size_t start, size_t count) { return slice_view((decltype(range))(range), start, count); }); } int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto result = numbers | my_views::slice(2, 5); for (int n : result) { std::cout << n << " "; // 输出:3 4 5 6 7 } std::cout << std::endl; return 0; }
这段代码首先定义了一个slice_view
类,它接受一个Range、一个起始位置和一个计数作为参数。begin()
函数返回指向起始位置的迭代器,end()
函数返回指向结束位置的迭代器。然后,使用ranges::make_view_adaptor
创建了一个名为slice
的View适配器,方便使用管道操作符。
3.4 Action的使用
Action用于修改原始Range。Ranges库提供了一些预定义的Action,例如:
ranges::actions::sort
:对Range进行排序。ranges::actions::unique
:移除Range中的重复元素。ranges::actions::remove_if
:移除满足特定条件的元素。
Action可以直接作用于Range,例如:
#include <iostream> #include <vector> #include <range/v3/all.hpp> int main() { std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}; ranges::actions::sort(numbers); ranges::actions::unique(numbers); for (int n : numbers) { std::cout << n << " "; // 输出:1 2 3 4 5 6 9 } std::cout << std::endl; return 0; }
这段代码首先对vector进行排序,然后移除重复元素。需要注意的是,Action会直接修改原始数据,因此在使用时需要谨慎。
4. Ranges库的实战案例:提升代码表达力
理论学习是基础,实战演练才能真正掌握Ranges库。下面我们通过几个实际案例,来展示Ranges库的强大之处。
4.1 案例一:查找字符串中最长的单词
传统方法:
#include <iostream> #include <string> #include <vector> #include <sstream> #include <algorithm> std::string longest_word(const std::string& text) { std::stringstream ss(text); std::string word; std::vector<std::string> words; while (ss >> word) { words.push_back(word); } if (words.empty()) { return ""; } std::string longest = words[0]; for (size_t i = 1; i < words.size(); ++i) { if (words[i].length() > longest.length()) { longest = words[i]; } } return longest; } int main() { std::string text = "This is a string with some words"; std::cout << "Longest word: " << longest_word(text) << std::endl; // 输出:Longest word: string return 0; }
使用Ranges库:
#include <iostream> #include <string> #include <range/v3/all.hpp> std::string longest_word_ranges(const std::string& text) { auto words = ranges::views::split(text, ' ') | ranges::to<std::vector<std::string>>(); if (words.empty()) { return ""; } auto longest = ranges::max(words, [](const std::string& a, const std::string& b) { return a.length() < b.length(); }); return longest; } int main() { std::string text = "This is a string with some words"; std::cout << "Longest word: " << longest_word_ranges(text) << std::endl; // 输出:Longest word: string return 0; }
使用Ranges库的代码更加简洁明了,ranges::views::split
将字符串分割成单词,ranges::max
找到最长的单词。整个过程一气呵成,可读性大大提高。
4.2 案例二:计算列表中所有正数的平方和
传统方法:
#include <iostream> #include <vector> int sum_of_squares_of_positives(const std::vector<int>& numbers) { int sum = 0; for (int n : numbers) { if (n > 0) { sum += n * n; } } return sum; } int main() { std::vector<int> numbers = {-2, -1, 0, 1, 2, 3}; std::cout << "Sum of squares of positives: " << sum_of_squares_of_positives(numbers) << std::endl; // 输出:Sum of squares of positives: 14 return 0; }
使用Ranges库:
#include <iostream> #include <vector> #include <range/v3/all.hpp> int sum_of_squares_of_positives_ranges(const std::vector<int>& numbers) { return ranges::accumulate(numbers | ranges::views::filter([](int n){ return n > 0; }) | ranges::views::transform([](int n){ return n * n; }), 0); } int main() { std::vector<int> numbers = {-2, -1, 0, 1, 2, 3}; std::cout << "Sum of squares of positives: " << sum_of_squares_of_positives_ranges(numbers) << std::endl; // 输出:Sum of squares of positives: 14 return 0; }
使用Ranges库的代码更加简洁,ranges::views::filter
过滤掉非正数,ranges::views::transform
计算平方,ranges::accumulate
计算总和。整个过程清晰流畅,易于理解。
4.3 案例三:从文件中读取所有邮箱地址
(假设邮箱地址满足xxx@xxx.xxx
的简单格式)
传统方法:
#include <iostream> #include <fstream> #include <string> #include <vector> #include <regex> std::vector<std::string> extract_emails(const std::string& filename) { std::vector<std::string> emails; std::ifstream file(filename); std::string line; std::regex email_regex("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"); if (file.is_open()) { while (std::getline(file, line)) { std::smatch match; std::string::const_iterator searchStart(line.cbegin()); while (std::regex_search(searchStart, line.cend(), match, email_regex)) { emails.push_back(match[0]); searchStart = match.suffix().first; } } file.close(); } return emails; } int main() { std::string filename = "emails.txt"; // 假设存在一个名为 emails.txt 的文件 std::vector<std::string> emails = extract_emails(filename); for (const auto& email : emails) { std::cout << email << std::endl; } return 0; }
(需要创建名为emails.txt的文件,内容包含若干email,用于测试)
使用Ranges库:
#include <iostream> #include <fstream> #include <string> #include <vector> #include <range/v3/all.hpp> #include <regex> std::vector<std::string> extract_emails_ranges(const std::string& filename) { std::regex email_regex("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"); std::ifstream file(filename); if (!file.is_open()) { return {}; // 或者抛出异常 } auto lines = ranges::getlines(file) | ranges::to<std::vector<std::string>>(); auto emails = lines | ranges::views::transform([&](const std::string& line) { return ranges::views::all(std::cregex_iterator(line.begin(), line.end(), email_regex), std::cregex_iterator()); }) | ranges::views::join | ranges::to<std::vector<std::string>>(); return emails; } int main() { std::string filename = "emails.txt"; // 假设存在一个名为 emails.txt 的文件 std::vector<std::string> emails = extract_emails_ranges(filename); for (const auto& email : emails) { std::cout << email << std::endl; } return 0; }
这个例子更复杂一些,但也更能体现Ranges的威力。
ranges::getlines(file)
: ranges 提供的读取文件每一行的 range。简化了文件读取操作。- 使用
ranges::views::transform
和std::cregex_iterator
将每一行转换为一个包含所有匹配邮箱地址的 range。 ranges::views::join
将所有行的邮箱地址 range 连接成一个单一的邮箱地址 range。- 最后,使用
ranges::to<std::vector<std::string>>
将结果转换为std::vector<std::string>
。
通过Ranges库,我们可以将复杂的数据处理逻辑分解成小的、可组合的步骤,从而提高代码的可读性和可维护性。
5. Ranges库的优势与局限
5.1 优势
- 代码简洁:Ranges库可以大大减少代码量,提高代码的表达能力。
- 可读性强:Ranges库的代码更易于理解,即使不熟悉Ranges库,也能很容易理解其功能。
- 可组合性高:Ranges库鼓励将小的、功能单一的操作组合成复杂的操作流水线。
- 效率高:Ranges库中的很多操作都是延迟执行的,可以避免不必要的计算。
5.2 局限
- 学习曲线:Ranges库引入了一些新的概念,需要一定的学习成本。
- 编译时间:Ranges库使用了大量的模板元编程,可能会增加编译时间。
- 调试难度:Ranges库的代码通常比较复杂,可能会增加调试难度。
- 并非所有编译器都完全支持:虽然 C++20 已经标准化,但并非所有编译器都完全支持 Ranges 库的所有特性。在使用前请确认你的编译器支持情况。
6. 总结与展望
C++20 Ranges库是一项强大的工具,它可以大大提高代码的表达能力和可维护性。虽然Ranges库有一定的学习成本,但一旦掌握,将会受益匪浅。未来,Ranges库将会得到更广泛的应用,成为C++开发的标配。
希望本文能够帮助你入门Ranges库,并在实际项目中灵活运用。让我们一起拥抱C++20,编写更优雅、更高效的代码!
最后,推荐一些学习资源:
- Eric Niebler's Range-v3 library: https://github.com/ericniebler/range-v3 (Ranges库的早期实现,很多概念和设计都源于此)
- cppreference.com: https://en.cppreference.com/w/cpp/ranges (C++标准库的官方文档,可以查阅Ranges库的各种类型和函数)
- 书籍: 搜索关于 C++20 的书籍,通常会包含 Ranges 库的章节。
多实践,多思考,你也能成为Ranges库的大师!
祝你编程愉快!