好的,各位观众老爷,今天咱们来聊聊C++20里新出的一个超级好玩的东西——std::ranges
,中文可以叫它“范围”或者“区间”,但我觉得叫“ranges”更酷炫,更有逼格。想象一下,你之前写C++代码,处理数组、vector等等,是不是得用迭代器开始结束,循环遍历,写得眼花缭乱?现在有了std::ranges
,你可以像写Python一样,用更简洁、更声明式的方式操作数据了!而且还能像搭乐高一样,把各种算法组合起来,简直爽翻天!
一、 啥是std::ranges
?为啥要用它?
简单来说,std::ranges
就是C++20里对范围操作的一套新标准。它主要解决了以下几个问题:
- 简化代码: 之前的C++算法需要传入迭代器开始和结束位置,代码冗长。
std::ranges
可以直接操作整个范围,代码简洁多了。 - 更安全: 避免了迭代器失效的问题。因为你直接操作范围,而不是手动管理迭代器。
- 组合性: 可以像搭积木一样,把多个算法组合起来,形成复杂的数据处理流程。这可比手写循环高效多了。
- 延迟计算: 很多
std::ranges
的操作都是延迟计算的,只有在真正需要结果的时候才会执行,提高了效率。
二、 核心概念:View、Action和Range
std::ranges
的核心概念有三个:
- Range(范围): 就是一个可以遍历的序列。比如
std::vector
、std::array
、std::list
,甚至你自己定义的类,只要满足一定的条件,都可以是Range。 - View(视图): 视图是一个轻量级的Range,它不会拥有数据,而是对现有Range进行转换和过滤。你可以把View想象成一个眼镜,戴上不同的眼镜,看到的东西就不同了,但眼镜本身并不会改变原来的东西。View是延迟计算的,只有在真正需要结果的时候才会执行。
- Action(动作): Action是对Range进行操作的函数或函数对象。比如排序、查找、转换等等。
这三个概念的关系是:Range是数据源,View是对Range的转换和过滤,Action是对Range进行操作。通过组合View和Action,我们可以构建复杂的数据处理流程。
三、 常用Range适配器(Views)
Range适配器是std::ranges
里最常用的东西,它们可以对Range进行各种转换和过滤。下面是一些常用的Range适配器:
适配器 | 功能 | 示例 | |
---|---|---|---|
views::filter |
过滤Range中的元素,只保留满足条件的元素。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto even_numbers = numbers | std::views::filter([](int n){ return n % 2 == 0; }); // even_numbers现在包含{2, 4, 6, 8, 10} |
|
views::transform |
转换Range中的元素,将每个元素映射到另一个值。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; auto squared_numbers = numbers | std::views::transform([](int n){ return n * n; }); // squared_numbers现在包含{1, 4, 9, 16, 25} |
|
views::take |
从Range中取出前N个元素。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto first_five = numbers | std::views::take(5); // first_five现在包含{1, 2, 3, 4, 5} |
|
views::drop |
从Range中丢弃前N个元素。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto last_five = numbers | std::views::drop(5); // last_five现在包含{6, 7, 8, 9, 10} |
|
views::reverse |
反转Range中的元素。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; auto reversed_numbers = numbers | std::views::reverse; // reversed_numbers现在包含{5, 4, 3, 2, 1} |
|
views::common |
将Range转换为common range。Common range是指begin()和end()返回相同类型的迭代器的Range。有些算法只能处理common range。 | “`c++ std::vector numbers = {1, 2, 3, 4, 5}; auto common_numbers = numbers | std::views::common; // common_numbers现在是一个common range。 |
views::iota |
生成一个递增的整数序列。 | c++ auto numbers = std::views::iota(1, 11); // numbers现在包含{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} |
|
views::elements |
从包含tuple或pair的Range中提取指定索引的元素。 | c++ std::vector<std::pair<int, std::string>> data = {{1, "one"}, {2, "two"}, {3, "three"}}; auto names = data | std::views::elements<1>; // names现在包含{"one", "two", "three"} |
|
views::split |
将Range分割成多个子Range,根据指定的分隔符。 | c++ std::string text = "hello,world,this,is,a,test"; auto words = text | std::views::split(','); // words现在包含{"hello", "world", "this", "is", "a", "test"} |
|
views::join |
将一个包含Range的Range连接成一个Range。 | c++ std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}, {5, 6}}; auto numbers = matrix | std::views::join; // numbers现在包含{1, 2, 3, 4, 5, 6} |
四、 常用算法(Actions)
std::ranges
也提供了一些常用的算法,可以直接操作Range。这些算法和std::algorithm
里的算法类似,但是用法更简洁。
算法 | 功能 | 示例 |
---|---|---|
ranges::sort |
对Range进行排序。 | c++ std::vector<int> numbers = {5, 2, 8, 1, 9, 4}; std::ranges::sort(numbers); // numbers现在包含{1, 2, 4, 5, 8, 9} |
ranges::copy |
将一个Range复制到另一个Range。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; std::vector<int> copy(5); std::ranges::copy(numbers, copy.begin()); // copy现在包含{1, 2, 3, 4, 5} |
ranges::find |
在Range中查找指定元素。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; auto it = std::ranges::find(numbers, 3); if (it != numbers.end()) { std::cout << "Found: " << *it << std::endl; } |
ranges::count |
统计Range中满足条件的元素个数。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 2, 2}; auto count = std::ranges::count(numbers, 2); // count现在是3 |
ranges::for_each |
对Range中的每个元素执行指定操作。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; std::ranges::for_each(numbers, [](int n){ std::cout << n << " "; }); // 输出:1 2 3 4 5 |
ranges::transform |
将一个Range转换成另一个Range,并将结果写入到指定的输出Range。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; std::vector<int> squared_numbers(5); std::ranges::transform(numbers, squared_numbers.begin(), [](int n){ return n * n; }); // squared_numbers现在包含{1, 4, 9, 16, 25} |
ranges::any_of |
判断Range中是否存在满足条件的元素。 | c++ std::vector<int> numbers = {1, 2, 3, 4, 5}; bool has_even = std::ranges::any_of(numbers, [](int n){ return n % 2 == 0; }); // has_even现在是true |
ranges::all_of |
判断Range中是否所有元素都满足条件。 | c++ std::vector<int> numbers = {2, 4, 6, 8, 10}; bool all_even = std::ranges::all_of(numbers, [](int n){ return n % 2 == 0; }); // all_even现在是true |
ranges::none_of |
判断Range中是否没有元素满足条件。 | c++ std::vector<int> numbers = {1, 3, 5, 7, 9}; bool none_even = std::ranges::none_of(numbers, [](int n){ return n % 2 == 0; }); // none_even现在是true |
ranges::remove_if |
从Range中移除满足条件的元素。注意:这个算法会改变原Range,并将不满足条件的元素移动到Range的前面,返回指向被移除元素起始位置的迭代器。通常需要结合erase 来真正删除元素。 |
c++ std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; auto it = std::ranges::remove_if(numbers, [](int n){ return n % 2 == 0; }); numbers.erase(it, numbers.end()); // numbers现在包含{1, 3, 5} |
五、 实战演练:组合View和Action
光说不练假把式,咱们来几个例子,看看怎么把View和Action组合起来,实现复杂的数据处理。
例子1:找到一个vector中所有偶数的平方,并排序。
#include <iostream>
#include <vector>
#include <algorithm>
#include <ranges>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9, 4, 6, 3, 7, 10};
// 1. 过滤出偶数
// 2. 计算平方
// 3. 排序
auto result = numbers | std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * n; });
std::vector<int> sorted_result(std::ranges::begin(result), std::ranges::end(result));
std::ranges::sort(sorted_result);
// 输出结果
for (int n : sorted_result) {
std::cout << n << " ";
}
std::cout << std::endl; // 输出:4 16 36 64 100
return 0;
}
这个例子里,我们用|
操作符把views::filter
和views::transform
连接起来,形成一个View。然后,我们把这个View转换成一个std::vector
,并用std::ranges::sort
进行排序。
例子2:从一个字符串中提取所有的数字。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <ranges>
int main() {
std::string text = "abc123def456ghi789";
// 1. 过滤出数字字符
// 2. 将字符转换为整数
auto digits = text | std::views::filter([](char c){ return std::isdigit(c); })
| std::views::transform([](char c){ return c - '0'; });
// 将结果保存到vector
std::vector<int> result(std::ranges::begin(digits), std::ranges::end(digits));
// 输出结果
for (int n : result) {
std::cout << n << " ";
}
std::cout << std::endl; // 输出:1 2 3 4 5 6 7 8 9
return 0;
}
这个例子里,我们用views::filter
过滤出数字字符,然后用views::transform
将字符转换为整数。
例子3:分割字符串,并统计每个单词的长度。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <ranges>
int main() {
std::string text = "hello world this is a test";
// 1. 分割字符串成单词
// 2. 统计每个单词的长度
auto word_lengths = text | std::views::split(' ')
| std::views::transform([](auto word){
// word是一个std::ranges::subrange,需要转换成string
std::string s(word.begin(), word.end());
return s.length();
});
// 将结果保存到vector
std::vector<size_t> result(std::ranges::begin(word_lengths), std::ranges::end(word_lengths));
// 输出结果
for (size_t len : result) {
std::cout << len << " ";
}
std::cout << std::endl; // 输出:5 5 4 2 1 4
return 0;
}
这个例子里,我们用views::split
将字符串分割成单词,然后用views::transform
统计每个单词的长度。注意,views::split
返回的是一个std::ranges::subrange
,我们需要把它转换成std::string
才能计算长度。
六、 自定义Range
除了使用标准的Range,我们还可以自定义Range。要自定义Range,需要满足一定的条件,比如提供begin()
和end()
函数,以及定义迭代器类型。
下面是一个简单的自定义Range的例子:
#include <iostream>
#include <ranges>
// 自定义Range:生成一个从start到end的整数序列
class IntRange {
public:
class iterator {
public:
using iterator_category = std::input_iterator_tag;
using value_type = int;
using difference_type = std::ptrdiff_t;
using pointer = int*;
using reference = int&;
iterator(int value) : value_(value) {}
int operator*() const { return value_; }
iterator& operator++() {
++value_;
return *this;
}
bool operator!=(const iterator& other) const {
return value_ != other.value_;
}
private:
int value_;
};
IntRange(int start, int end) : start_(start), end_(end) {}
iterator begin() const { return iterator(start_); }
iterator end() const { return iterator(end_); }
private:
int start_;
int end_;
};
int main() {
IntRange numbers(1, 11); // 生成一个从1到10的整数序列
// 使用std::ranges::for_each遍历Range
std::ranges::for_each(numbers, [](int n){ std::cout << n << " "; });
std::cout << std::endl; // 输出:1 2 3 4 5 6 7 8 9 10
return 0;
}
这个例子里,我们定义了一个IntRange
类,它可以生成一个从start
到end
的整数序列。我们还定义了一个iterator
类,用于遍历这个序列。
七、 注意事项
- 编译器支持:
std::ranges
是C++20的特性,需要使用支持C++20的编译器。 - 性能: 虽然
std::ranges
通常比手写循环更高效,但也要注意避免过度使用View,导致性能下降。 - 调试: 调试
std::ranges
的代码可能会比较困难,因为很多操作都是延迟计算的。
八、 总结
std::ranges
是C++20里一个非常强大的特性,它可以让你用更简洁、更声明式的方式操作数据。通过组合View和Action,你可以构建复杂的数据处理流程,提高代码的可读性和可维护性。虽然学习std::ranges
需要一些时间,但绝对值得!
希望今天的讲解对你有所帮助。记住,编程的乐趣在于不断学习和尝试!下次再见!