实现一个range

前言

  for-each 就像沙漠中的一淌小河,在 STL 的沙漠中将我拯救;for-each 就像黑暗里的一盏明灯,照亮我学 STL 的路;for-each 就像一支画笔,画过的地方屎山代码再也不见……

为了让 C++ 变得更 python 一样简易,好渴鹅想要做一个 range 函数,这将返回一个左闭右开的区间。该如何实现这个区间呢?好渴鹅这里有几种想法,分别是迭代器式、vector 式:

for i in range(1, 10):
    print(i, end=' ')
python 源代码
for (int i : range(1, 10)) {
  cout << i << ' ';
}
C++ 理想型

过程

基础原理

首先,我们知道一个 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 << ' ';
}
对于 C++11 之后的 for-each 代码

但是我们发现编译错了,这时因为虽然我们定义了 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);
  } 
}
foreach 函数

这样,你就可以类似于这样来进行循环了,个人感觉没有之前那种好看,就是感觉有点奇怪。这里为了简介,我塞了一个 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
使用 foreach 函数进行循环

完成阶段

这就已经差不多了,但是想要想前面一样使用 range-for,我们需要添加 begin 和 end 函数。

iterator begin() {
  return iterator(this, __begin);
}

iterator end() {
  return iterator(this, __end);
}

完结散花!

总结

  跟着好渴鹅一起自定义了这么多的头文件,想必大家已经会初步的定义一个自己的头文件了吧?其实非常简单,只要不编译错误,那么就基本不会出太大问题。我们学习了模板、类与对象、泛型编程、迭代器、重载运算符等等等等,这都是 OI 无法体会到的乐趣,

posted @ 2023-11-13 21:59  haokee  阅读(40)  评论(0)    收藏  举报