• 重读STL源码剖析:迭代器


    首先关于迭代器:

    迭代器不属于容器,它与容器属于不同的类,但通过迭代器(迭代器中有某些成员变量;同时也对*等运算符进行了重载),可以访问到容器内的元素(比如list的迭代器,它不属于list,但它里面存放了一个指针,这个指针指向list结构里的成员node,这样就可以借用迭代器去访问容器了。迭代器并不属于容器,它只是个工具)。迭代器可以继承自一个公用的迭代器(当然也有没有继承的,如deque)。

    迭代器的目的是用于黏合容器和算法,算法以迭代器为媒介,通过迭代器去对容器中的数据进行计算。

    相应型别:

    在算法中,对容器内的元素进行操作时,有时候需要使用到容器内的变量的类型,如要声明一个和迭代器所指向的元素相同类别的变量,这个时候处理就很困难了。

    解决办法是利用函数末班的参数推导:

    template<class I>
    inline void func(I iter)
    {
      func_impl(iter,*iter);  
    }
    
    template <class I,class T>
    void func_impl(I iter, T t)
    {
      T tmp;      
    }
    

     

    这里为什么要将func作为接口,而将实际操作交给func_impl呢?

    这是因为我们在调用时,只希望传递迭代器就好了,即以这样的方式调用: func(&i);(&i相当于迭代器,i相当于这个迭代器所指向的元素)

    而如果不加一层封装只能这样使用:

    template<class I,class T>
    void func(I iter,T t)
    {
      T tmp;  
    }
    

     这样的使用的问题在于,每次使用func都要将迭代器所指向的元素暴露出来:

     func(&i,i);

     而在这种情况下func(&i)这样调用是非法的。

     因此加一层封装可以使调用更符合这种设计模式,即将一切都隐藏在迭代器以下。

     实际上,首先func(&i)首先在func中推导出I为int*型,然后转入func_impl,在func_impl调用时,经由(&int,int)中第二个参数推导出迭代器指向的元素的型别为int,推导出T为int,然后用int创建tmp.

    Traits编程技法:

    在上面的讨论中,实现了在算法内部声明迭代器所指向元素类别的功能,但是当算法的返回值也要是迭代器所指向的元素的类别时呢?

    在这种情况下,是无法以上面的方式实现这个功能的。

    因此大佬们换了个思路,在设计迭代器的时候,在迭代器的模板内部声明了迭代器所指向的元素的类型T为value_type.

    即:

    template<class T>
    struct iterator
    {
      typedef T value_type;
      T* ptr;
      .......    
    }
    

      这样,我们可以通过iterator::value_type的形式来声明一个变量,这个变量就是迭代器所指向的元素的型别。

    template<class I>
    typename I::value_type
    func(I ite)
    {
      typename I::value_type tmp;  
    }
    

      这样,如果我们调用函数func:向里传入一个迭代器,函数模板将I判定为迭代器的型别,然后据此将返回值的型别判断为迭代器所指向的值得型别,因为我们在迭代器内部已经声明了value_type为一种类型了。同时,我们也可以用这种方式在迭代器的内部声明一个迭代器所指向的元素的变量。

      看上去非常巧妙,但这个方法有一个问题:

    问题在于,如果我们要通过这种方式来推导返回值的类型和在算法内部声明一个迭代器所指向的元素的类型的变量,必须要求在迭代器内部声明它所指向的元素的型别为value_type。但在基本数据类型情况下这是做不到的,因为像int*,char*这些类型,他们是基本的数据类型,我们无法在里面人为的添加typedef int value_type这种声明语句。

    但像如果将int*这种原生指针都排除在外,拿迭代器的设计还有意义吗?

    在上述问题下,大佬们通过模板偏特化来解决这个问题。

    模板偏特化相当于提供一个更为具体的模板,在进行推导时,实际上会选取更为贴近所推导类型的模板来进行使用,因此完全可以为这些原始指针设计偏特化版本来解决。

    偏特化的意思是提供另一份模板定义是,其本身还是模板,但模板参数更进一步加上了条件限制。

    比如:

    template <typename T>
    class A 
    {
    	typedef T value_type;
    };
    template <typename T>
    class A<T*>
    {
    	typedef T value_type;
    };
    

      其中,当用A<int*> a;时,int*将会使用第二个模板,因为相比第一个,T*相比T更贴近于指针的形式。

    另外注意,第二个是无法单独存在的,也就是说如果删除第一个模板将会报错,偏特化是依赖于最普遍的那个模板而存在的。


    现在,就可以引入这部分的核心了,trait编程技法:

    首先看下这个模板类:

    template<class I>
    struct iterator_traits
    {
      typedef typename I::value_type value_type;  
    }
    

      这个模板是最普通话的模板,也就是说所有的偏特化的模板都基于这个版本之上。

    现在再来看前面写的func函数:

    template <class I>
    typename iterator_traits<I>::value_type
    func(I ite)
    {};
    

      现在,只要我们调用func函数,并将迭代器传入,将自动推导出I的类型,并将返回值的类型设置为迭代器类里面的value_type型别。

    此外,我们可以使用这个模板设计出若干个偏特化版本,如T*,const T*,这样,所有的类型都统一到了迭代器下,我们可以使用迭代器指向任何元素,算法也可以推导出相应的型别。

    迭代器所指向的元素的类别vlaue_type只是相应型别中的一种,还有很多种,下面展开介绍。

     

  • 相关阅读:
    java注解,通过反射解析注解,模仿hibernate,获取sql语句。
    Eclipse/Myeclipse中查看和调试JDK源代码的方法
    TCP为什么会出现 RST
    《浅谈F5健康检查常用的几种方式》—那些你应该知道的知识(二)
    负载均衡服务TCP端口健康检查成功,为什么在后端业务日志中出现网络连接异常信息?
    haproxy的丰富特性简介
    健康检查概述
    firewall防火墙常用操作
    gitlab修改默认端口
    vim脚本判断操作系统
  • 原文地址:https://www.cnblogs.com/lxy-xf/p/11391189.html
Copyright © 2020-2023  润新知