概述
Ch3 迭代器与traits编程技法
3.1 迭代器设计思维概述
- STL中心思想:将数据容器和算法分开,彼此独立设计,最后再以“胶合剂”将它们撮合在一起
迭代器便是这所谓的“胶合剂“,它能够实现:不管容器的数据结构是什么样的,迭代器都可以以指针的方式去操作容器内的每一个元素。 - 以算法find ()为例,它接受两个迭代器和一个搜寻目标。这个算法会对两个迭代器之间的内容进行遍历并比较(用到operator ++,operator != 和 operator * )。从这个例子可以看到,迭代器依附于容器之下。
3.2 迭代器是一种smart pointer
- 在使用方法上,可以认为迭代器是一种指针,因为迭代器也需要像指针那样进行间接引用 ( operator * )和成员访问(operator -> )。于是关键在于重载这些运算符
- 可以参考c++标准程序库中的auto_ptr。据此可以设计一个适配于链表list的迭代器,数据成员为指针变量。为此需要实现operator * 、-> 、前置++、后置++、==、!= 运算符的重载。而拷贝构造函数和operator = 不需要,因为构造函数中包含默认参数,并且没有深拷贝的操作(只是运用指针的值,即使对象消失也不会丢内存,因此连析构函数都不需要)。
- 但实际上,为了完成针对List而设计的迭代器,需要暴露List的实现细节(因为运算符的操作需要用到List的函数和数据成员)。因此要设计迭代器就必须对容器的细节了如指掌。为了达到封装的效果,迭代器通常会和容器一起设计,因此每一种STL容器都有专属迭代器。
3.3 迭代器相应型别推导
- 实际上,在算法中运用迭代器时,很可能会用到其相应型别(比如说指针的基类型),但c++现有的运算符或函数即使能求出其型别,也并不能用于变量声明使用。
关于这一点,以下面代码为例
class MyClass{
/*省略*/
};
typedef MyClass* ElemPtr; //可以认为ElemPtr是“迭代器”(实际的迭代器比这个复杂)
int main(){
ElemPtr p; //p是Myclass*
ELemPtr* pp; //pp是MyClass**
ElemPtr& pr=p; //pr是引用!
//typeof(*ElemPtr) 无此用法!
//typeid(*ElemPtr) 得到的是型别名称,不能用于声明变量!
MyClass num; //这种声明才能得到ElemPtr的基类型数据!
}
-
解决办法是利用function template的参数推导机制
请看代码:
template<class I,class T> void func_impl(I iter,T value){ T temp; //temp是T类型 /*省略*/ } template<class I> inline void func(I iter){ func_impl(iter,*iter); //注意:这里传入的两个参数的类型分别为I* 和I,第二个参数前面的*是取内容的operator* } int main(){ MyClass C;//见前面 func(&C); }
分析:main函数传入&C,其类型为MyClass *(&为取地址运算符),根据模板,I被翻译成MyClass ,因此 iter为MyClass * 类型的变量,而iter为MyClass类型的变量。此时再传入func_impl函数,根据模板,T类型就被翻译成MyClass类型,I被翻译成MyClass *类型。这时候T类型就是迭代器的型别。
-
然而,并非任何情况都可以用这种方法解决,请看下一节的例子
3.4 Traits编程技法
-
我们称迭代器对应数据的基类型为value type。如果value type必须用于函数的返回值,则上述方法无效。因为template参数推导机制只能导出参数型别,无法用于回返值
(疑问:在上例中main函数直接传指针和基类型,然后根据模板翻译结果令返回值为模板中的某个类型,例如模板中包含class T 和 class I,然后传MyClass* 类型和MyClass类型的变量,令返回值类型为I,相当于返回值为MyClass,这样做也能解决吧……)
(思考:但是上面的做法必须传入两个变量,书上的意思可能是只传一个变量无法解决。并且对于原生类型的常值而言,无法直接传递其地址,所以不能适用于所有情况)
-
一种解决方法是声明内嵌型别,请看代码:
template<class T> struct MyIter{ typedef T value_type; //内嵌式声明 T* ptr; MyIter(T* p=NULL):ptr(p){} T& operator*()const {return *ptr;} /*省略其他*/ } template<class I> typename I::value_type func(I ite){ return* ite }//返回类型是结构体I中的value_type,必须加上typename关键词,以告诉编译器这是个类型 int main(){ MyIter<int> ite(new int(8)); cout<<func(ite)<<endl; //输出8 return 0; }
这种方法的局限性:原生指针不是迭代器,无法定义内嵌型别。若想做特殊化处理以适应原生指针,需要用到template partial specialization(模板偏特化)。
-
模板偏特化的意思是:如果class template拥有一个以上的参数,可以针对某些(但非全部)参数进行特化工作,也就是说提供一个特化版本
假设一个class template为:
template<typename T> class C{ //... }
则其偏特化版本为:
template<typename T> class C<T*>{ //... }
这里是针对于模板范围的偏特化,因为现在C类只接受指向T类型的指针。
据此,我们可以针对template参数为指针的情况设计特化的迭代器
-
下面这个class template专门用来萃取迭代器 的特性,value type也是特性之一
template<class I> struct iterator_traits{ typedef typename I::value_type value_type; };//获取迭代器I中的value type。
于是本节2.中的func函数又可以这么写
template <class I> typename iterator_traits<I>::value_type func(I ite){ return *ite; }//这相当于把返回类型改成了iterator_traits中的value_type。(模板取I)
现在我们对iterator_traits进行偏特化处理
template <class T> struct iterator_traits<T*>{ typedef T value_type; }//现在,value_type取T类型即可
于是即使T是int类型,我们也可以使用4.中的func函数,因为这时候使用得是偏特化的iterator_traits,value_type能获取到int类型。
当然,考虑到常指针的存在,我们还需要进行偏特化。于是const int* 也可以被使用
template<class T> struct iterator_traits<const T*>{ typedef T value_type; }
-
思路总结:
我们认为,原生指针也是一种迭代器,理应跟自定义迭代器享用相同的函数。对于自定义迭代器,我们采用内嵌式声明的方式以实现value_type的获取,然而原生指针并没有办法进行内嵌式声明定义。于是我们考虑使用模板偏特化的方式,用一个结构体iterator_traits获取类型T(T是迭代器)的value_type,同时编写偏特化模板接收原生(常)指针(这个是很容易分清的,因为你不会传入一个迭代器对象的指针,只会传对象,但你会传原生指针)。于是这样,我们就可以获取原生指针和迭代器类型的value_type了
(注意:模板不能写成< class T* >的形式,因此无法用重载func函数来解决)
-
最常用的迭代器相应型别有五种(列在下方),traits需要能将其一一取出
template<class I> struct iterator_traits{ typedef typename I::iterator_category iterator_category; //迭代器种类,下文介绍 typedef typename I::value_type value_type; //迭代器基类型T typedef typename I::difference_type difference_type; //迭代器中的ptrdiff_t typedef typename I::pointer pointer; //迭代器基类型的指针T* typedef typename I::reference reference; //迭代器基类型的引用T& }
3.4.1 value type获取
见上方1-5
3.4.2 difference type获取
-
difference type代表着两个迭代器之间的距离,用容器头尾之间的距离可以表示容器的最大容量。如果一个算法需要提供计数功能(例如STL中的count ()),就必须用到difference type
-
两个偏特化版本(原生指针和const 指针)的iterator_traits写法如下:
template<class T> struct iterator_traits<T*>{ typedef ptrdiff_t difference_type; } //const T*版本内容相同,不再复述
于是当我们需要用到迭代器的difference type时,就可以这么写:
typename iterator_traits<I>::difference_type
3.4.3 reference type获取
- 首先要确定,我们只能对非 常迭代器(对应普通原生指针)进行提领操作。当我们进行提领操作时,获得的应该是个左值,因为右值不允许赋值操作。
- 在c++中,函数如果需要传回左值,都要以by reference的形式进行。因此当p是非 常迭代器时,如果p的value type是T,那么* p的型别应当是T&,并非T。同理,当p是常迭代器时,* p的型别应当是const T&,并非const T
- 具体的细节将在下一节展示
3.4.4 pointer type获取
-
既然可以传回左值,它代表p所指之物,那么也应当可以传回一个指针,指向迭代器所指之物。而实际上,operator * 返回的就是p所指之物(类型为T &),operator ->返回的是一个指向迭代器所指之物的指针(类型为T* )
-
于是,我们把这两个运算符加进traits当中,并设计偏特化版本
template<class I> struct iterator_traits{ typedef typename I::pointer pointer; typedef typename I::reference reference; } template<class T> struct iterator_traits<T*>{ typedef T* pointer; typedef T& reference; } template<class T> struct iterator_traits<const T*>{ typedef const T* pointer; typedef const T& reference; }
3.4.5 iterator_category分类与获取
-
首先我们先讨论一下迭代器种类,根据移动特性与施行操作,可分为5种:
- Input Iterator:只读迭代器,不允许外界改变
- Output Iterator:只写迭代器
- Forward Iterator:允许读写
- Bidirectional Iterator:可双向移动并进行读写
- Random Access Iterator:除了前4种的功能,还能进行指针的所有操作(operator +,-,[],<,>等)
看起来是第五种最强大,那是不是意味着都用第五种就好了呢?实际上不然。对于STL而言,效率和空间是最重要的,因为如果算法可以接受Forward Iterator,而你却使用Random Access Iterator,那么这是一种浪费。再比如,对于线性链表而言,访问元素只能从某一结点开始一一访问,因此只需要实现operator ++即可,这时候用RAI是一种浪费因为operator+仍然要用operator++实现
-
例如函数advance (),接收参数为迭代器引用和步长,用于向前移动指针。以下是三个版本的函数实现代码
//略去函数声明等细节,参数为:Iterator& i,Distance n while(n--) i++; //Input Iterator 版本 if(n>=0) while(n--) ++i; else while(n++) --i; //BiDirectional Iterator 版本 i+=n; //Random Access Iterator 版本
那么这时候调用那么函数定义呢?如果选择第一个,对于RAI(第5中迭代器)而言很缺乏效率。如果选择第三个,II又无法接受(因为不支持operator +=)。可以用if else语句进行判断,但这样是在执行版本才决定用哪个函数,很浪费效率。因此采用重载的机制达成这一目标。
-
为了让函数同名,又能实现重载并使用模板,我们需要加上一个型别已经确定的参数,这个参数辨识迭代器类型。当然这个参数必须是一个类,不能是数值字符串,因为编译器需要用它的类型来决定执行哪个函数。因此下面需要定义5个class,并且没有任何成员,只是个标签。
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{};
于是底层的实现函数便可以用重载的方式实现了,函数声明不同的地方仅在于参数部分。由于函数不需要使用标签(也没法使用,因为根本没有成员),因此声明参数时不必带上对象名。
-
完成了重载函数,我们还需要一个上层接口以调用这些函数,这个接口函数只需要两个参数,在函数的实现部分才会添加iterator_category参数。具体的实现为
template<class InputIterator,class Distance> inline void advance(InputIterator& i,Distance n){ __advance(i,n,iterator_traits<InputIterator>::iterator_category()); } //iterator_traits<InputIterator>::iterator_category()产生的是一个临时对象,编译器根据这个临时 //对象的型别判断使用哪个重载函数__advance //在SGI STL中实际上会再声明一个函数求取第三个参数
因此在iterator_traits中必须再加一个相应型别
template<class I> struct iterator_traits{ //... typedef typename I::iterator_category iterator_category; }
任何一个迭代器的类型都应该是能包含其全部内容的迭代器的类型。例如int * 作为迭代器来看,应该属于RAI(因为RAI本来就是指能实现原生指针所有运算的迭代器)。因此特化版本的iterator_traits应该把RAI作为其类型。
注:实际上,前文的advance ()函数,其模板类型为Input Iterator,这是STL的命名规则:以算法所能接受的最低阶迭代器类型来为其迭代器型别参数命名。
-
迭代器类型标签的类(结构体)采用继承的目的是:不仅可以契合重载机制,还可以规避写多余的传递调用的函数。以下是个例子:
//一个例子展现继承的作用 class A{}; class B:public A{}; class C:public B{}; template<class I> void func(I& item,A){ cout<<"A Class Version"<<endl; } template<class I> void func(I& item,C){ cout<<"C Class Version"<<endl; } int main(){ int p; func(p,A()); //输出"A Class Version",因为参数完全契合 func(p,B()); //输出“A Class Version”,因参数不完全契合,根据继承关系调用A版本 func(p,C()); //输出"C Class Version",因为参数完全契合 }
从上面的例子可以看到,对于某个功能而言,B类和A类完全相同。那么我们通过继承的方式,可以少写一个重载函数。对于迭代器标签类型,也是这个道理。
-
对于迭代器类型标签的应用,再举distance ()函数一例。distance函数用于计算两迭代器之间的距离,涉及到指针的相关运算,因此对于不同的迭代器而言,计算方法略有不同。除了RAI可以直接计算以外,其余都需要逐一累计(因为只有operator ++)。采用继承的方式,使得我们只需要写两个重载函数,就可以适合于所有的迭代器类型。
3.5 std::iterator 的保证
-
为了符合规范,任何迭代器都应该能提供五个内嵌相应型别,以利于traits萃取。为了简化工作,STL提供了一个iterators的class,作为任何迭代器的基类,这样可以保证符合规范
-
代码见下
template<class Category,class T,class Distance=ptrdiff_t class Pointer=*T,class Reference=T &> struct iterator{ typedef Category iterator_category; typedef T value_type; typedef Distance difference_type; typedef Pointer pointer; typedef Reference reference; };
-
总结:
traits编程技法的关键在于用内嵌型别的编程技巧和template的模板机制增强型别的认证。此外还需要特别注意原生(常)指针也可以看做一种迭代器,而函数重载无法判别(因为模板无法接受class T*),因此需要对iterator_traits进行偏特化处理以适应原生(常)指针
对于一个迭代器,它应该包含5个相应型别:
- 迭代器种类。是一个“标签”,通过传入参数的类型决定。对于原生指针而言,是RAI
- 基类型。就是迭代器内部指针的基类型,对于原生指针而言就是指针的基类型。
- 指针类型。由基类型决定
- 引用类型。由基类型决定
- 指针距离。固定为ptrdiff_t
3.7 __type_traits
对于SGI STL而言,iterator_traits 负责萃取迭代器的特性,而__ type_traits则负责萃取value type的型别特性。(注意,__ type traits只是SGI STL使用的东西,不在STL标准内部)
-
value type的型别特性包括:
- 是否具备默认构造函数
- 是否具备默认拷贝构造函数
- 是否具备默认赋值函数
- 是否具备默认析构函数
- 是否为原生类型。(诸如int,char等C++自带的类型)
实际上,原生类型都是具备默认的构造,拷贝构造,赋值,析构函数的,而自定义数据类型(结构体,共用体,类等)则不一定。
对于1-4而言,如果答案是肯定的,那么我们在进行构造,析构,拷贝,赋值等操作的时候,就可以采用最有效率的方式(即根本不调用那些不必要的默认函数),并且可以采用内存直接处理操作malloc (),memcpy ()等。
-
以下是代码实现。基本原理仍然是用typedef声明类型。
template<class type> struct __type_traits{ typedef true_type this_dummy_member_mus_be_first; //用于通知某些有能力自动特化type_traits的编译器,这个模板是特殊的。 typedef __false_type has_trivial_default_constructor; typedef __false_type has_trivial_copy_constructor; typedef __false_type has_trivial_assignment_operator; typedef __false_type has_trivial_destructor; typedef __false_type is_POD_type; //对于自定义的类型而言,我们认为都存在非默认的四个型别,并且不是POD。对于原生类型,会利用偏特化版本 //加以区分 }; //由于采用类型名进行区分(类似于iterator_traits中对于iterator category的处理),因此要定义两个标签 //__true_type和__flase_type。这两个标签都是空结构体 struct __true_type{}; struct __false_type{};
以下是针对于原生类型的偏特化版本。篇幅原因,仅给出一种原生类型和其指针类型的代码实现
//偏特化版本,以int为例 template<> struct __type_traits<int>{ typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; //指针版本 template<> struct __type_traits<T*>{ typedef __true_type has_trivial_default_constructor; typedef __true_type has_trivial_copy_constructor; typedef __true_type has_trivial_assignment_operator; typedef __true_type has_trivial_destructor; typedef __true_type is_POD_type; }; //指针也是POD
-
以uninitialized_fill_n() 函数为例
//这个函数的功能为以x为蓝本,从迭代器first开始构造n个元素。 template<class ForwardIterator,class Size,class T> inline ForwardIterator uninitialized_fill_n(ForwardIterator first,Size n,const T& x){ return __uninitialized_fill_n(first,n,x,value_type(first)); }//先萃取出迭代器first的value type,value_type是用来取value type的函数 template<class ForwardIterator,class Size,class T,class T1> inline ForwardIterator __uninitialized_fill_n (ForwardIterator first,Size n,const T& x,T1*){ typedef typename __type_traits<T1>::is_POD_type is_POD; return __uninitialized_fill_n_zux(first,n,x,is_POD()); }//感觉参数表的最后一个参数应该是T1 //不是POD类型 template<class ForwardIterator,class Size,class T> inline ForwardIterator __uninitialized_fill_n_aux (ForwardIterator first,Size n,const T& x,__false_type){ ForwardIterator cur=first; for(;n>0;--n,++cur){ construct(&*cur,x); //*cur是针对于迭代器取其基类型数据,再把这个数据的地址传进参数,不可以直接传cur } } //是POD类型 template<class ForwardIterator,class Size,class T> inline ForwardIterator __uninitialized_fill_n_aux (ForwardIterator first,Size n,const T& x,__true_type){ return fill_n(first,n,x);//交给高阶函数处理 } //fill_n template<class OutputIterator,class Size,class T> OutputIterator fill_n(OutputIterator first,Size n,const T& value){ for(;n>0;--n,++first) *first=value;//直接赋值 return first; }
-
关于自定义数据类型,即使没有非默认构造,析构,拷贝构造,赋值函数, __ type_traits也会认为拥有,这是出于保守起见,宁可牺牲部分情况的效率, 也要保证数据的操作不会出错。虽然有的强大的编译器会自行判断这些,但是不能保证精度。
如果一定要进行区分,可以写一个偏特化版本告诉编译器有哪些是非默认的。
注:如果class类成员有指针,并且进行了内存的动态配置,就一定要实现非默认的构造,拷贝构造,赋值和析构,这是为了防止出现内存丢失和浅拷贝带来的错误情况。
最后
以上就是彪壮康乃馨为你收集整理的【理论学习/C++】《STL源码剖析》学习笔记:Ch3迭代器与traits编程技法Ch3 迭代器与traits编程技法的全部内容,希望文章能够帮你解决【理论学习/C++】《STL源码剖析》学习笔记:Ch3迭代器与traits编程技法Ch3 迭代器与traits编程技法所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复