C++11 工程级应用 06:自己造一个类似Python的Range迭代器

C++11 工程级应用 06:自己造一个类似Python的Range迭代器

  • [Bilibili 同步视频](#Bilibili 同步视频)
  • [一、先回顾:基于范围的 for 循环到底是什么?](#一、先回顾:基于范围的 for 循环到底是什么?)
  • 二、迭代器的接口该怎么设计?
  • [三、实现 Range 容器外壳 impl 类](#三、实现 Range 容器外壳 impl 类)
  • [四、封装对外的 range 函数模板](#四、封装对外的 range 函数模板)
  • 五、完整测试,体验效果
  • [六、总结 & 拓展思考](#六、总结 & 拓展思考)

摘要:C++11 推出的基于范围的 for 循环(range‑based for)用起来十分清爽,但很多人只知道拿它遍历 STL 容器。你有没有想过:如何让自定义类型也支持这个语法?甚至手写一个类似 Python 的range(),直接写for(auto n : range(2,8,2))完成区间迭代?今天我们就从底层原理到完整实现,一步步拆解这个有趣的小轮子。

Bilibili 同步视频

C++11 工程级应用 06:自己造一个类似Python的Range迭代器

一、先回顾:基于范围的 for 循环到底是什么?

很多同学写 C++11 之后的代码,经常写出这样赏心悦目的代码:

cpp 复制代码
std::vector<int> vec{1,2,3,4,5};
for(auto v : vec){
    std::cout << v << " ";
}

对比老式迭代器写法,代码简洁度直接拉满👍。但很多人只知其然,不知其所以然。

本质:基于范围的 for 循环只是编译器提供的语法糖。

编译器会在背后帮你去找对象的begin()end()迭代器,然后转换成普通迭代器循环。

⚠️重要避坑提醒:

尽量不要在基于范围的 for 迭代过程中修改被迭代的容器(增删元素)。一旦迭代器失效,会产生未定义行为。业务代码优先规避;如果业务场景不得不修改容器,一定要理解迭代器失效规则,提前做好逻辑防护。

那编译器是按什么规则去找beginend呢?优先级分为 3 条:

  1. 如果是原生数组,begin就是数组首地址,end是首地址 + 数组长度;

  2. 如果是类对象,优先调用类内部的成员函数begin()end()获取迭代器;

  3. 如果类没有提供成员版本,则尝试查找全局命名空间下的begin()end()函数。

划重点:想要自定义类型支持 **for(xxx : obj)语法,核心就是提供合法的 begin() end()****,返回能够做迭代对比的迭代器对象。**

日常开发中,STL 的vectormaplist都已经内置这套接口,开箱即用。但有些场景我们并不想迭代一个实实在在存放数据的容器,只是想迭代一段区间,例如:

cpp 复制代码
// 输出2,4,6
for(int n = 2; n < 8; n +=2){
    std::cout << n << " ";
}

上面代码逻辑很简单:区间 [2,8),步长为2。

在Python中我们可以优雅写:for n in range(2,8,2)

但原生C++标准库并没有提供这样的range工具 ,只能手写普通for循环。那我们能不能自己造一个轮子,实现C++版本的range,实现下面这样的效果?

cpp 复制代码
// 我们想要的效果
for(auto n : range(2,8,2)){
    std::cout << n << " ";
}

答案是完全可以,接下来我们动手造轮子。

二、迭代器的接口该怎么设计?

想要实现range,首先需要实现专属迭代器。

基于范围的 for 循环迭代时,只会做几件事:解引用*it取值、前置自增++it、使用!=做结束判断。

💡小知识点:range‑for并不会用到****== < >等运算符,只要求实现 operator!=和前置 operator++,可以少写很多代码。

这里有一个非常关键的思考点:迭代器之间比较,我们应该比较什么?

如果直接拿迭代器保存的数值做!=判断,会踩大坑!

举个例子:区间[2,8)步长 3。序列是2、5,下一个值是 8。

但是(8‑2)/3 = 2,刚好整除还好;如果区间[2,9)步长 3:序列 2、5、8。

但如果步长不能被区间差值整除,单纯对比 value 值,!=判断会失效。

✅ 解决方案:不对比迭代器内部数值,而是维护一个迭代计数器 cursor(游标)。拿游标做不等判断,而不是拿实际迭代的值。

  • cursor记录当前是第几次迭代;

  • value_保存当前迭代产出的值;

  • step_保存步长,迭代过程中不可修改,设置为const

迭代器基础骨架:

cpp 复制代码
namespace detail_range{
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 + step_ * cursor_;
    }

    // 解引用,获取值
    value_type operator*() const{
        return value_;
    }

    // 只需要实现!=,range‑for只调用!=
    bool operator!=(const iterator& rhs) const{
        return cursor_ != rhs.cursor_;
    }

    // 前置++,range‑for只用前置++
    iterator& operator++(){
        value_ += step_;
        ++cursor_;
        return *this;
    }
};
} // namespace detail_range

关键逻辑:

构造函数传入初始游标、起始值、步长,直接计算出初始 value;每次++,值增加步长,游标 + 1;比较迭代器是否结束,仅仅对比cursor_

这样不管步长是正数、负数、小数,都不会出现终止条件错乱的问题🎉。

三、实现 Range 容器外壳 impl 类

迭代器写好了,还需要一个 "容器外壳" 对象,给外部提供begin()end()成员函数,供 range‑for 调用。

我们把这个外壳命名为impl<T>

这个对象仅仅保存区间配置(起点、终点、步长、最大迭代次数),并不存储所有迭代元素,属于惰性迭代,没有内存开销,性能非常优秀

成员全部使用const修饰,对象构造完成之后参数就不能修改。

内部需要一个私有函数get_adjusted_count()计算最大迭代次数max_count_

  1. 做合法性校验:正向迭代步长 > 0 时,终点必须大于起点;反向迭代步长 < 0,终点必须小于起点;非法直接抛异常。

  2. 计算迭代次数,向上取整,处理不能整除的场景。

cpp 复制代码
namespace detail_range{
template<typename T>
class impl{
public:
    using value_type = T;
    using reference = const value_type&;
    using const_reference = const value_type&;
    using iterator = const iterator<T>;
    using const_iterator = const iterator<T>;
    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() 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() const{
        return max_count_;
    }

    const_iterator begin() const{
        return const_iterator(0, begin_, step_);
    }

    const_iterator end() const{
        return const_iterator(max_count_, begin_, step_);
    }
};
} // namespace detail_range

📌性能说明:

impl 对象只是保存 3 个数值 + 1 个计数值,迭代器也只有简单数值成员。整个 range 不分配堆内存,没有存储全部序列,属于惰性计算,时间、空间开销都是 O (1),千万级别迭代也不会占用大量内存。

四、封装对外的 range 函数模板

现在底层的detail_range::impl已经就绪,我们对外提供友好的函数重载,给使用者调用。提供 3 种调用形式:

  1. range(end):从 0 开始,步长 1;

  2. range(begin, end):自定义起点终点,步长默认为 1;

  3. range(begin, end, step):自定义起点终点和步长;

重点注意第三个重载:起点、步长可以是不同类型,比如range(2,8,0.5)整型 + 浮点。需要用decltype(begin + step)自动推导最终返回值类型,使用后置返回类型语法。

cpp 复制代码
// [0, end), step =1
template<typename T>
detail_range::impl<T> range(T end){
    return {T{}, end, 1};
}

// [begin, end), step =1
template<typename T>
detail_range::impl<T> range(T begin, T end){
    return {begin, end, 1};
}

// [begin, end), custom step
template<typename T, typename U>
auto range(T begin, T end, U step)
-> detail_range::impl<decltype(begin + step)>
{
    using r_t = detail_range::impl<decltype(begin + step)>;
    return r_t(begin, end, step);
}

五、完整测试,体验效果

写测试函数,测试整数、负数、浮点数、字符多种场景:

cpp 复制代码
#include <iostream>
#include <stdexcept>

// 把上面 iterator、impl、range函数全部放这里

void test_range(){
    std::cout << "range(15): ";
    for(auto i : range(15)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range(2,6): ";
    for(auto i : range(2,6)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range(2,6,3): ";
    for(auto i : range(2,6,3)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range(-2,-6,-3): ";
    for(auto i : range(-2,-6,-3)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range(10.5,15.5): ";
    for(auto i : range(10.5,15.5)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range(35,27,-1): ";
    for(auto i : range(35,27,-1)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range(2,8,0.5): ";
    for(auto i : range(2,8,0.5)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range(8,7,-0.1): ";
    for(auto i : range(8,7,-0.1)){
        std::cout << i << " ";
    }
    std::cout << "n";

    std::cout << "range('a','z'): ";
    for(auto ch : range('a','z')){
        std::cout << ch;
    }
    std::cout << "n";
}

int main(){
    test_range();
    return 0;
}

运行输出节选:

Plain 复制代码
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('a','z'): abcdefghijklmnopqrstuvwxy

可以看到:正数、反向负数、浮点步长、字符迭代全部正常工作✅。

⚠️小提示:浮点数迭代会存在浮点数精度误差,业务场景需要留意。

六、总结 & 拓展思考

  1. 基于范围的 for 循环不是魔法,只是语法糖 。编译器依赖begin()end()获取迭代器,只要自定义类型提供对应的迭代器接口,就能使用这个语法。

  2. 迭代器不一定需要持有全部数据,像我们实现的 range 就是惰性迭代,边迭代边计算,内存开销极小。

  3. 迭代结束判断优先使用计数器 cursor,不要直接拿业务值做不等对比,否则步长无法整除时会出现死循环。

  4. 注意合法性校验:正向迭代终点必须大于起点;反向迭代终点要小于起点,非法入参要做防护。

💡拓展思考:C++20 标准引入了 Ranges 库,原生支持类似 range 的视图,性能更强,接口更丰富。如果你的项目可以升级 C++20,可以优先使用标准 Ranges。但手动实现一遍这个小轮子,能够极大加深你对迭代器、基于范围 for 循环底层原理的理解,对阅读 STL 源码非常有帮助。


本篇代码基于 C++11 标准编译,GCC 编译命令参考:g++ main.cpp -std=c++11 -o range_demo

相关推荐
满怀冰雪13 分钟前
23-PaddleClas 数据集、配置文件与训练参数详解
人工智能·python·深度学习·paddlepaddle
zh_xuan22 分钟前
c++ jthread
开发语言·c++·jthread
旧梦952723 分钟前
Java 捕获多个异常:从基础语法到最佳实践
java·开发语言
笔触狂放1 小时前
第3章 抓取静态网页数据
爬虫·python
海兰1 小时前
【 Python 量化交易】开篇
开发语言·python·信息可视化·量化交易
pt10431 小时前
网络自动化Python课程:Netconf与Restconf及Cisco自动化配置实验
网络·python·自动化
神仙别闹1 小时前
基于C++ WinPcap 的网络抓包软件
网络·c++·php
slacker-kian1 小时前
[笔记]-什么是相似性搜索?
python·ai·向量·相似性搜索·点积·l2 距离·余弦相似度和距离
阿童木写作1 小时前
跨境电商图片翻译工具,批量翻译视频字幕还免费
python·音视频