实现一个range
前言
for-each 就像沙漠中的一淌小河,在 STL 的沙漠中将我拯救;for-each 就像黑暗里的一盏明灯,照亮我学 STL 的路;for-each 就像一支画笔,画过的地方屎山代码再也不见……
为了让 C++ 变得更 python 一样简易,好渴鹅想要做一个 range 函数,这将返回一个左闭右开的区间。该如何实现这个区间呢?好渴鹅这里有几种想法,分别是迭代器式、vector 式:
for i in range(1, 10):
print(i, end=' ')
for (int i : range(1, 10)) {
cout << i << ' ';
}
过程
基础原理
首先,我们知道一个 vector 创建后是需要很大的空间的,如果是很大的循环,我们无法承受这个空间。那么就只能使用第一种了,用一个封装的类,分别表示前段以及后端。(这里的区间是左闭右闭的)然后实现一个迭代器,让 C++ 的 range-for 循环访问迭代器,直到无法循环。
注意如果想要实现到序的话,那么我们需要进行特殊的处理。
实现一个类
我们先敲好这个类的基础代码:
#pragma once
#ifdef __cplusplus
#ifndef HRANGE
#define HRANGE
#endif // HRANGE
#else
#error You must build in C++
#endif
HRange.h 头文件然后我们写一个没有用处的类 HRange。对于一个最最基础的 for 循环,我们需要哪些条件啊?答对了,是初始状态、结束状态以及增量表达式。所以,我们需要三个量,分别是 begin,end 以及 step。其中,step 是可为负数的增量。
既然都用上 C++ 了,那么泛型编程整一整。毕竟状态有可能是小数嘛……所以我们使用一个模板类,_Ty 就是类型,然后在类里面使用 value_type 进行替换,确保代码的可读性。
#pragma once
#ifdef __cplusplus
#ifndef HRANGE
#define HRANGE
#include <bits/c++config.h>
using size_t = unsigned long long;
template <class _Ty>
class HRange {
using value_type = _Ty;
private:
value_type __begin, __end, __step;
};
#endif // HRANGE
#else
#error You must build in C++
#endif
加上必要的构造函数:
HRange() = default;
HRange(const value_type &begin, const value_type &end) :
__begin(begin), __end(end) { __step = 1; }
HRange(const value_type &begin, const value_type &end, const value_type &step) :
__begin(begin), __end(end), __step(step) { }
HRange(const HRange<_Ty> &__other) {
__begin = __other.__begin;
__end = __other.__end;
__step = __other.__step;
}
一共四种,一种是默认构造函数,其他分别是步长为 \(1\) 的范围、自己调整的步长以及另一个对象赋值全拷贝,也就是非引用,不懂没关系。有了这三个函数,我们就可以任意创建不同的范围了。注意由于我们将范围的表示变量声明为了 private,你将无法在类的外面使用 x.xx 的形式进行调用,这将会引发编译器的编译错误。这有助于我们的数据保护。
HRange<int> r(1, 10, 1);
cout << r.__begin << '\n'; // 不可以在类外访问私有成员
迭代器
作为一个范围类,一个迭代器是十分重要的。我们这里暂时不考虑反向迭代,因为步长可以是负数。我们定义一个 iterator 类,并使用一个指针 p 指向迭代的对象,用一个变量 now 表示当前迭代的状态,也就是循环变量。重载加加运算符,调用指针内的步长,这样子就可以跨越到下一个迭代的元素了。注意,如果想要使用后缀加加的话,你可以这样写:operator(int i),i 只是个占位符。
同时重载
class iterator {
private:
value_type __now;
HRange<_Ty> *__p;
public:
iterator() = default;
iterator(HRange<_Ty> *__r, const value_type &__x) {
__now = __x;
__p = __r;
}
iterator operator++() {
__now += __p->__step;
return *this;
}
iterator operator++(int i) {
auto __tmp = *this;
__now += __p->__step;
return __tmp;
}
constexpr value_type operator*() {
return __now;
}
bool operator!=(const iterator &it){
return __now < it.__now;
}
bool operator==(const iterator &it){
return it.__now == __now;
}
};
注意重载不等于要用小于而非大于,有些人可能会问:大于不就是超出了迭代范围了吗?其实不对,因为它的比较是反着的,所以我们得反着写。可能这点是编译器实现的,大家都不太懂,多试几次就行了。然后步长有可能是负数,我们要进行特殊的优化:
bool operator!=(const iterator &it){
return __p->__step > 0 ? __now < it.__now : __now > it.__now;
}
定义额外的构造函数
由于我们的 HRange 直接构造有点过于难看,那我们就单独使用一个 range 函数来进行构造吧。值得注意的是,在这段代码上,我把 step 设成了可缺省参数,如果不填,那么就默认为 \(1\),符合的逻辑。
template <class _Ty>
HRange<_Ty> range(_Ty __begin, _Ty __end, _Ty __step = 1) {
return HRange<_Ty>(__begin, __end, __step);
}
接下来,我们就可以使用迭代器进行迭代了。具体操作如下:
- 首先使用一个临时变量,赋值为 begin 的值;
- 判断不等于运算符,然后加加;
- 判断,加加;
- ……
- 当不等于时,退出循环。
而 *临时变量 的值就是你给他的循环变量。我们可以通过自定义加加和不等于操作来使它变成一个十分好用的 range 迭代器。值得注意的是,在 C++11 以前还不能使用 for-each 的时候,你只能显式地调用迭代器,然后通过 * 运算符来访问元素。
for (double i : range(1, 11)) {
cout << i << ' ';
}
但是我们发现编译错了,这时因为虽然我们定义了 iterator 类,但是编译器并不会自动找到 begin 迭代器,需要我们自己写。可以翻到完成阶段进行对 for-each 的完善。
非迭代器的 foreach
如果你不想使用迭代器,我们也可以使用 for-each 进行循环。对于每一个在范围内的元素,我们都会调用你所提供的函数,这个函数必须要接受一个参数 \(x\),表示当前的状态。代码就很简单了:
bool __hasNext__(const value_type &__x) {
if (__step > 0 && __begin <= __end) {
return __x + __step <= __end;
} else if (__step < 0 && __begin >= __end) {
return __x + __step >= __end;
} else if (!__step) {
throw HError::EndlessLoop; // 自定义异常
} else {
return 0;
}
}
template <class _Function>
void foreach(const _Function &f) {
for (value_type i = __begin; __hasNext__(i); i += __step) {
f(i);
}
}
这样,你就可以类似于这样来进行循环了,个人感觉没有之前那种好看,就是感觉有点奇怪。这里为了简介,我塞了一个 lambda 表达式,如果不懂可以看下面我的函数传参版本
#if __cplusplus >= 20114514 // C++11 之后
range(1, 11).foreach([](int x) {
cout << x << ' ';
});
#else // 老版本编译器
void out(int x) {
cout << x << ' ';
}
range(1, 11).foreach(out);
#endif
完成阶段
这就已经差不多了,但是想要想前面一样使用 range-for,我们需要添加 begin 和 end 函数。
iterator begin() {
return iterator(this, __begin);
}
iterator end() {
return iterator(this, __end);
}
完结散花!
总结
跟着好渴鹅一起自定义了这么多的头文件,想必大家已经会初步的定义一个自己的头文件了吧?其实非常简单,只要不编译错误,那么就基本不会出太大问题。我们学习了模板、类与对象、泛型编程、迭代器、重载运算符等等等等,这都是 OI 无法体会到的乐趣,

浙公网安备 33010602011771号