当年我们做数据处理。
最常见的妥协。
就是“先拷贝一份”。
你想跳着取。
就先把要的元素 push 进新 vector。
能跑。
但一不小心。
你就多拷贝了一份大数据。
线上啪一下:采样日志,结果内存涨到报警
你写了个小项目。
采集一堆日志。
你只想采样。
比如每 10 条取一条。
你以前可能这么写。
std::vector<int> sampled;
for (std::size_t i = 0; i < v.size(); i += 10) {
sampled.push_back(v[i]);
}
它没错。
但它是拷贝。
而你真正想要的。
可能只是。
“遍历的时候跳着走”。
一个最小的自定义 view:every_n
下面这个版本。
只追求“能看懂、能跑”。
#include <ranges>
template <std::ranges::view V>
class every_n_view : public std::ranges::view_interface<every_n_view<V>> {
public:
every_n_view(V base, std::size_t n) : base_(std::move(base)), n_(n) {}
struct iterator;
iterator begin();
std::default_sentinel_t end() { return {}; }
private:
V base_;
std::size_t n_;
};
第一眼你会觉得“还挺像样”。
但它的核心其实很朴素。
第一。
保存底层 view。
第二。
迭代器每次 ++ 走 n 步。
第三。
end 用一个 sentinel 表示“到头了”。
begin() 的最小迭代器:每次 ++ 走 N 步
先把迭代器写出来。
template <std::ranges::view V>
struct every_n_view<V>::iterator {
std::ranges::iterator_t<V> cur;
std::ranges::sentinel_t<V> end;
std::size_t n;
auto operator*() const { return *cur; }
iterator& operator++() {
for (std::size_t i = 0; i < n && cur != end; ++i) ++cur;
return *this;
}
bool operator==(std::default_sentinel_t) const { return cur == end; }
};
它没有花活。
就三个字段。
当前迭代器。
结束哨兵。
还有步长 n。
begin():把三样东西塞进 iterator
template <std::ranges::view V>
typename every_n_view<V>::iterator every_n_view<V>::begin() {
return iterator{std::ranges::begin(base_), std::ranges::end(base_), n_};
}
它做的事情很直接。
把底层 begin/end 拿出来。
再把步长带上。
怎么用
#include <ranges>
#include <vector>
std::vector v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto sampled = every_n_view(std::views::all(v), 3);
for (int x : sampled) {
// 0, 3, 6, 9
}
你看到的。
不是拷贝后的容器。
而是一种遍历方式。
关键结论
自定义 view 的价值。
不在于“你要把标准库重写一遍”。
而在于。
你能把自己的遍历规则。
写成可组合的一段管道。
小结
你写过一次小 view。
再回去看 filter/transform/take。
它们就不再是黑盒。
它们只是。
一堆很克制的迭代器技巧。