C++迭代器模式遍历聚合对象
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::find、std::sort(如果支持)等所有泛型算法——你没写一行遍历逻辑,却完成了全部访问适配。关键在于:begin()/end()返回的类型,必须满足对应迭代器类别(这里是双向迭代器),否则std::sort这种要求随机访问的算法就会编译失败。
这里有个容易踩的坑:很多人以为只要重载operator++和operator*就行。错。迭代器必须通过std::iterator_traits暴露自己的“身份信息”:value_type、difference_type、pointer、reference、iterator_category。比如自定义迭代器漏了iterator_category = std::bidirectional_iterator_tag,std::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_iterator;operator*每次现场构造string_view,零拷贝;iterator_category明确声明为前向迭代器——因为map本身不支持随机跳转。这比写个get_keys()返回std::vector<std::string>省内存,也比暴露原始map接口更安全。
最后说个常被忽略的实践原则:迭代器应尽量轻量且无状态。它不该持有锁、不该缓存计算结果、不该在operator*里做IO。如果遍历过程需要复杂状态(比如解析流式JSON),那就别硬套迭代器模式——改用回调、生成器(C++20 coroutine)或显式游标类更诚实。
迭代器模式在C++里早已超越设计模式教科书里的UML图。它是泛型编程的基石契约,是算法与数据解耦的物理接口,更是C++程序员对“抽象不牺牲效率”这一信条的日常践行。下次你再写begin()和end(),不妨停半秒:我交付的不是一个“能走的指针”,而是一份清晰、可验证、可组合的访问承诺。
这承诺不华丽,但足够坚实——就像拧紧每一颗螺丝,整台机器才能转起来。


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