《STL源码剖析》学习笔记:Ch3迭代器与traits编程技法

Ch3 迭代器与traits编程技法

3.1 迭代器设计思维概述

  1. STL中心思想:将数据容器和算法分开,彼此独立设计,最后再以“胶合剂”将它们撮合在一起
    迭代器便是这所谓的“胶合剂“,它能够实现:不管容器的数据结构是什么样的,迭代器都可以以指针的方式去操作容器内的每一个元素。
  2. 以算法find ()为例,它接受两个迭代器和一个搜寻目标。这个算法会对两个迭代器之间的内容进行遍历并比较(用到operator ++,operator != 和 operator * )。从这个例子可以看到,迭代器依附于容器之下。

3.2 迭代器是一种smart pointer

  1. 在使用方法上,可以认为迭代器是一种指针,因为迭代器也需要像指针那样进行间接引用 ( operator * )和成员访问(operator -> )。于是关键在于重载这些运算符
  2. 可以参考c++标准程序库中的auto_ptr。据此可以设计一个适配于链表list的迭代器,数据成员为指针变量。为此需要实现operator * 、-> 、前置++、后置++、==、!= 运算符的重载。而拷贝构造函数和operator = 不需要,因为构造函数中包含默认参数,并且没有深拷贝的操作(只是运用指针的值,即使对象消失也不会丢内存,因此连析构函数都不需要)。
  3. 但实际上,为了完成针对List而设计的迭代器,需要暴露List的实现细节(因为运算符的操作需要用到List的函数和数据成员)。因此要设计迭代器就必须对容器的细节了如指掌。为了达到封装的效果,迭代器通常会和容器一起设计,因此每一种STL容器都有专属迭代器。

3.3 迭代器相应型别推导

  1. 实际上,在算法中运用迭代器时,很可能会用到其相应型别(比如说指针的基类型),但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的基类型数据!
 }
  1. 解决办法是利用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类型就是迭代器的型别。

  2. 然而,并非任何情况都可以用这种方法解决,请看下一节的例子

3.4 Traits编程技法

  1. 我们称迭代器对应数据的基类型为value type。如果value type必须用于函数的返回值,则上述方法无效。因为template参数推导机制只能导出参数型别,无法用于回返值

    (疑问:在上例中main函数直接传指针和基类型,然后根据模板翻译结果令返回值为模板中的某个类型,例如模板中包含class T 和 class I,然后传MyClass* 类型和MyClass类型的变量,令返回值类型为I,相当于返回值为MyClass,这样做也能解决吧……)

    (思考:但是上面的做法必须传入两个变量,书上的意思可能是只传一个变量无法解决。并且对于原生类型的常值而言,无法直接传递其地址,所以不能适用于所有情况)

  2. 一种解决方法是声明内嵌型别,请看代码:

    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(模板偏特化)。

  3. 模板偏特化的意思是:如果class template拥有一个以上的参数,可以针对某些(但非全部)参数进行特化工作,也就是说提供一个特化版本

    假设一个class template为:

    template<typename T>
    class C{
        //...
    }
    

    则其偏特化版本为:

    template<typename T>
    class C<T*>{
        //...
    }
    

    这里是针对于模板范围的偏特化,因为现在C类只接受指向T类型的指针。

    据此,我们可以针对template参数为指针的情况设计特化的迭代器

  4. 下面这个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;
    }
    
  5. 思路总结:

    我们认为,原生指针也是一种迭代器,理应跟自定义迭代器享用相同的函数。对于自定义迭代器,我们采用内嵌式声明的方式以实现value_type的获取,然而原生指针并没有办法进行内嵌式声明定义。于是我们考虑使用模板偏特化的方式,用一个结构体iterator_traits获取类型T(T是迭代器)的value_type,同时编写偏特化模板接收原生(常)指针(这个是很容易分清的,因为你不会传入一个迭代器对象的指针,只会传对象,但你会传原生指针)。于是这样,我们就可以获取原生指针和迭代器类型的value_type了

    (注意:模板不能写成< class T* >的形式,因此无法用重载func函数来解决)

  6. 最常用的迭代器相应型别有五种(列在下方),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获取

  1. difference type代表着两个迭代器之间的距离,用容器头尾之间的距离可以表示容器的最大容量。如果一个算法需要提供计数功能(例如STL中的count ()),就必须用到difference type

  2. 两个偏特化版本(原生指针和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获取

  1. 首先要确定,我们只能对非 常迭代器(对应普通原生指针)进行提领操作。当我们进行提领操作时,获得的应该是个左值,因为右值不允许赋值操作。
  2. 在c++中,函数如果需要传回左值,都要以by reference的形式进行。因此当p是非 常迭代器时,如果p的value type是T,那么* p的型别应当是T&,并非T。同理,当p是常迭代器时,* p的型别应当是const T&,并非const T
  3. 具体的细节将在下一节展示

3.4.4 pointer type获取

  1. 既然可以传回左值,它代表p所指之物,那么也应当可以传回一个指针,指向迭代器所指之物。而实际上,operator * 返回的就是p所指之物(类型为T &),operator ->返回的是一个指向迭代器所指之物的指针(类型为T* )

  2. 于是,我们把这两个运算符加进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分类与获取

  1. 首先我们先讨论一下迭代器种类,根据移动特性与施行操作,可分为5种:

    1. Input Iterator:只读迭代器,不允许外界改变
    2. Output Iterator:只写迭代器
    3. Forward Iterator:允许读写
    4. Bidirectional Iterator:可双向移动并进行读写
    5. Random Access Iterator:除了前4种的功能,还能进行指针的所有操作(operator +,-,[],<,>等)

    看起来是第五种最强大,那是不是意味着都用第五种就好了呢?实际上不然。对于STL而言,效率和空间是最重要的,因为如果算法可以接受Forward Iterator,而你却使用Random Access Iterator,那么这是一种浪费。再比如,对于线性链表而言,访问元素只能从某一结点开始一一访问,因此只需要实现operator ++即可,这时候用RAI是一种浪费因为operator+仍然要用operator++实现

  2. 例如函数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语句进行判断,但这样是在执行版本才决定用哪个函数,很浪费效率。因此采用重载的机制达成这一目标。

  3. 为了让函数同名,又能实现重载并使用模板,我们需要加上一个型别已经确定的参数,这个参数辨识迭代器类型。当然这个参数必须是一个类,不能是数值字符串,因为编译器需要用它的类型来决定执行哪个函数。因此下面需要定义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{};
    

    于是底层的实现函数便可以用重载的方式实现了,函数声明不同的地方仅在于参数部分。由于函数不需要使用标签(也没法使用,因为根本没有成员),因此声明参数时不必带上对象名。

  4. 完成了重载函数,我们还需要一个上层接口以调用这些函数,这个接口函数只需要两个参数,在函数的实现部分才会添加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的命名规则:以算法所能接受的最低阶迭代器类型来为其迭代器型别参数命名。

  5. 迭代器类型标签的类(结构体)采用继承的目的是:不仅可以契合重载机制,还可以规避写多余的传递调用的函数。以下是个例子:

    //一个例子展现继承的作用
    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类完全相同。那么我们通过继承的方式,可以少写一个重载函数。对于迭代器标签类型,也是这个道理。

  6. 对于迭代器类型标签的应用,再举distance ()函数一例。distance函数用于计算两迭代器之间的距离,涉及到指针的相关运算,因此对于不同的迭代器而言,计算方法略有不同。除了RAI可以直接计算以外,其余都需要逐一累计(因为只有operator ++)。采用继承的方式,使得我们只需要写两个重载函数,就可以适合于所有的迭代器类型。

3.5 std::iterator 的保证

  1. 为了符合规范,任何迭代器都应该能提供五个内嵌相应型别,以利于traits萃取。为了简化工作,STL提供了一个iterators的class,作为任何迭代器的基类,这样可以保证符合规范

  2. 代码见下

    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;
    };
    
  3. 总结:

    traits编程技法的关键在于用内嵌型别的编程技巧和template的模板机制增强型别的认证。此外还需要特别注意原生(常)指针也可以看做一种迭代器,而函数重载无法判别(因为模板无法接受class T*),因此需要对iterator_traits进行偏特化处理以适应原生(常)指针

    对于一个迭代器,它应该包含5个相应型别:

    1. 迭代器种类。是一个“标签”,通过传入参数的类型决定。对于原生指针而言,是RAI
    2. 基类型。就是迭代器内部指针的基类型,对于原生指针而言就是指针的基类型。
    3. 指针类型。由基类型决定
    4. 引用类型。由基类型决定
    5. 指针距离。固定为ptrdiff_t

3.7 __type_traits

对于SGI STL而言,iterator_traits 负责萃取迭代器的特性,而__ type_traits则负责萃取value type的型别特性。(注意,__ type traits只是SGI STL使用的东西,不在STL标准内部)

  1. value type的型别特性包括:

    1. 是否具备默认构造函数
    2. 是否具备默认拷贝构造函数
    3. 是否具备默认赋值函数
    4. 是否具备默认析构函数
    5. 是否为原生类型。(诸如int,char等C++自带的类型)

    实际上,原生类型都是具备默认的构造,拷贝构造,赋值,析构函数的,而自定义数据类型(结构体,共用体,类等)则不一定。

    对于1-4而言,如果答案是肯定的,那么我们在进行构造,析构,拷贝,赋值等操作的时候,就可以采用最有效率的方式(即根本不调用那些不必要的默认函数),并且可以采用内存直接处理操作malloc (),memcpy ()等。

  2. 以下是代码实现。基本原理仍然是用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
    
  3. 以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;
    }
    
  4. 关于自定义数据类型,即使没有非默认构造,析构,拷贝构造,赋值函数, __ type_traits也会认为拥有,这是出于保守起见,宁可牺牲部分情况的效率, 也要保证数据的操作不会出错。虽然有的强大的编译器会自行判断这些,但是不能保证精度。

    如果一定要进行区分,可以写一个偏特化版本告诉编译器有哪些是非默认的。

    注:如果class类成员有指针,并且进行了内存的动态配置,就一定要实现非默认的构造,拷贝构造,赋值和析构,这是为了防止出现内存丢失和浅拷贝带来的错误情况。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页