MyTinySTL学习之迭代器:iterator.h(二)
- 【引言】在上一篇文章中,我们分析了iterator_traits实现的相关代码,包括它的associated type是什么?有什么用?以及如何定义一个iterator_traits类使其能够萃取任意一个迭代器。那么,这一节我们就来看如何使用iterator_traits来帮助我们萃取迭代器的associated type(由于iterator_traits的存在,得以实现的一些函数)、type_traits的实现与使用以及iterator_traits和type_traits技法给我们的程序带来了什么收益。
一、Type_traits.h
-
你可能会疑惑,我们不是在讲iterator.h吗?怎么突然来了个type_traits.h,让我们来看iterator.h中紧接着iterator_traits实现的一段代码。
从命名上我们可以很容易地看出这段代码的功能:判断迭代器类型T具体是哪个iterator_category,但在实现方式上却有点奇怪:判断类型为什么用类实现而不是函数呢?空的类又没返回值,它是怎么帮助判断呢? 先讲结论,这是用了
type_traits的技法,该技法的核心是定义一些typedef(这些别名的名字就是我们想要判别的问题),其值不是__true_type就是__false_type(即两个表示真假的类),通过它我们就可以知道任意型别(类别)是否有某些特性。接下来,让我们从源码中来理解这个type_traits技法的作用。从图片中具体函数的实现可以看出各个类型迭代器的判断类都是继承了has_iterator_cat_of这个类,这些迭代器类接受一个迭代器类型和一个确定的实参(代表迭代器类型的5个空类)传给has_iterator_cat_of类。那has_iterator_cat_of类做了什么呢?我们来仔细看一下它的实现:
可以看出,该类有两个版本,如果第三参数是false,它就直接继承m_false_type类,如果是true,它就继承m_bool_constant类,这代表什么呢?让我们看一下这两个类的实现:
从上图可以看出,当m_bool_constant类的模板类型实参是true时,我们将其重命名为m_true_type,反之重命名为m_false_type。那m_bool_constant模板类又是什么呢?让我们看一下上面的using代码,通过20-21行我们可以知道,m_nool_constant是m_integral_constant类的别名(c++11的用法,using 为模板类定义别名:using 别名=模板类),再看上面m_integral_constant的定义,当bool类型的变量b(真的bool类型的true/false)传入该类后,该类会声明一个静态常量value,其值初始化为b。这样我们就大概知道了
m_bool_constant类的作用了,该类接受一个bool型的实参(其他类继承了m_bool_constant类,所以它是接口),然后传给m_integral_constant类初始化一个值为实参值的静态常量value,最后再将T具体化为true/false的m_integral_constant类重命名为m_true_type/m_false_type。在知道该类的作用后,我们再回到has_iterator_cat_of的泛化版本中看一看继承了该类后,has_iterator_cat_of类有什么作用。我们可以发现,has_iterator_cat_of类接受一个迭代器类型T和一个代表迭代器可选类型的迭代器类型U,以及上一节我们讲过的类has_iterator_cat的返回结果。首先根据第三个参数判断进泛化or特化版本,然后通过is_convertible判断T能够转化为U,如果可以就返回true,否则返回false。这样has_iterator_cat_of类最终就变成了继承m_false_type/m_true_type。看到这里,整个结构就一目了然了,5个迭代器类别的判别类继承has_iterator_cat_of,而has_iterator_cat_of是一个辅助函数,首先判断T是否有iteratory_category,如果没有就继承m_false_type(调用这个类的函数会有一个判断,如果是m_false_type就不用再看它具体是什么类型了,因为它压根就不是迭代器)。当has_iterator_cat_of的第三参数为ture时,就会进入继承m_bool_constant类的版本,然后根据T是否可以转化成U来给m_bool_constant传入实参true/false,即T是迭代器且可以转化为U,就继承m_bool_constant<true>,即继承m_true_type,否则继承m_false_type。所以最终这5个判别空类就实现了判别T是不是U这个类型的判别功能,如果是,该类的静态常量就是true,否则就是false。看到这里,我想你应该明白了,这一大堆空的类,在编译期都会转化为以下格式:
template <class Iter> struct is_input_iterator : public has_iterator_cat_of<Iter, input_iterator_tag> {}; 变为 template <class Iter> struct is_input_iterator : public m_bool_constant<true>/m_bool_constant<false> {}; 变为 template <class Iter> struct is_input_iterator : public m_true_type/m_false_type {}; //而m_true_type/m_false_type又分别是模板类m_integral_constant接受实参true和false时实例类的别名,该类有一个静态常量 //所以最终的结果就是is_input_iterator有一个静态常量,其值为static constexpr true/static constexpr false,即 template <class Iter> struct is_input_iterator : public m_integral_constant { static constexpr true/static constexpr false; }; -
从上面的源码分析中,我们已经可以看出
type_traits的作用:将判别类(都是一些继承/typedef出的最后体内只有static constexpr true/false的一些类,eg.is_input_iterator类、has_trivial_default_constructor类)typedef为__true_type类和__false_type类,这样使得其在编译期间就可以通过模板函数对class object的反向推导机制,推断出这个判别结果(即判别结果的真假,要回答的问题就是这个判别类的名字所问的问题)是true/false(通过推导出判别类最终是继承了__true_type类还是__false_type类),这样编译器就可以在同个功能的多个重载函数中做出最佳选择。那么这一节就来讲讲type_traits在编译期间帮我们完成函数派送的决定能够带来什么收益,以及type_traits自身是如何实现的。首先来说为什么需要type_traits,那是因为当算法通过迭代器操作容器时,如果可以知道容器中对象的一些特性,那么就可以选择更为高效的实现方法。例如,我们在对一个对象进行拷贝时,我们就可以通过type_traits去检查它的构造函数是否重要。
typedef __false_type has_trivial_default_constructor;//如果是false,就说明重要(双重否定) typedef __false_type has_trivial_copy_constructor; typedef __false_type has_trivial_assignment_constructor;首先,我们会为拷贝操作实现几个版本的重载函数,用拷贝构造、构造、memcpy等,然后在编译期间通过type_traits来萃取出容器对象的特性,如果拷贝构造重要就用拷贝构造,如果构造函数都不重要就直接memcpy,这样就可以根据不同的情况而做出最佳选择而不用再去调用那些不做事的构造函数,而告诉编译器如何选择的就是type_traits(通过判别对象的判别类是true/false)。
那么type_traits为什么会有这种能力呢?首先我们看看type_traits的源码(其中__true_type和__false_type两个类的定义,侯捷老师讲的SGI2.9版本和本开源项目基于C++11的实现有所不同,我们来分别看一下): 首先看一下gcc版本中的__true_type和__false_type两个类的源码实现(MyTinySTL中的实现就是这个版本,基于c++11)
从上图我们可以看出,MyTinySTL中__true_type和__false_type的实现和gcc下type_traits.h中的实现是一样的,这两个版本下的__true_type和__false_type类,都是integral_constant类实例化为true/false后的别名,然后提供bool_constant类作为接口提供给外界使用(继承)。 然后我们再看一下侯捷老师所讲的SGI2.9中这两个类的实现:
可以看到两者的区别,C++11版本中的实现是有静态成员常量的,而SGI2.9中就是一个空类,那在编译期间,不同的实现方法(即现代版本的型别的特性判别类继承的是非空版本,而旧的SGI2.9的型别的特性判别类继承的是空版本)的实参分别被模板函数推导为非空/空的__true_type和__false_type,这两种不同的设计,都是如何告诉编译器判别结果“真还是假”的呢?(即两种情况下,type_traits从实参中萃取出来的东西是如何和这两个类进行匹配的),我们分别看一下算法中对这些判别类的调用:
首先看一下gcc中是如何使用有bool变量true/false的__true_type和__false_type类来决定使用哪个重载函数的的:
如上图所示,_Destroy()函数是对外接口,其接受两个ForwardIterator类的迭代器,然后使用iterator_traits萃取迭代器所指对象的型别_Value_type,然后再通过type_traits来萃取_Value_type的析构函数重不重要(如果__has_trivial_destructor(_Value_type)的结果是true,那就是不重要)来决定使用_Destroy_aux类的泛化和特化版本,_Destroy_aux类的源码实现如下:
从上面的源码可以看出,当模板实参为true时进入特化版本(即该对象的析构函数不重要,即它只有default dtor,我们就没必须再调用它的析构函数了,因为这次转发是无意义的,纯属浪费时间),此时就啥都不用做了(仅仅定义了一个空的函数)。而如果实参为false进了泛化版本,它就会定义一个__destroy函数,然后遍历迭代器区间,去调用该对象的析构函数。也就是说,
在现代版本中,已经直接通过true/false来当实参传入判断了(这是因为C++11提供了constexpr关键字,该关键字可以使编译器在编译期间就得到表达式的常量值),而SGI2.9版本中还没有这个关键字,所以无法这样推导出具体的bool类型值,而是采用空类的方式,接下来我们再看看同一功能的函数再SGI2.9中的实现:我们从下往上来看这段源码实现,destroy是对外接口,接受两个迭代器,然后使用value_type()函数来获取这个迭代器所指对象的型别(value_type的实现其实就是调用了iterator_traits),将这3个参数传给__destroy函数,这个函数又做了什么呢?它先通过type_traits来萃取出这个型别的析构函数是否重要,然后它的返回值是什么呢?是__false_type和__true_type(这里和C++11之后的版本不同了,不是具体的static constexpr true/false,而是一个空类类型),然后再根据是哪个空类去调用相应的__destroy_aux函数(从这里我们也可以看出,形参是两个自定义的空类类别,表示如果传入的实参是哪个空类类别,就进哪个函数),最后__destroy_aux的实现就是相同的了。
可以看出C++11之后如何让不同的实参,通过type_traits后的结果进不同的函数?直接通过__destroy_aux类的泛化和特化,而SGI2.9中则需要将两个空类放到模板函数的参数中利用重载机制,而在编译期就选派好函数,效率是更高的,这也就是为什么会有新的__true_type和__false_type的实现。 至此,关于type_traits相关的内容就讲完了,这边在看源码时由于实现方法和侯捷老师STL网课中讲的有所不同,所以在文章中用提出问题加看两个版本源码的方式来讲述,表达和理解可能都不太好,如果有误还请指出。
二、迭代器的5种类型
- 上一节为什么会将type_traits呢?那是因为MyTinySTL在实现判断迭代器具体类型这个类时,采用了type_traits手法,所以本节我们回归正题,继续顺着iterator.h中的源码往下读。
- 首先我们来了解一下迭代器有哪些类别,迭代器根据移动特性和施行操作的依据被分为5类:
- Input Iterator:这种迭代器所指的对象,不允许被外界改变。所以这种迭代器可以称为只读迭代器,read only;
- Output Iterator:只写迭代器,write only;
- Forward Iterator:允许“写入型”算法(例如replace())在此迭代器所形成的区间上进行读写操作。可单向移动的读写迭代器;
- Bidirectional Iterator:可双向移动(当算法需要逆向遍历某个迭代器区间时,可以使用此迭代器)可双向移动的读写迭代器;
- Random Access Iterator:提供迭代器的所有计算能力!,前三种只支持operator++,第四种还支持operator--,但这种支持p+n、p-n、p1-p2、p1<p2;
这5种迭代器之间是什么关系呢?判别迭代器的具体类型能带来什么收益?如下图
首先,我们先别看这个接口直接看4个重载函数思考一个问题,当程序调用advance()时,应该选择哪一份函数呢?如果实参迭代器是一个RandomIterator,那它即可以使用第4个重载函数,也可以使用前3个,但是对RandomIterator而言,明明可以o(1)的时间复杂度完成任务,那就不应该再让他选择其他的版本(除非没有针对RandomIterator的重载版本),而STL中是如何解决这个问题的呢?就是利用iterator_categpry的5个标记类,让他们有继承关系+iterator_traits的使用。我们看一下接口函数,利用iterator_traits萃取出迭代器的category(),然后根据这个类型再去选择重载函数,此时如果有自己的类型就会选择该版本,如果不存在则利用继承关系去“降级使用”,即达到优先效率,如果没效率那就找能完成任务的版本。//5个作为标记用的类别(tag type) 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_access_iterator_tag:public bidirectional_iterator_tag {}; //【NOTE】1、这里的这些空类只是用来标记用的,所以不需要任何成员------>即typedef为iterator_category // 的空类(重载函数的形参,就会放这几个类名之一,然后匹配iterator_traits(实参)的结果(这5个之一)) // 2、采用继承关系是因为:高等级的迭代器可以实现低等级的需求,所以如果没有高等级的迭代器的重载版本, // 则应该选择相应的低等级的重载版本,所以迭代器的类型之间需要有继承关系(但迭代器本身没有!) //下面有4个重载函数 template <class InputIterator,class Distance> inline void __advance(InuputIterator& i,Distance n,input_iterator_tag){ while(n--) ++i; //单向,逐一向前 } template <class ForwardIterator,class Distance> inline void __advance(ForwardIterator& i,Distance n,forward_iterator_tag){ __advance(i,n,input_iterator_tag()); //这是一个单纯的传调用函数, } template <class BidirectionalIterator,class Distance> inline void __advance(BidirectionalIterator& i,Distance n,bidirectional_iterator_tag){ //双向 逐一向前 if(n>=0) while(n--) ++i; else while(n++) --i; } template <class RandomAccessIterator,class Distance> inline void __advance(RandomAccessIterator& i,Distance n,random_access_iterator_tag){ //双向 跳跃前进 i+=n; } //为4个重载函数写一个接口: template <class InputIterator,class Distance> inline void advance(InputIterator& i,Distance n){ __advance(i,n,iterator_traits<InputIterator>::iterator_category()); //这是一个单纯的传调用函数, }
三、利用iterator_traits实现的一些函数
-
iterator.h在实现了iterator_traits类及其相关的源码定义后,就开始利用iterator_traits来实现一些函数供外部调用了。
-
萃取某个迭代器的category
如图所示,该函数接受一个迭代器,然后利用iterator_traits萃取出它的iterator_category(即那几个空的标记类之一,迭代器是什么类型定义在type_traits中,针对不同的实参类型有特化版本),然后返回一个该类的临时对象。
-
萃取某个迭代器的distance_type(迭代器之间的距离、容器的最大容量)
这里用到了一个强制类型转换函数:
这个函数的意思是将表达式强制转换为new_type类型,所以上图中代码的意思就是将0强制转换为difference_type*类型,即返回一个空指针,其类型是difference_type*。
-
萃取某个迭代器的value_type
返回指向迭代器所指对象的指针,并将指针初始化为0.
-
distance函数
由上图可以看出,源码根据迭代器的移动特性(单向逐一向前/任意移动)实现了两个重载函数distance_dispatch(),然后实现了distance()作为对外接口,完成收到实参后选派函数和转发的工作。如果迭代器是random_access_iterator就进下面这个版本直接计算两个迭代器之间的距离,如果不是random_access_iterator但是input_iterator就执行上面这个版本,每次前进一步计数,循环结束时的计数值就是两个迭代器的距离。
-
advance函数
由上图可以看出,源码根据迭代器的移动特性(单向逐一向前、双向逐一向前和任意移动)实现了3个重载函数,然后实现了一个对外接口advance来实现根据iterator_category()结果的重载函数选派和实参转发,根据以下原则“任何一个迭代器,其类型永远应该落在该迭代器所属的类别集合中,最强化的那个迭代器类别”,使实参迭代器永远能进入当前效率最高的那个函数。
-
反向迭代器reverse_iterator 反向迭代器其实就是一个对iterator进行封装的适配器,所以这里先不讲,等讲到适配器的时候我们再回头看。
-
总结:至此iterator.h和type_traits.h的相关知识和源码我们就分析完了,总的来说其中最重要的思想就iterator_traits和type_traits的实现和使用(内置类型、模板泛化/特化、模板函数对class object具有反向推导机制、重载函数)。
iterator_traits技法①使我们能够对任意的迭代器进行associated type的萃取,而不仅仅使class iterator②使我们能够使用一些我们无法直接获取的类型(eg.定义函数时使用实参的类型作返回值,我们就可以通过间接萃取的方式,将萃取结果作为函数的返回值),type_traits技法使编译器在编译期就能够知道实参的一些特性,这使得我们可以实现不同情况下效率最优的重载函数供编译器在编译期间选派,而不是仅仅用重载函数,那样就需要到执行期间才知道具体用哪个函数,影响效率。
最后:这是一篇记录学习疑惑和想法的帖子,里面的内容、表述并不一定正确,只是自己当下的理解,如有错误,还望提出您宝贵的意见。