假如有一个自己自定义的容器类,如何让它能够支持range-based for呢?其实上面已经提到了,基于范围的for循环只是普通for循环的语法糖。它需要查找到容器提供的begin和end迭代器。
具体来说,基于范围的for循环将以下面的方式查找容器的begin和end:
1)若容器是一个普通array对象(如int arr[10]),那么begin将为array的首地址,end将为首地址加容器长度。
2)若容器是一个类对象,那么range-based for将试图通过查找类的begin()和end()方法来定位begin、end迭代器。
3)否则,range-based for将试图使用全局的begin和end函数来定位begin、end迭代器。
由上述可知,对自定义类类型来说,分别实现begin()、end()方法即可。下面通过自定义一个range对象来看看具体的实现方法。
我们知道,标准库中有很多容器,如vector、list、queue、map,等等。这些对象在概念上属于Containers。但是如果我们并不需要对容器进行迭代,而是对某个“区间”进行迭代,此时标准库中并没有对应的概念。
在这种情况下,只能使用普通的for循环,代码如下:
for (int n = 2; n < 8; n += 2) // [2, 8) { std::cout << " " << n; }
上面的for循环其实是在区间[2, 8)中做迭代,迭代步长为2。如果我们可以实现一个range方法,使用range(2, 8, 2)来代表区间[2, 8),步长为2,就可以通过基于范围的for循环直接对这个区间做迭代了。
我们来看一下range的实现。首先,需要一个迭代器来负责对一个范围取值。
考虑到for循环中迭代器的使用方法(结束条件:ite !=end),迭代器的对外接口可以像下面这样:
template <typename T> class iterator { public: using value_type = T; using size_type = size_t; iterator(size_type cur_start, value_type begin_val, value_type step_val); value_type operator*() const; bool operator!=(const iterator& rhs) const; iterator& operator++(void); // pref?ix ++ operator only };
构造函数传递3个参数来进行初始化,分别是开始的迭代次数、初始值,以及迭代的步长。如果容器的begin()方法将设置初始值cur_start为0,end()方法将限定最多能够迭代的次数,那么在对ite和end做比较的时候,只需要比较迭代的次数就可以了。由于迭代次数不可能是负数,所以类型使用size_t。
这里为何不直接使用iterator当中的值做比较呢?这是因为迭代的步长step并不一定是begin-end的约数。如果我们直接采用iterator中的值做比较,就不能使用!=了。
operator*()用于取得迭代器中的值。
operator!=用于和另一个迭代器做比较。因为在基于范围的for循环的迭代中,我们只需要用到!=,因此,只需要实现这一个比较方法。
operator++用于对迭代器做正向迭代。当step为负数时,实际上会减少iterator的值。同样,在基于范围的for循环的迭代中也只需要实现前置++。
整理出这些必需的接口之后,它的功能实现就不困难了,如代码清单1-11所示。
代码清单1-11 迭代器类的实现
namespace detail_range { //////////////////////////////////////////////////////////////// /// The range iterator //////////////////////////////////////////////////////////////// template <typename T> class iterator { public: using value_type = T; using size_type = size_t; private: size_type cursor_; const value_type step_; value_type value_; public: iterator(size_type cur_start, value_type begin_val, value_type step_val) : cursor_(cur_start) , step_ (step_val) , value_ (begin_val) { value_ += (step_ * cursor_); } value_type operator*() const { return value_; } bool operator!=(const iterator& rhs) const { return (cursor_ != rhs.cursor_); } iterator& operator++(void) // pref?ix ++ operator only { value_ += step_; ++ cursor_; return (*this); } }; } // namespace detail_range
上述分别定义了cursor_、step_和value_这3个成员变量,来保存构造函数中传递进来的参数。
在iterator的构造中,我们通过一个简单的计算得到value_当前正确的值。
在之后的operator++(前置++)运算符中,我们通过+=操作计算出value_的下一个值。
在上面的代码中,step_被定义为一个常量。这是因为step_作为步长的存储,在迭代器中是不能被修改的。
接下来思考range类型的具体实现。
基于范围的for循环只需要对象具有begin和end方法,因此,impl类可以如代码清单1-12所示。
代码清单1-12 impl类的声明
template <typename T> class impl { public: using value_type = T; using reference = const value_type&; using const_reference = const value_type&; using iterator = const detail_range::iterator<value_type>; using const_iterator = const detail_range::iterator<value_type>; using size_type = typename iterator::size_type; impl(value_type begin_val, value_type end_val, value_type step_val); size_type size(void) const; const_iterator begin(void)const; const_iterator end(void)const; };
impl是range函数最终返回的一个类似容器的对象,因此,我们给它定义了容器所拥有的基本的概念抽象,比如value_type、reference及iterator。
很明显,我们不希望外部能够修改range,这个range应当是一个仅能通过构造函数作一次性赋值的对象,因此reference及iterator都应该是const类型,并且begin、end接口也应当返回const_iterator。
下面,让我们来实现impl类,如代码清单1-13所示。
代码清单1-13 impl类的实现
namespace detail_range { //////////////////////////////////////////////////////////////// /// The impl class //////////////////////////////////////////////////////////////// template <typename T> class impl { public: using value_type = T; using reference = const value_type&; using const_reference = const value_type&; using iterator = const detail_range::iterator<value_type>; using const_iterator = const detail_range::iterator<value_type>; using size_type = typename iterator::size_type; private: const value_type begin_; const value_type end_; const value_type step_; const size_type max_count_; size_type get_adjusted_count(void) const { if (step_ > 0 && begin_ >= end_) throw std::logic_error("End value must be greater than begin value."); else if (step_ < 0 && begin_ <= end_) throw std::logic_error("End value must be less than begin value."); size_type x = static_cast<size_type>((end_ - begin_) / step_); if (begin_ + (step_ * x) != end_) ++x; return x; } public: impl(value_type begin_val, value_type end_val, value_type step_val) : begin_(begin_val) , end_ (end_val) , step_ (step_val) , max_count_(get_adjusted_count()) {} size_type size(void) const { return max_count_; } const_iterator begin(void) const { return { 0, begin_, step_ }; } const_iterator end(void) const { return { max_count_, begin_, step_ }; } }; } // namespace detail_range
在上面的实现中,只允许impl做初始化时的赋值,而不允许中途修改,因此,它的成员变量都是const类型的。
通过private的get_adjusted_count方法,我们在获取max_count_也就是最大迭代次数时会先判断begin_、end_和step_的合法性。很显然,合法的组合将只可能计算出大于等于0的最大迭代次数。
之所以在get_adjusted_count中对计算的值做自增的调整,是为了让计算的结果向上取整。
最后,我们实现出range的外覆函数模板,如代码清单1-14所示。
代码清单1-14 range类的外覆函数模板
//////////////////////////////////////////////////////////////// /// Make a range of [begin, end) with a custom step //////////////////////////////////////////////////////////////// template <typename T> detail_range::impl<T> range(T end) { return{ {}, end, 1 }; } template <typename T> detail_range::impl<T> range(T begin, T end) { return{ begin, end, 1 }; } template <typename T, typename U> auto range(T begin, T end, U step) -> detail_range::impl<decltype(begin + step)> { // may be narrowing using r_t = detail_range::impl<decltype(begin + step)>; return r_t(begin, end, step); }
可以看到,通过初始化列表,可以让return的书写非常便捷。
这里需要注意的是第三个range重载。我们当然不希望限制用户使用range(1, 2, 0.1)这种方式来创建区间[1, 2),因此,step可以是不同的数据类型。
那么相对的,impl的模板参数类型应当是begin+step的返回类型。因此,我们需要通过返回值后置语法,并使用decltype(begin+step)来推断出range的返回值类型。
这样,在运行return的时候,可能会导致类型收窄(整型和浮点型的加法运算将总是返回浮点型),不过此处的类型收窄一般不会影响计算结果,因此,最后一个重载函数需要使用直接初始化方式返回impl对象。
下面运行一组示例来看看效果,如代码清单1-15所示。
代码清单1-15 测试代码
void test_range(void) { std::cout << "range(15):"; for (int i : range(15)) { std::cout << " " << i; } std::cout << std::endl; std::cout << "range(2, 6):"; for (auto i : range(2, 6)) { std::cout << " " << i; } std::cout << std::endl; const int x = 2, y = 6, z = 3; std::cout << "range(2, 6, 3):"; for (auto i : range(x, y, z)) { std::cout << " " << i; } std::cout << std::endl; std::cout << "range(-2, -6, -3):"; for (auto i : range(-2, -6, -3)) { std::cout << " " << i; } std::cout << std::endl; std::cout << "range(10.5, 15.5):"; for (auto i : range(10.5, 15.5)) { std::cout << " " << i; } std::cout << std::endl; std::cout << "range(35, 27, -1):"; for (int i : range(35, 27, -1)) { std::cout << " " << i; } std::cout << std::endl; std::cout << "range(2, 8, 0.5):"; for (auto i : range(2, 8, 0.5)) { std::cout << " " << i; } std::cout << std::endl; std::cout << "range(8, 7, -0.1):"; for (auto i : range(8, 7, -0.1)) { std::cout << " " << i; } std::cout << std::endl; std::cout << "range('a', 'z'):"; for (auto i : range('a', 'z')) { std::cout << " " << i; } std::cout << std::endl; } int main(void) { test_range(); return 0; } 示例的运行结果如下: range(15): 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 range(2, 6): 2 3 4 5 range(2, 6, 3): 2 5 range(-2, -6, -3): -2 -5 range(10.5, 15.5): 10.5 11.5 12.5 13.5 14.5 range(35, 27, -1): 35 34 33 32 31 30 29 28 range(2, 8, 0.5): 2 2.5 3 3.5 4 4.5 5 5.5 6 6.5 7 7.5 range(8, 7, -0.1): 8 7.9 7.8 7.7 7.6 7.5 7.4 7.3 7.2 7.1 range('a', 'z'): a b c d e f g h i j k l m n o p q r s t u v w x y
通过上面的range实现,我们不仅完成了自定义类型的range-based for支持,同时也使用了我们前面所学的自动类型推导、模板别名以及初始化列表的相关知识。
我们完成的这个range,目前还有一些限制(比如参数传递采用值语义,不支持range返回值的存储后修改等)。如果读者感兴趣,可以自己进一步完善range的实现。