迭代器设计思路
引言
本篇主要思路总结一下《STL源码剖析》第三章中迭代器设计的知识,看得出是好书,可惜目前还没计划开始看STL源码,第一章allocator直接看不懂,迭代器因为之前项目参考的开源代码有涉及因此只能硬着头皮看,最近有面试问倒了干脆就重新拿出来总结一下。迭代器的设计中一个比较关键的部分是traits设计模式的理解。
指针行为的模拟
迭代器是一种类似指针行为的对象, 因此肯定需要对*
解引用运算符和->
箭头运算符进行重载. 因外迭代器至少要支持++
运算符移动迭代器所指容器中的元素, 以及==
, !=
, 支持迭代器对所指元素地址的比较, 这5个运算符是迭代器必须重载的. 再次总结一下5个重载运算符的功能
*
运算符:解引用运算符,返回迭代器指向对象实例,返回类型为指向对象的引用(《C++ Primer》P504)->
运算符:箭头运算符,返回迭代器指向对象指针,返回类型为指向对象指针(《C++ Primer》P505)++
运算符:返回指向容器下一个对象的迭代器,注意++
分前置和后置:- 前置
++
返回类型迭代器引用,函数声明形如ListSimpleIter<T>& operator++()
(《C++ Primer》P502) - 后置
++
返回类型旧迭代器拷贝,函数声明形如ListSimpleIter<T> operator++(int)
,多带一个防止重复定义的int
参数(《C++ Primer》P503)
- 前置
++
:判断迭代器指向的对象地址是否相等!=
:判断迭代器指向对象地址是否不相等
下面给出一个基于《数据结构(邓俊辉)》P69, List
数据结构的一个非标准的简单迭代器实现(更像是模拟了指针的行为), 重载了上面5个运算符:
#include "List.h"
namespace TSH_DS
{
/**
* @brief List类型迭代器的一个简单实现, 没有任何继承关系
*
* @tparam T List类型的数据类型
*/
template <typename T>
class ListSimpleIter
{
public:
/**
* @brief Construct a new List Simple Iter object
* List迭代器的默认构造函数, 输入List中迭代器所指节点位置
*
* @param node
*/
ListSimpleIter(ListNodePosi(T) node=NULL) :
_iter(node)
{}
/**
* @brief 解引用符号函数重载, 默认是返回ListSimpleIter
* 自身对象引用, 现在需要返回迭代器所示节点内容对象的引用
*
* @return T& 返回当前迭代器指向节点内容的引用
*/
T& operator*() const
{return _iter->data;}
/**
* @brief 箭头符号函数重载, 默认是返回ListSimpleIter
* 自身对象地址, 现在需要返回迭代器所示节点内容对象的地址
* 即等价于:
*
* @code
* ListSimpleIter iter;
* iter.operator()->mem; // 返回T类型的成员mem, 等价于下面的语句
* (*iter).mem; // 解引用符号已经被重载, 返回了节点T类型内容的引用, 再取成员mem
* @endcode
*
* @return T* 返回当前迭代器所指向节点内容的地址
*/
T* operator->() const
{
return &this->operator*(); // 调用解引用运算符
}
/**
* @brief List简单迭代器前置递增符号函数重载
* 向所指的List下一节点进行移动
*
* @return ListSimpleIter&
*/
ListSimpleIter<T>& operator++()
{ _iter = _iter->succ;}
/**
* @brief List简单迭代器后置递增符号函数重载
* 向所指的List下一节点进行移动
*
* @return ListSimpleIter&
*/
ListSimpleIter<T>& operator++(int)
{_iter = _iter->succ;}
/**
* @brief 比较相等符号函数重载, 比较的是指向节点的地址
*
* @param iter
* @return true
* @return false
*/
bool operator==(const ListSimpleIter<T> &iter) const
{return _iter == iter._iter;}
/**
* @brief 比较不相等符号函数重载, 比较的是指向节点的地址
*
* @param iter
* @return true
* @return false
*/
bool operator!=(const ListSimpleIter<T> &iter) const
{return !this->operator==(iter);}
private:
ListNodePosi(T) _iter; ///<@brief 迭代器所指链表位置
};
} // namespace TSH_DS
简单的一个测试代码如下:
#include <cstdio>
#include <cstdlib>
#include <TSH_DS/List/List.h>
#include <TSH_DS/List/ListSimpleIter.h>
#define TEST_ID 0
/// 以InputIterator为传入模板,T对迭代器指向容器中具体对象类型
template <class InputIterator, class T>
InputIterator myfind(InputIterator first, InputIterator last, const T &value)
{
while (first != last && *first != value)
first++;
return first;
}
int main()
{
printf("==== Test %2d. Generate a random vector and test simple iterator.\n", TEST_ID);
TSH_DS::List<int> mylist;
int num = 10;
for (int i=0; i<num; i++)
mylist.insertAsLast(rand()%10);
for(int i=0; i<mylist.size(); i++)
printf("%d, ", mylist[i]);
printf("\n");
TSH_DS::ListSimpleIter<int> begin(mylist.first());
TSH_DS::ListSimpleIter<int> end(mylist.last()->succ); // ListSimpleIter cannot deal with header and tailer
TSH_DS::ListSimpleIter<int> iter;
for (int i=0; i<5; i++)
{
int target = rand() % 10;
iter = myfind(begin, end, target);
if (iter!=end)
printf("Find target: %2d\n", target);
else
printf("Not find target: %2d\n", target);
}
return 0;
}
首先,从设计上来说,容器中每个元素可能会对储存对象再进行一次封装,比如上面代码中需要对模板对象T
封装一层ListNode
, 这种内部对象的数据结构不应该暴露给客户端程序, ListSimpleIter
因为遍历节点的需要又不得不使用它, 每个数据结构实现自己遍历的方式又不太一样(可能有自己TreeNode
等单位数据结构), 所以迭代器最好是定义和实现在容器的内部, 不同容器迭代器一般不通用。以std::vector
为例,我们声明一个std::vector
迭代器类型为:
std::vector<int> vec {1, 2, 3};
std::vector<int>::Iterator it = vec.begin();
std::vector<T>::Iterator
就是在std::vector<T>
中定义的迭代器类型,在STL中每一种容器内部都定义了自己专属的迭代器。
另外,需要注意一下myfind
这个函数,接受迭代器类型InputIerator
和指向对象类型T
,可以发现T
就是InputIterator
指向的容器中存储对象类型(value_type
),和迭代器类型实际上是有很大的关系的,我们将这种和迭代器有很大关系的类型称为:迭代器的相关类型。
迭代器traits编程
traits设计模式
traits设计模式主要是为了获得模板类型中的关联类型, 比如迭代器所指容器中元素数据的类型(value_type
), 当迭代器对于STL算法作为一个模板template <class I>
传进来的时候, 我们对于模板类I
一无所知,甚至编译器只有在使用该模板函数的时候才会对其实例化,迭代器I
所指对象类型即value_type
我们是不知道的, 最简单的方法就是在迭代器中定义一个对数据类型的typedef
:
然后在调用迭代器的泛型算法中,我们不直接获得迭代器的typedef
, 而是用一个iterator_traits
, 以迭代器类型作为模板I
输入, 通过typedef
提取出value_type
:
typedef
中的typename
显示指明了这是一个嵌套类型(模板I
没有实例化,我们也不能知道value_type
是类型中定义的类型别名还是静态成员).
为什么看似多此一举, 不是使用iterator
中自己定义的iterator::value_type
,而是需要使用iterator_traits
先封装一次, 再转换给STL算法, 是因为iterator_traits
能够保证输入STL算法的类型为指针时也能正常运行, 为此, iterator_traits
对上面定义的模板做了一个进一步的限制, 为普通指针类型专门 偏特化(partial specialization) 出来一个版本:
由此iterator_traits
也能够提取出普通指针的关联类型value_type
, 这时候到STL算法中, 先使用iterator_traits
提取出输入迭代器/普通指针模板中的关联类型, 再使用这些类型进行数据比较的操作, 就没有问题了:
iterator_traits涉及的关联类型
Iterator
和iterator_traits
中定义的相关类型当然不止value_type
,常用的关联类型一共有5种,换句话说,一个符合规范的iterator
中必须定义自己的以下几种关联类型:
template <class I>
class iterator_traits<I> {
typedef typename I::value_type value_type;
typedef typename I::difference_type difference_type;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
typedef typename I::iterator_catergory iterator_catergory;
};
template <class T>
class iterator_traits<T*> {
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
typedef random_access_iterator iterator_catergory;
};
value_type
这个没什么好说的, 就是迭代器所指数据的类型, 比如:
std::vector<int>::iterator::value_type == int
difference_type
表示两个迭代器之间距离的类型, STL算法count()
提供计数功能, 那么返回的就是这个类型:
对于C++内置指针的距离类型ptrdiff_t,
在<cstddef>
头文件中, 普通指针和const
指针的iterator_traits
的特偏化也是直接定义了这个类型:
reference_type
迭代器所指数据类型的引用类型, 这个一般就是T&
了, 对于普通指针和const
指针iterator_tarits
会有区别
普通指针T中iterator_traits
的偏特化reference_type
typedef为:
typedef T& reference_type
而const
指针中为了防止所指数据返回引用被修改, 定义为:
typedef const T& reference_type
pointer_type
和reference_type
差不多, 对于一般数据类型的指针类型是T*
, 普通指针和const
指针iterator_tarits
会有(顶层)const
之分:
typedef T* pointer_type
typedef const T* poiner_type
iterator_cartegory
用于表示迭代器的类型,这是由具体迭代器所属容易特性来决定的, 非常重要, 决定了STL算法重载的函数以及程序运行的效率, 一共有5种:
input_iterator:
只读迭代器(不许被外部程序改变)ouput_iterator
: 只写迭代器forward_iterator
: 前向迭代器, 只允许++
操作符 (forward_list
)bidirectional_iterator
: 双向迭代器, 允许++
,--
操作符 (list
)random_access_iterator
: 随机访问迭代器, 允许++
,--
,p+n
,p-n
,p[n]
,p1<p2
五个迭代器具有如下的继承关系(最顶上是父类):
STL针对不同的迭代器类型进行函数的重载, 而不是利用标志位进行判断(比如说函数内使用if
或者switch
判断具体执行版本), 这可以在编译时而不是运行时决定调用函数的版本, 以提高效率. 重载决断是根据输入形参的类型来决断的(重载决议最佳匹配原则,《C++ Primer》P209), 因此需要为上面5种迭代器类型定义成标记用的类型(tag types
):
在STL算法中, 具体需要重载的算法(比如__advance
P95), 会把tag_types
作为一个占位类型输入, 仅用来判断重载的函数版本, 最后使用一个不带tag_types
的函数(比如advance
P96)封装重载算法的接口.
tag_types
使用了上图相同的继承关系图, 这可以防止“单纯传递调用的函数”(说人话不用为每一种iterator_cartergory
标志类型都定义重载类型,没有定义对应重载类型的标记类型可以向上转型调用父类的重载版本), 比如__advance
可以不一定forward_iteration_tag
, 因为它可以兼容父类input_iterator_tag
的重载版本.
std::iterator
对于每一个想要兼容STL算法的迭代器, 上面5个关联类型必须都要提供, 自定义的iterator
需要继承std::iterator
, 帮助补全默认的嵌套关联类型:
std::iterator
里面啥也没有, 就是typedef了5个嵌套的关联类型. 其中 iterator_category
和value_type
必须是用户显式给到继承的std::iterator
模板中的 。 这样定义自己的迭代器时候通过对std::iterator
模板类实例化后继承, 这些关联类型就不用再typedef了. 当然, 必要的操作符: ++
, ==
, !=
, --
等等运算符还是要自定义的, 具体他们的实现这是由容器本身的特性决定的.
总结
迭代器traits设计框架是怎么样的
迭代器的traits萃取了迭代器的五种关联类型:
value_type
: 迭代器指向数据的类型difference_type
: 迭代器间距离类型, 默认是指针的距离类型ptrdiff_t
reference_type
: 数据引用类型T&
, 注意防止修改数据的const T&
类型pointer_type
: 数据指针类型T&
, 注意防止数据修改的const T*
类型iterator_cartegory
: 迭代器类型, 一共5种, 4层继承关系: 只读/只写–>前向–>双向–>随机
首先, 对于一个STL算法兼容的迭代器, 必须包含上面5个关联类型的声明, 即必须有内嵌类型:
typename IteratorType::value_type
typename IteratorType::difference_type
typename IteratorType::reference_type
typename IteratorType::pointer_type
typename IteratorType::iterator_cartegory
这个可以通过继承std::iterator
类完全补全声明,以满足STL泛型算法对于迭代器调用的规范:
template <T>
class IteratorType: public std::iterator<<std::forward_iterator_tag, T>
{...};
注意定义的IteratorType
中必须至少重载*
, ->
, ++
, ==
, !=
操作,一般来说它最菜也是一个前向迭代器.
对于一个STL的泛型算法, 首先会将迭代器作为模板接受, 可以指定具体允许到哪种迭代器类型iterator_catergory
, 以std::find
声明为例:
template <class InputIterator, class T>
InputIterator find (InputIterator first, InputIterator last, const T& val);
对于可以通过不同迭代器类型进行优化的情况(advance
), 可以在原型实现中使用重载决议iterator_catergory
作为参数进行重载
在泛型算法中, 如果要使用迭代器的关联类型, 可以使用std::iterator_traits<I>
萃取, 比如上面的iterator_catergory
参数:
iterator_traits<IteratorType>::iterator_catergory
为了让支持迭代器的泛型算法同时支持指针, std::iterator_traits<I>
有对原始指针的特偏化声明:
template< class T >
struct iterator_traits<T*>;
为什么要迭代器
在泛型算法使用迭代器进行操作时, 需要使用到其相关的类型, 比如指向数据的类型, 迭代器本身的类型(是否支持随机访问, 是否支持后向遍历…), 这些需要迭代器类型内部对其typedef, 然后使用typename Iterator<...>::XXX
进行访问.
但是泛型算法应该支持原始指针, 原始指针里是没有这些关联类型的, 因此需要traits,
一方面traits会通过typedef I::XXX XXX
的方法萃取迭代器的相关类型, 通过iterator_traits<I>::XXX
获得类型
另一方面, 使用template< class T > struct iterator_traits<T*>
;对指针进行特偏化, 通过iterator_traits<T*>::XXX
也能获得对应的相关类型
泛型算法如何使用迭代器类型调用不同的算法实现
使用重载决议编译时确定, 而不是if...else...
这种运行时判断, 可以加速STL的效率. 具体的, 在底层函数声明时, 将迭代器类型作为形参列表的一个参数, traits获得即iterator_traits<IteratorType>::iterator_catergory
, 不同的迭代器类型可以调用不同的重载实现. 获得最大的效率
参考文献
iterator_traits
文档std::iterator
文档- 《STL源码剖析》
- 《C++ Primer》
- 《数据结构(C++语言版)》