C++迭代器模式遍历聚合对象

2026-04-11 23:30:29 1395阅读 0评论

C++里,迭代器不是“遍历工具”,而是“访问协议”

刚学C++那会儿,我总把std::vector::iterator当成一个“高级for循环指针”——写个for (auto it = v.begin(); it != v.end(); ++it)就完事。后来在项目里改一段老代码,发现容器从vector换成list后,所有带下标访问(it[5])的地方全崩了。这才意识到:迭代器的本质,从来不是“怎么走”,而是“能怎么走”

C++的迭代器模式,表面看是为统一遍历接口,深层其实是把“访问权”和“存储权”彻底剥离开。它不关心数据存在哪、怎么组织,只约定好三件事:怎么起步(begin)、怎么停步(end)、怎么挪一步(++ / * / ->)。这个契约,才是它能在STL里稳坐二十年的底气。

举个实在的例子。假设你正在写一个日志缓冲区类:

class LogBuffer {
private:
    std::deque<std::string> m_entries;
public:
    // 你不会直接暴露 deque,但得让人能读日志
    auto begin() { return m_entries.begin(); }
    auto end()   { return m_entries.end(); }
    auto begin() const { return m_entries.begin(); }
    auto end() const   { return m_entries.end(); }
};

就这么几行,调用方就能用范围for、std::findstd::sort(如果支持)等所有泛型算法——你没写一行遍历逻辑,却完成了全部访问适配。关键在于:begin()/end()返回的类型,必须满足对应迭代器类别(这里是双向迭代器),否则std::sort这种要求随机访问的算法就会编译失败。

这里有个容易踩的坑:很多人以为只要重载operator++operator*就行。错。迭代器必须通过std::iterator_traits暴露自己的“身份信息”value_typedifference_typepointerreferenceiterator_category。比如自定义迭代器漏了iterator_category = std::bidirectional_iterator_tagstd::advance(it, 10)可能退化成10次++,而不是一次跳转——性能悄无声息地垮掉。

更实际的问题是:什么时候该自己写迭代器?答案很朴素——当你封装的数据结构无法直接复用STL容器迭代器时。比如你实现了一个内存池管理的FixedBlockList,节点分散在不同内存块中;或者一个二叉搜索树,想按中序遍历提供有序视图。这时,你写的不是“遍历器”,而是“访问翻译器”:把树的递归路径,翻译成线性++操作;把内存池的跨块跳转,封装成对用户透明的++语义。

我们来拆解一个极简但真实的场景:一个只读的配置映射类,底层用std::map<std::string, ConfigValue>,但你想屏蔽ConfigValue细节,只暴露std::string_view键:

class ConfigMap {
    std::map<std::string, ConfigValue> m_data;
public:
    struct key_iterator {
        using iterator_category = std::forward_iterator_tag;
        using value_type = std::string_view;
        using difference_type = std::ptrdiff_t;
        using pointer = std::string_view*;
        using reference = std::string_view;

        std::map<std::string, ConfigValue>::const_iterator it;

        key_iterator(std::map<std::string, ConfigValue>::const_iterator i) : it(i) {}

        std::string_view operator*() const { return it->first; }
        key_iterator& operator++() { ++it; return *this; }
        key_iterator operator++(int) { auto tmp = *this; ++it; return tmp; }
        bool operator==(const key_iterator& other) const { return it == other.it; }
        bool operator!=(const key_iterator& other) const { return it != other.it; }
    };

    key_iterator begin() const { return key_iterator{m_data.cbegin()}; }
    key_iterator end() const   { return key_iterator{m_data.cend()}; }
};

注意几个细节:key_iterator里没有存std::string_view副本,只持有一个原map的const_iteratoroperator*每次现场构造string_view,零拷贝;iterator_category明确声明为前向迭代器——因为map本身不支持随机跳转。这比写个get_keys()返回std::vector<std::string>省内存,也比暴露原始map接口更安全

最后说个常被忽略的实践原则:迭代器应尽量轻量且无状态。它不该持有锁、不该缓存计算结果、不该在operator*里做IO。如果遍历过程需要复杂状态(比如解析流式JSON),那就别硬套迭代器模式——改用回调、生成器(C++20 coroutine)或显式游标类更诚实。

迭代器模式在C++里早已超越设计模式教科书里的UML图。它是泛型编程的基石契约,是算法与数据解耦的物理接口,更是C++程序员对“抽象不牺牲效率”这一信条的日常践行。下次你再写begin()end(),不妨停半秒:我交付的不是一个“能走的指针”,而是一份清晰、可验证、可组合的访问承诺。

这承诺不华丽,但足够坚实——就像拧紧每一颗螺丝,整台机器才能转起来。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,1395人围观)

还没有评论,来说两句吧...

目录[+]