WEBKT

C++20 Ranges库实战?告别繁琐循环,代码优雅升级!

64 0 0 0

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::vectorstd::liststd::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::transformstd::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,编写更优雅、更高效的代码!

最后,推荐一些学习资源:

多实践,多思考,你也能成为Ranges库的大师!

祝你编程愉快!

代码魔法师 C++20Ranges库STL

评论点评

打赏赞助
sponsor

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

分享

QRcode

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