本文编译环境:clang-21,C++23标准。
迭代器
迭代器(iterator)是 STL 的核心抽象之一:它将“如何遍历一个容器”与“容器本身的结构”解耦,让算法可以不关心底层数据结构。
用过 STL 的话,你一定见过类似这样的代码:
std::vector vec = {1, 2, 3, 4, 5};for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) { std::print("{} ", *it);}std::println();
std::map<std::string, int> mp = {{"a", 1}, {"b", 2}};for (std::map<std::string, int>::iterator it = mp.begin(); it != mp.end(); ++it) { std::print("{}:{} ", it->first, it->second);}std::println();vec.begin() 和 mp.begin() 返回的都是迭代器,但两者的底层结构截然不同——vector 是连续内存,map 是红黑树。迭代器将这种差异隐藏起来,对外统一提供 *、->、++ 这套接口。
我们平时写的范围 for 循环,本质上也是迭代器的语法糖:
for (auto& x : Container) // 等价于用 begin()/end() 迭代不同容器的迭代器,能力各不相同,本章将具体解释迭代器的分类与内部实现。
五种迭代器类型
C++ 标准库将迭代器按能力分为五个等级,构成一个继承层次:
input_iterator_tag / output_iterator_tagforward_iterator_tag (继承自 input)bidirectional_iterator_tag (继承自 forward)random_access_iterator_tag (继承自 bidirectional)这个层次反映的是”能做什么”:
| 类型 | 代表容器 | 支持操作 |
|---|---|---|
input / output | 流迭代器(比如输入输出流) | 单次单向遍历 |
forward | std::forward_list | 多次单向遍历 |
bidirectional | std::list | 双向遍历 (++ / --) |
random_access | std::vector、原生指针 | 随机访问(+n、-n、[]) |
正如继承关系所示,每一级迭代器的约束都在逐渐增强:
input_iterator / output_iterator_tag:常见于输入/输出流。因为底层是流,数据读过即消失,无法回头也无法重复读取,不保证迭代器副本独立有效,因此只支持单次的单向遍历。std::istream_iterator是典型代表。forward_iterator_tag:在输入迭代器基础上增加了多遍保证——迭代器可以被复制,两个副本独立前进互不影响,同一位置可以反复解引用,但是只支持向前遍历(++操作)。比如单向链表std::forward_list。bidirectional_iterator_tag:在前向迭代器基础上增加了--操作,支持向后移动。std::list、std::map、std::set等的迭代器属于这级,双向链表和红黑树都能在节点间双向跳转,但一次跳n步只能一步步来。random_access_iterator_tag:在双向迭代器基础上增加了 的任意跳跃(即随机访问能力),支持it + n、it - n、it1 - it2和it[n],以及迭代器之间的大小比较<、>。std::vector、std::deque的迭代器属于这级。std::sort等算法要求这级,因为内部需要 的分治跳跃。contiguous_iterator_tag(C++17 概念引入,C++20 正式有标签):在随机访问基础上额外保证内存物理连续,std::vector、std::array和裸数组满足这级,std::deque虽然支持随机访问但内存不完全连续,因此不满足。这个tag主要在拷贝时有效,如果物理内存完全连续,那么就是直接将std::copy优化为一次memcpy,但是某些支持随机访问但不完全连续的容器就不行。
这些 tag 不仅是“标记”,在后面我们会看到,它们也是编译期分发的关键。
std::iterator 的五个关联类型
一个合规的迭代器需要向外暴露五个类型信息,STL 算法依赖这些信息工作:
| 类型名 | 含义 |
|---|---|
value_type | 迭代器指向的元素类型 |
pointer | 元素的指针类型 |
reference | 元素的引用类型 |
difference_type | 两个迭代器之间距离的类型(通常是 ptrdiff_t) |
iterator_category | 迭代器类别 tag |
看不懂没关系,继续往下看实例。
实现一个自定义迭代器
这一节的目标是实现一个带哨兵节点的双向链表 my_list<T>,并为它配套实现一个符合规范的自定义迭代器。显然,对于一个自己手动实现的双向链表,不能直接通过 std::iterator 进行遍历,因此我们需要实现一个配套的自定义迭代器。
双向链表实现如下:
template <typename T>class my_list {private: struct node { T data = 0; node *prev = nullptr; node *next = nullptr; }; node* _head; node* _tail;
public: my_list() : _head(new node()), _tail(new node()) { _head->next = _tail; _tail->prev = _head; }
void push_back(const T &elem) { node *new_node = new node{elem, nullptr, _tail}; new_node->prev = _tail->prev; _tail->prev->next = new_node; _tail->prev = new_node; }
void push_front(const T &elem) { node *new_node = new node{elem, _head, nullptr}; new_node->next = _head->next; _head->next->prev = new_node; _head->next = new_node; }
void print() { auto current = _head->next; while (current != _tail) { std::print("{} ", current->data); current = current->next; } std::println(); }
~my_list() { auto current = _head->next; delete _head; while (current != _tail) { _head = current; current = current->next; delete _head; } delete _tail; }};使用哨兵节点(dummy head / tail)的好处是:
begin()返回_head->next,end()返回_tail,边界条件统一,无需特判。
迭代器的实现如下:
struct iterator { using value_type = T; using pointer = T*; using reference = T&; using difference_type = std::ptrdiff_t; using iterator_category = std::bidirectional_iterator_tag; // 双向,不是随机访问
node* ptr;
reference operator*() const { return ptr->data; } pointer operator->() const { return &ptr->data; }
bool operator==(const iterator& o) const { return ptr == o.ptr; } bool operator!=(const iterator& o) const { return ptr != o.ptr; }
// ++ 的语义是跳到 next 节点,而不是地址 +1 iterator& operator++() { ptr = ptr->next; return *this; } iterator operator++(int) { auto tmp = *this; ptr = ptr->next; return tmp; }
// 双向迭代器特有:往回走 iterator& operator--() { ptr = ptr->prev; return *this; } iterator operator--(int) { auto tmp = *this; ptr = ptr->prev; return tmp; }
// 没有 +n、-n、[] - 链表做不到随机访问};
iterator begin() { return {_head->next}; }iterator end() { return {_tail}; }关联类型
对于一个迭代器而言,我们需要首先指定它的五个关联类型,对于这个双向链表的迭代器,它的五个关联信息分别是:
value_type = T:迭代器所指向的元素类型,即链表存储的数据类型。解引用*it返回的就是这个类型的值。pointer = T*:元素的指针类型,即value_type*。operator->()的返回类型就是它,用于支持it->member这样的访问语法。reference = T&:元素的引用类型,即value_type&。operator*()的返回类型就是它,表示解引用后得到的是元素本身的引用,而不是拷贝,所以*it = 42可以直接修改链表中的元素。difference_type = std::ptrdiff_t:表示两个迭代器之间距离的类型。对链表来说虽然没有it1 - it2运算符,但std::distance等算法内部需要用这个类型来计数,所以仍然需要声明。std::ptrdiff_t是标准的有符号整数类型,基本上固定用它即可。iterator_category = std::bidirectional_iterator_tag:声明这个迭代器属于哪个级别。这是五个类型里最关键的一个,STL 算法在编译期通过它做分发——标记为双向迭代器后,std::sort等要求随机访问的算法会在编译期直接报错,std::reverse等只需要双向迭代器的算法则可以正常使用。
必要运算符
以下是实现一个双向迭代器所必须的部件:
- 引用与解引用:
reference和pointer。 - 比较运算符:
==和!=。 - 前进(前向迭代):
++iter和iter++。 - 后退(反向迭代):
--iter和iter--。 - 可以补充的是
const_iterator相关的代码,本文暂时不需要所以省略。
测试
我们可以对于这个双向链表类进行一些简单的测试:
85 collapsed lines
#include <algorithm>#include <ranges>#include <string>#include <print>
template <typename T>class my_list {private: struct node { T data = 0; node *prev = nullptr; node *next = nullptr; }; node* _head; node* _tail;
public: my_list() : _head(new node()), _tail(new node()) { _head->next = _tail; _tail->prev = _head; } void push_back(const T &elem) { node *new_node = new node{elem, nullptr, _tail}; new_node->prev = _tail->prev; _tail->prev->next = new_node; _tail->prev = new_node; }
void push_front(const T &elem) { node *new_node = new node{elem, _head, nullptr}; new_node->next = _head->next; _head->next->prev = new_node; _head->next = new_node; }
void print() { auto current = _head->next; while (current != _tail) { std::print("{} ", current->data); current = current->next; } std::println(); }
~my_list() { auto current = _head->next; delete _head; while (current != _tail) { _head = current; current = current->next; delete _head; } delete _tail; }
struct iterator { using value_type = T; using pointer = T*; using reference = T&; using difference_type = std::ptrdiff_t; using iterator_category = std::bidirectional_iterator_tag; // 注意:双向,不是随机访问
node* ptr;
reference operator*() const { return ptr->data; } pointer operator->() const { return &ptr->data; }
bool operator==(const iterator& o) const { return ptr == o.ptr; } bool operator!=(const iterator& o) const { return ptr != o.ptr; }
// ++ 的语义是跳到 next 节点,而不是地址 +1 iterator& operator++() { ptr = ptr->next; return *this; } iterator operator++(int) { auto tmp = *this; ptr = ptr->next; return tmp; }
// 双向迭代器特有:往回走 iterator& operator--() { ptr = ptr->prev; return *this; } iterator operator--(int) { auto tmp = *this; ptr = ptr->prev; return tmp; }
// 没有 +n、-n、[],链表做不到 O(1) 跳跃 };
iterator begin() { return {_head->next}; } iterator end() { return {_tail}; }};
int main() { my_list<int> lst; lst.push_back(1); lst.push_back(2); lst.push_back(3); lst.push_front(0); lst.push_front(-1); lst.print(); // 输出: -1 0 1 2 3
for (auto iter = std::begin(lst); iter != std::end(lst); ++iter) { // 输出: -1 0 1 2 3 std::print("{} ", *iter); } std::println(); for (auto& iter : lst) { // 输出: -1 0 1 2 3 std::print("{} ", iter); } std::println(); for (auto& iter : std::ranges::reverse_view(lst)) { // 输出: 3 2 1 0 -1 std::print("{} ", iter); } std::println();}如何泛化地对接 STL —— 以 std::advance 为例
std::advance(iter, n) 的作用是让迭代器前进 步。对不同类型的迭代器,最优实现截然不同,我们这里以双向迭代器和随机访问迭代器为例说明:
- random_access:直接
iter += n, - bidirectional:由于不连续,只能循环
++iter / --iter,
如果我们想根据上述标准库的实现方法,自己实现一个可泛化的 my_advance,可以用于和 std::advance 对接,最朴素的想法是运行期 if-else:
// 反面教材:运行期判断template<typename Iter>void my_advance_bad(Iter& iter, int n) { if (/* iter 是 random access?*/) { iter += n; } else { while (n--) ++iter; }}这有两个问题:
- 怎么在运行期判断迭代器类型?
- 更根本的问题是:对
bidirectional迭代器,iter += n根本不能编译——这必须是编译期决策,而不是运行期分支。比如我们在双向链表迭代器中完全没有实现operator+=,那么对于该迭代器,iter += n这行代码在编译阶段一定会报错(尽管我们可以在运行期确保不会走进该分支,但在编译期,编译器完全无法判断iter的类型)。
正确的做法是编译期分发(tag dispatch),而这需要知道迭代器的 iterator_category。因此,自定义的 iterator 泛化对接STL的核心问题就是:如何在编译期得到迭代器的类型信息,也就是我们前文中提到过的五个关联类型(对于 std::advance,最关键的类型信息自然是 iterator_category,这决定了具体调用的方法)。
Typename 关键字
由于我们在迭代器中已经用五个 using 声明了对应的类型信息,现在要在模板中访问迭代器的 category,直觉上写法是:
template<typename Iter>void my_advance(Iter& iter, int n) { Iter::iterator_category tag; // 编译错误?}但是这行代码会编译失败,原因涉及 C++ 模板解析的一个核心规则:dependent name 问题。
依赖名称与二义性
在模板中,Iter::iterator_category 是一个依赖名称(dependent name)——它的含义依赖于模板参数 Iter,在模板实例化之前编译器并不知道 Iter 是什么。
问题在于:Iter::iterator_category 既可以是一个类型,也可以是一个静态成员变量。考虑这两种情况:
struct A { using iterator_category = std::bidirectional_iterator_tag; }; // 类型struct B { static int iterator_category; }; // 变量编译器在解析模板时,面对 T::something,默认假设它是值,而不是类型。因此:
Iter::iterator_category* tag; // 编译器认为 Iter::iterator_category 是值, 这里的 * 会被解析为乘法运算符而非指针这自然是错误的。我们需要显式告诉编译器:iterator_category 是一个类型。
typename 的作用
typename Iter::iterator_category tag;加上 typename,编译器才会正确地将 Iter::iterator_category 解析为类型名(也就是通过 typename 关键词告诉编译器,这里的 Iter::iterator_category 不会是变量,一定是类型)。
类型萃取(Type Traits)
现在我们有了 typename,可以写出 typename Iter::iterator_category 来表示迭代器的类型。但还有一个场景没有解决:原生指针。比如 int* 也是合法的随机访问迭代器(可以 ptr + n、ptr[i]),但它没有任何成员类型,int*::iterator_category 根本不存在。
这就是 std::iterator_traits 要解决的问题。
类型萃取的设计逻辑 - 以 iterator_traits 为例
// 通用版本:直接转发迭代器自身定义的五个关联类型template <typename Iter>struct iterator_traits { using value_type = typename Iter::value_type; using pointer = typename Iter::pointer; using reference = typename Iter::reference; using difference_type = typename Iter::difference_type; using iterator_category = typename Iter::iterator_category;};这里体现了模板编程的核心设计思路:用一个中间层结构体统一对外暴露类型信息,使得外部代码无需关心不同迭代器的内部实现差异,始终通过 iterator_traits<Iter>::xxx 这同一种方式访问所需的类型。也就是设置一个中间层,将类型的差异封装在萃取类内部,对外提供统一的访问接口。
对于 my_list::iterator 这样规范的自定义迭代器,通用版本的 iterator_traits 直接生效。
对原生指针的偏特化
// 对 T* 的偏特化:手动填充这些信息template <typename T>struct iterator_traits<T*> { using value_type = T; using pointer = T*; using reference = T&; using difference_type = std::ptrdiff_t; using iterator_category = std::random_access_iterator_tag;};这就是类型萃取设计的精华所在:通过模板偏特化,为没有内嵌成员类型的类型(比如原生指针)补充元信息,使其融入统一接口。
调用方永远只需要写 iterator_traits<Iter>::iterator_category,无论 Iter 是自定义迭代器还是 int*,都能正确得到 category。
别名模板:告别冗长的 typename
经过前面的铺垫,我们写出了正确的类型访问方式:
typename std::iterator_traits<Iter>::iterator_category这行代码每次使用都要写这么长,很繁琐。C++11 引入的别名模板(alias template)可以解决这个问题:
template<typename Iter>using iter_category_t = typename std::iterator_traits<Iter>::iterator_category;此后直接写 iter_category_t<Iter> 即可,typename 被封装进别名定义里,使用侧不再需要重复书写。
_t 惯例
这种把 typename ...::type 包装成 ..._t 的写法,从 C++14 起被标准库大量采用。例如:
// C++11 写法typename std::remove_const<T>::typetypename std::decay<T>::type
// C++14 起的 _t 别名std::remove_const_t<T>std::decay_t<T>对于 iterator_traits,标准库本身没有直接提供 _t 别名,但 C++20 引入了一套更现代的迭代器工具:
std::iter_value_t<Iter> // = iterator_traits<Iter>::value_typestd::iter_reference_t<Iter> // = iterator_traits<Iter>::referencestd::iter_difference_t<Iter> // = iterator_traits<Iter>::difference_type注意:C++20 的这套别名底层实现比
iterator_traits更复杂,能处理更多边缘情况(如 range 的迭代器),但核心思想是一致的。
在 C++14/17 环境下,按需自定义 _t 别名是常见做法,也是标准库的编写惯例。
my_advance 的完整实现
有了 iterator_traits,我们可以实现完整的 my_advance:
// 针对 bidirectional 的实现:逐步前进template<typename Iter>void my_advance_impl(Iter& iter, int dist, std::bidirectional_iterator_tag) { std::println("bidirectional_iterator, step forward {}.", dist); if (dist > 0) { while (dist--) ++iter; } else { while (dist++) --iter; }}
// 针对 random_access 的实现:O(1) 跳跃template<typename Iter>void my_advance_impl(Iter& iter, int dist, std::random_access_iterator_tag) { std::println("random_access_iterator, step forward {}.", dist); iter += dist;}
// 别名模板template<typename Iter>using iter_category_t = typename std::iterator_traits<Iter>::iterator_category;
// 对外接口:通过 iterator_traits 萃取 category,构造 tag 对象传入template<typename Iter>void my_advance(Iter& iter, int dist) { my_advance_impl(iter, dist, iter_category_t<Iter>());}这里有几个细节值得注意:
typename再次出现:typename std::iterator_traits<Iter>::iterator_category——Iter是模板参数,所以iterator_traits<Iter>::iterator_category是 dependent name,必须加typename。- Tag 对象:
iter_category_t<Iter>()构造了一个 tag 类型的临时对象,但这个对象本身没有任何数据,它只是作为类型信息的载体传递给重载函数,让编译器选择正确的my_advance_impl版本。整个分发过程发生在编译期,零运行时开销。 - 继承的作用:
random_access_iterator_tag继承自bidirectional_iterator_tag。如果我们没有为bidirectional提供特化,编译器会自动匹配到bidirectional的版本(因为 random_access tag 可以隐式转换为 bidirectional tag)。这使得 tag dispatch 天然地支持”能力降级”。
验证一下效果:
int main() { my_list<int> lst; lst.push_back(1); lst.push_back(2); lst.push_back(3); lst.push_front(0); lst.push_front(-1); lst.print(); auto it2 = std::begin(lst); my_advance(it2, 2); std::println("{}", *it2); // 1 my_advance(it2, -1); std::println("{}", *it2); // 0
int* arr = new int[10]; std::iota(arr, arr + 10, 0); auto arr_iter = arr; my_advance(arr_iter, 5); std::println("{}", *arr_iter); // 5 my_advance(arr_iter, -2); std::println("{}", *arr_iter); // 3}my_list 的迭代器走了 bidirectional 分支,原生指针走了 random_access 分支,完全符合预期。
用这一套 iterator_traits 的设计方案,用户只需要定义好五个关联类型,自定义的迭代器就能够对接所有可用的STL方法了。