前言
本系列文章所学习的Github上基于C++11的开源项目MyTinySTL,项目地址为:(https://github.com/Alinshans/MyTinySTL),感谢Alinshans大佬开源这个优质的学习项目。
一、什么是迭代器
无论是序列容器还是关联容器,最常做的操作无疑是遍历容器中存储的元素,而实现此操作,多数情况会选用“迭代器(iterator)”来实现。那么,迭代器到底是什么呢?
我们知道,尽管不同容器的内部结构各异,但它们本质上都是用来存储大量数据的,换句话说,都是一串能存储多个数据的存储单元。因此,诸如数据的排序、查找、求和等需要对数据进行遍历的操作方法应该是类似的。
既然类似,完全可以利用泛型技术,将它们设计成适用所有容器的通用算法,从而将容器和算法分离开。但实现此目的需要有一个类似中介的装置,它除了要具有对容器进行遍历读写数据的能力之外,还要能对外隐藏容器的内部差异,从而以统一的界面向算法传送数据,于是迭代器就产生了。简而言之,在STL中,迭代器就是算法得以访问容器中数据的桥梁。
我们再从代码的角度上去看,迭代器和 C++ 的指针非常类似,其实迭代器在代码层面就是一个行为类似于指针的对象,即①iterator是个对象 ②iterator这个对象具有指针的行为。
在C++中,STL为每一种标准容器定义了一种迭代器类型,这意味着,不同容器的迭代器也不同,其功能强弱也有所不同。
容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。
而常用的迭代器按功能强弱分为输入迭代器
、输出迭代器
、前向迭代器
、双向迭代器
、随机访问迭代器
5 种。
//五种迭代器
struct input_iterator_tag {}; //输入迭代器
struct output_iterator_tag {}; //输出迭代器
struct forward_iterator_tag : public input_iterator_tag {};//单向迭代器
struct bidirectional_iterator_tag : public forward_iterator_tag {};//双向迭代器
struct random_iterator_tag : public bidirectional_iterator_tag {};//随机迭代器
二、什么是相关类型
既然迭代器是算法访问容器中数据的桥梁,那么如果此时有一个算法需要知道所指向的对象的类型,我们怎么知道这个容器的“肚子”里都装了些什么?假如有如下这个代码:
vector<xxx> vec = {1,2,3};
auto it = vec.begin();
cout << *it << ' '; //输出1
++it;
cout << *it << endl; //输出2
我们可以对vector的迭代器通过解引用,来访问容器vec中的数据1,2,3。但现在的问题是,我们应该如何获取这个类型xxx?
在算法中,我们可能需要知道这个类型,以便对算法进行优化,所以就需要有一种方法,能让我们查询到迭代器的associated_type(相关类型),以满足我们的需要。
- 迭代器的常用的(算法会询问迭代器的)5种associated_type:
//iterator 模板 定义一个统一的接口
template<class Category, class T, class Distance = ptrdiff_t//ptrdiff_t是减去两个指针的结果的带符号整数类型
, class Pointer = T*, class Reference = T&>
struct iterator
{
typedef Category iterator_category;//迭代器的种类
typedef T value_type;//值的类型
typedef Pointer pointer;//指针
typedef Reference reference;//引用
typedef Distance difference_type;//两个迭代器之间的距离
};
从代码中我们可以知道,每个iterator必须提供5个统一的类型的别名,分别是iterator_category
、value_type
、pointer
、reference
、difference_type
。它们用来回答以下的问题:
- iterator_category:迭代器的类型(决定了迭代器可以怎么走,例如vector的迭代器可以随机访问,而链表的迭代器一次只能走一步);
- value_type:迭代器所指容器中元素的类型;
- difference_type:迭代器距离范围(即迭代器相减结果的范围)对应的类型,一般是内置类型ptrdiff_t(如果容器的容量范围是[0,2^32-1],则可以将difference_type定义为unsigned int);
- reference:迭代器所指容器中元素的引用类型;
- pointer:迭代器所指容器中元素的指针类型;
每个容器在实现自己的iterator时继承这个基类,为自己的5种associated_type赋上实参(通过typedef),这样就避免了容器实现自己的迭代器时接口不统一。
三、如何知道相关类型?iterator_traits的作用
我们已经知道了算法需要根据迭代器的5种associated_type来对迭代器所指容器做出最佳操作这个需求。那么现在,我们来了解一下算法与迭代器之间的问答机制,即迭代器是如何解决算法的需求。
我们知道iterator根据有没有能力定义自己的5种associated_type可以分为class iterator和non-class iterator(即native pointer,原生指针),class iterator可以通过直接问答机制回答算法的问题,而原生指针不可以。
那么有没有一个办法,这两种iterator输入进去,都可以得到相应的associated_type呢?
那么iterator_traits就闪亮登场了!
由上图可知,通过类模板偏特化的作用,不论是原生指针或者class iterator都可以让外界方便的得到其associated_type。
它的实现如下:
// iterator traits
template <class T>
struct has_iterator_cat
{
private:
struct two { char a; char b; };
template <class U> static two test(...);//C++允许定义形参个数和类型不确定的函数,不确定的形参可以使用省略号
template <class U> static char test(typename U::iterator_category* = 0);//Typename关键字 告诉编译把一个特殊的名字解释成一个类型
public:
static const bool value = sizeof(test<T>(0)) == sizeof(char);
};
template <class Iterator, bool>
struct iterator_traits_impl {};
template <class Iterator>
struct iterator_traits_impl<Iterator, true>
{
typedef typename Iterator::iterator_category iterator_category;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
typedef typename Iterator::difference_type difference_type;
};
//iterator_traits_helper类的实现是两个模板偏特化
//如果是value是true就使用继承iterator_traits_impl类的版本
//如果是false就使用空类体的版本,不执行任何操作
template <class Iterator, bool>
struct iterator_traits_helper {};
template <class Iterator>
struct iterator_traits_helper<Iterator, true>
: public iterator_traits_impl<Iterator,
std::is_convertible<typename Iterator::iterator_category, input_iterator_tag>::value ||//std::is_convertible
//判断:参数一是否能转化成参数二的类型
std::is_convertible<typename Iterator::iterator_category, output_iterator_tag>::value>//是否能将传入的类型转换成迭代器
//如果能,就返回true,反之false
{
};
// 萃取迭代器的特性
template <class Iterator>
struct iterator_traits
: public iterator_traits_helper<Iterator, has_iterator_cat<Iterator>::value> {};
// 针对原生指针的偏特化版本
template <class T>
struct iterator_traits<T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef ptrdiff_t difference_type;
};
template <class T>
struct iterator_traits<const T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef const T* pointer;
typedef const T& reference;
typedef ptrdiff_t difference_type;
};
由源码可知,iterator_traits类的实现为一个泛化版本+两个特化版本,泛化版本用于处理class iterator,特化版本用于处理native pointer和const native pointer。
我们还可以从源码中发现,偏特化版本只定义了5个类型别名,而泛化版本除此之外还有一系列的预处理函数,接下来我们就来了解一下iterator_traits是如何知道任意一个输入迭代器的associated_type的。
iterator_traits类在做的事情就是:接受一个迭代器类型(Iterator/T * /const T * )后,通过typedef的方法在自己的类中定义类型别名,将输入的迭代器的associated_type提取出来(即typedef后,iterator_traits类中的associated_type和输入的迭代器的associated_type就是相同的了)。
template <class Iterator>
struct iterator_traits_impl<Iterator, true>
{
typedef typename Iterator::iterator_category iterator_category;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
typedef typename Iterator::difference_type difference_type;
};
其中class iterator因为在类 iterator 内定义了5种associated_type,所以直接typdedef即可,而non-class iterator(native pointer)则需要做一些处理:
①如果传入的是一个普通的原始指针T*,那它所指容器种的对象就是T类型的,所以将value type重命名为T,其他同理。
②如果传入的是一个const的原始指针const T*,此时要注意!其所指容器中的对象是T类型而不是const T!!,这里地方需要解释一下,为什么指针定义为指向常量的指针,但容器中元素的类型却没有定义为const T,这是因为如果在定义中就将元素类型定义为const,那这些元素就无法被赋初值了,也就失去了意义,所以在这里的实现是将元素定义为T类型,指针仍是const T*,这样元素即可以在初始化时被赋值,又避免了用户通过迭代器对其进行修改。
//情况一
template <class T>
struct iterator_traits<T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef ptrdiff_t difference_type;
};
//情况二
template <class T>
struct iterator_traits<const T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef const T* pointer;
typedef const T& reference;
typedef ptrdiff_t difference_type;
};
这样我们就可以通过iterator_traits获得任意迭代器的associated_type了,如使用:typename iterator_traits<Iterator>::value_type
。
既然这样就可以了,那为什么该源码的实现中,还为泛化版本写了那么多辅助函数呢?我们来看看源码的泛化版本中,是谁调用了这个核心模块。
我们可以看到,源码中泛化版本的iterator_traits类的定义,其实就是继承了iterator_traits_helper这个类,为什么要这么做?
template <class Iterator>
struct iterator_traits
: public iterator_traits_helper<Iterator, has_iterator_cat<Iterator>::value> {};
可以理解成为实际上实现traits功能的类是iterator_traits_helper,但是它有两个模板参数而且类名也不符合STL标准,即最终的对外接口必须是struct iterator_traits {}
template <class Iterator, bool>
struct iterator_traits_helper {};
template <class Iterator>
struct iterator_traits_helper<Iterator, true>
: public iterator_traits_impl<Iterator,
std::is_convertible<typename Iterator::iterator_category, input_iterator_tag>::value
||std::is_convertible<typename Iterator::iterator_category, output_iterator_tag>::value>{};
iterator_traits_helper类的实现是两个模板偏特化,根据has_iterator_cat类的value值,来决定使用哪个helper函数。如果是value如果是true就使用继承iterator_traits_impl类的版本,如果是false就使用空类体的版本,不执行任何操作。
而由has_iterator_cat这个类名,我们不难猜出,这一层的意思是,如果传入的这个Iterator有iterator_category,就执行iterator_traits_impl类,进行最后的萃取。如果是false,它就不是个迭代器也就没必要萃取了。
那么has_iterator_cat类又是如何实现的呢?它为什么可以知道一个迭代器有没有iterator_category?让我们来看一下它的源码:
template <class T>
struct has_iterator_cat
{
private:
struct two { char a; char b; };
template <class U> static two test(...);
template <class U> static char test(typename U::iterator_category* = 0);
public:
static const bool value = sizeof(test<T>(0)) == sizeof(char);
};
这里的处理十分巧妙!has_iterator_cat类利用SFINAE机制( 当一个模板展开失败的时候, 会尝试用其他的重载进行展开, 而不是直接报错,),使编译器根据迭代器有还是没有iterator_catefory类型成员,分别送入两个test函数,这两个test函数并不会执行,而是在编译期间,通过test函数的返回值判断迭代器进了哪个函数,从而判断迭代器有没有iterator_category类。
- 第一个test函数因为形参是(…),它的意思是这个函数能够接受任意个任意类型的参数,该函数返回值为two。
- 第二个函数拥有一个
U::iterator_category
类型的形参(这么写就是假设U必须有iterator_category),该函数返回值为char。
接下来我们就看test< T >(0)会如何在SFINAE规则下作用于这两个函数。在这个表达式中,尝试用T去代替U,此时如果T::iterator_category
是有效的,当我们传入T时,则进入第二个test函数,此时这个函数虽然不会执行,但是在编译期间,我们就可以知道它的返回值为char类型,从而使value为ture。否则就会进入第一个函数,这时函数返回类型为two,sizeof(two)=2*sizeof(char),从而使value为false。
于是最后,我们再回到源码上发现:当传入一个类型T时,会首先在has_iterator_cat中萃取,如果类型T拥有iterator_category,那么返回一个true,这时iterator_traits_helper会判断其基类iterator_traits_impl,在iterator_traits_impl中如果类型T是5种迭代器之一的话,返回一个true值,typedef出5个associated_type。否则不执行任何操作。
最后
这是一篇记录学习收获和疑惑的帖子,文章的内容,参考自他人的资料以及个人当下的理解,表述并不一定正确。如有错误,还望提出您宝贵的意见。