MyTinySTL学习之迭代器:iterator.h(一)

1,946 阅读9分钟

MyTinySTL学习之迭代器:iterator.h(一)

一、迭代器是什么?

  • 迭代器是一种抽象的设计概念,其模式定义如下:提供一种方法,使之能够依序遍历某个聚合物(容器)内的各个元素,而又不用暴露该聚合物的内部表达方式。简而言之,在STL中,迭代器就是算法得以访问容器中数据的桥梁
  • 可能你会觉得上面的内容太抽象了,迭代器是怎么起桥梁这个作用的?迭代器从代码层面看到底是个什么东西?其实迭代器在代码层面就是一个行为类似于指针的对象,即①iterator是个对象 ②iterator这个对象具有指针的行为(即能够对其所指对象进行提领dereference和成员访问member access操作),所以定义迭代器对象的这个类(后称为迭代器/iterator,因为源码中实现此类时,类名即为iterator)必须重载operator*和operator->函数

二、迭代器的相关类型associated type是什么?为什么需要定义它们?

  • 算法中运用迭代器访问容器时,很可能会用到其相关类型(eg.迭代器所指容器中对象的类型、迭代器的类型),如果此时算法的实现中需要知道所指对象的类型,我们此时就会觉得无能为力”我只是指向了你这个封闭的容器啊,我怎么知道你肚子里装的是什么?“,举个例子,如下:

    vector<xxx> vec={y,yy,yyy};
    
    auto iterator=vec.begin();
    cout<<*iterator<<endl;      //输出y
    ++iterator;
    cout<<*iterator<<endl;      //输出yy
    

    我们可以通过iterator来对xxx类对象y,yy,yyy进行提领,但我们无法获取其类型xxx。而在算法中,我们可能需要知道这个类型xxx以实现对当前容器的最优化操作,所以就需要有一种方法来帮我们查询迭代器的一些associated type,以满足算法的要求。

  • 迭代器的常用的(算法会询问迭代器的)5种associated type1.png

    由上图可知,每个iterator都必须提供5个类型别名(这些别名是STL的约定,只有同样的别名才能融入STL),分别是iterator_category、value_type、pointer、reference、difference_type。这5个iterator的associated分别可以回答算法以下5个问题

    • iterator_category:迭代器的类型(决定了迭代器可以怎么走,例如vector的迭代器可以随机访问,而链表的迭代器一次只能走一步,迭代器的5种类型,将在下面用一个栏专门讲解)
    • value_type:迭代器所指容器中元素的类型
    • difference_type:迭代器距离范围(即迭代器相减结果的范围)对应的类型,一般是内置类型ptrdiff_t,(eg.如果容器的容量范围是[0,232-1],则可以将difference_type定义为unsigned int)
    • reference:迭代器所指容器中元素的引用类型
    • pointer:迭代器所指容器中元素的指针类型 【NOTE】:每个容器都要实现自己的迭代器,为什么要在iterator.h中定义上面这段代码? 2.png 我们在文件中找一下对这个类的引用,可以发现上面这段结果,于是就很清晰了,iterator.h中定义了这个接口基类(为了统一iterator associated type的5个类型别名),每个容器在实现自己的iterator时继承这个基类,为自己的5种associated type赋上实参(通过typedef),这样就避免了容器实现自己的迭代器时接口不统一。

三、如何获取迭代器的associated type? iterator_traits盛大出场!

  • 经过前面两节,我们已经知道了算法需要根据迭代器的5种associated type来对迭代器所指容器做出最佳操作这个需求,那么这一节就来讲解,算法与迭代器之间的问答机制,即迭代器是如何解决(回答)算法的需求(问题)的。

  • 【直接问答机制存在无法解决的问题】在上一篇文章中,我们已经讲过算法和class iterator之间的应答机制,以及native pointer没有能力为自己实现5种associated type的问题。详见以下链接。 juejin.cn/post/710240…

  • 由上可知,iterator根据有没有能力定义自己的5种associated type可以分为class iterator和non-class iterator(即native pointer),class iterator可以通过直接问答机制回答算法的问题,而non-class pointer不可以,那有没有一个黑箱,这两种iterator输入进去,都可以得到相应的associated type呢?--->iterator_traits就是这个黑箱

    5.jpg

    由上图可知,通过类模板偏特化的作用,不论是原生指针或者class iterator都可以让外界方便的得到其associated type(iterator_traits的源码定义和使用方法在下面介绍)

  • iterator_traits源码分析 3.png 4.png

    由上图可知,iterator_traits类的实现为一个泛化版本+两个特化版本,泛化版本用于处理class iterator,特化版本用于处理native pointer和const native pointer。我们还可以从上图的源码中发现,偏特化版本只定义了5个类型别名,而泛化版本除此之外还有一系列的预处理函数,接下来我们就分析其中的主成分,来了解一下iterator_traits是如何知道任意一个输入迭代器的associated type的。 5.png

    从泛化版本中截取取与特化版本结构一致的部分如上图,我们可以发现,iterator_traits类在做的事情就是:接受一个迭代器类型(Iterator/T*/const T*)后,通过typedef方法在自己的类中定义类型别名,将输入的迭代器的associated type提取出来(即typedef后,类体中的别名和输入迭代器的associated type就是相同的了)。其中class iterator因为在类内定义了5种associated type,所以直接typdedef即可(对应源码51-55行),而non-class iterator(native pointer)则需要做一些处理,①如果传入的是一个普通的原始指针T*,那它所指容器种的对象就是T类型的,所以将value type重命名为T,其他同理(源码78-82)。②如果传入的是一个const的原始指针const T*,此时要注意!其所指容器中的对象是T类型而不是const T!!,这里地方需要解释一下,为什么指针定义为指向常量的指针,但容器中元素的类型却没有定义为const T,这是因为如果在定义中就将元素类型定义为const,那这些元素就无法被赋初值了,也就失去了意义,所以在这里的实现是将元素定义为T类型,指针仍是const T*,这样元素即可以在初始化时被赋值,又避免了用户通过迭代器对其进行修改(源码88-92)。综上,iterator_traits通过偏特化为不同的迭代器都设计了对应的萃取方式,所以我们就可以通过iterator_traits来获取(间接回答算法)任意迭代器的associated type了,方法如下:

    typename iterator_traits<Iterator>::iterator_category   //这里typename的作用是告诉c++编译器,后面这串东西是类型,而不是class的某个成员变量\函数
    typename iterator_traits<Iterator>::value_type
    ···
    

    这样我们就通过iterator_traits来间接的取出了Iterator的iterator_category和value_type。至此我们就知道了iterator_traits能够萃取任意迭代器5种associated type的原理以及如何手动使用iterator_traits来间接获取迭代器的associated type。既然这样就可以了那为什么该源码的实现中,还为泛化版本写了那么多辅助函数呢?我们来看看源码的泛化版本中,是谁调用了这个核心模块。 6.png 首先,我们可以看到,源码中泛化版本的iterator_traits类的最终定义如上,其实就是继承了iterator_traits_helper这个类,为什么要这么做?可以理解成为实际上实现traits功能的类是iterator_traits_helper,但是它有两个模板参数而且类名也不符合STL标准,即最终的对外接口必须是

    template <class Iterator>
    struct iterator_traits {};
    

    所以用这个方式做了个适配器,使对外接口符合STL标准。那为什么不直接在iterator_traits类中实现traits功能,而是要用一个辅助类呢?让我们来看一下iterator_traits_helper的源码: 7.png

    从上图可以看出,iterator_traits_helper类的实现是两个模板偏特化,根据has_iterator_cat类的value值,来决定使用哪个helper函数,如果是value如果是true就使用继承iterator_traits_impl类的版本,如果是false就使用空类体的版本,不执行任何操作。而由has_iterator_cat这个类名,我们可以猜测出,这一层的意思是,如果传入的这个Iterator有iterator_category,就执行iterator_traits_impl类,进行最后的萃取。如果是false,它就不是个迭代器也就没必要萃取了。所以不在iterator_traits中直接实现,而是要借助一个辅助类是为了判断传入的这个Iterator有没有迭代器类型,如果有我们再用就执行iterator_traits_impl类去萃取它,如果没有,那它也就没东西给我们萃取了,不是符合STL标准的迭代器,直接结束流程。 那么has_iterator_cat类又是如何实现的呢?它为什么可以知道一个迭代器有没有iterator_category?让我们来看一下它的源码定义: 8.png

    可以看到has_iterator_cat类有一个结构体成员变量,两个成员模板函数(即实例化类和这个成员函数是独立的)和一个静态成员变量。既然是根据这个静态成员变量来决定使用哪个helper函数的,那我们就先看源码是如何给这个value赋值的,我们可以很明显的看出value的取值其实就取决于test<T>(0)是否等于char,那test<T>(0)是个什么玩意?test不是模板函数吗,为什么还手动传参数类型?test函数(它甚至不能被成为函数,仅仅是个声明)没有函数体,它哪来的返回值?这里这个test构造函数其实是用了SFINAE规则(Substitution Failure Is Not An Error),它的作用是当我们进行模板特化的时候,自动去选择正确的那个模板,避免失败。接下来我们就看看这个技术是如何在两个test函数上运行的。首先我们看一下两个test函数的定义。第一个test函数是variadic function(因为形参是...),它的意思是这个函数能够接受任意个任意类型的参数,该函数返回值为two。第二个函数拥有一个U::iterator_category类型的形参(这么写就是假设U必须由内嵌类型iterator_category),并赋缺省值为0,该函数返回值为char。好了,接下来我们就看test<T>(0)会如何在SFINAE规则下作用于这两个函数。在这个表达式中,尝试用T去代替R,此时如果T::iterator_category是有效的,那这个代替就是有效的,否则就是无效的。所以当我们传入T时,如果T::iterator_category有效,则进入第二个test函数(此时虽然第一个test函数也生效,但是此时我们会执行重载原则),此时这个函数虽然不会执行,但是在编译期间,我们就可以知道它的返回值为char类型,从而使value为ture,否则就会进入第一个函数,函数返回类型为two,sizeof(two)=2*sizeof(char),从而使value为false。综上,has_iterator_cat类利用SFINAE规则,使编译器根据迭代器有/没有iterator_catefory类型成员,分别送入两个test函数,这两个test函数并不会执行,而是在编译期间,通过test函数的返回值判断迭代器进了哪个函数,从而判断迭代器有没有iterator_category类。至此我们就看完了iterator_traits实现的源码,下一篇将分析基于iterator_traits,从而得以实现的一些函数以及反向迭代器。

最后:这是一篇记录学习疑惑和想法的帖子,里面的内容、表述并不一定正确,只是自己当下的理解,如有错误,还望提出您宝贵的意见。