[硬核 | 万字长文炸裂!手撕 STL 迭代器源码与 traits 编程技法] - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
herongwei
V2EX    推广

[硬核 | 万字长文炸裂!手撕 STL 迭代器源码与 traits 编程技法]

  •  
  •   herongwei 2021-04-17 15:00:14 +08:00 1555 次点击
    这是一个创建于 1708 天前的主题,其中的信息可能已经有所发展或是发生改变。

    迭代器与 traits 编程技法

    原文首发:公众号:herongwei

    前言

    这一篇文章,我们来学习下 C++ STL 迭代器以及背后的 traits 编程技法。

    在 STL 编程中,容器和算法是独立设计的,容器里面存的是数据,而算法则是提供了对数据的操作,在算法操作数据的过程中,要用到迭代器,迭代器可以看做是容器和算法中间的桥梁。

    1 、迭代器设计模式

    为何说迭代器的时候,还谈到了设计模式?这个迭代器和设计模式又有什么关系呢?

    其实,在《设计模式:可复用面向对象软件的基础》( GOF )这本经典书中,谈到了 23 种设计模式,其中就有 iterator 迭代模式,且篇幅颇大。

    碰巧,笔者在研究 STL 源码的时候,同样的发现有 iterator 迭代器,而且还占据了一章的篇幅。

    在设计模式中,关于 iterator 的描述如下:一种能够顺序访问容器中每个元素的方法,使用该方法不能暴露容器内部的表达方式。而类型萃取技术就是为了要解决和 iterator 有关的问题的。

    有了上面这个基础,我们就知道了迭代器本身也是一种设计模式,其设计思想值得我们仔细体会。

    那么 C++ STL 实现 iterator 和 GOF 介绍的迭代器实现方法什么区别呢? 那首先我们需要了解 C++ 中的两个编程范式的概念,OOP (面向对象编程)和 GP (泛型编程)。

    在 C++ 语言里面,我们可用以下方式来简单区分一下 OOP 和 GP:

    gp-and-oop.png

    OOP:将 methods 和 datas 关联到一起 (通俗点就是方法和成员变量放到一个类中实现),通过继承的方式,利用虚函数表( virtual )来实现运行时类型的判定,也叫"动态多态",由于运行过程中需根据类型去检索虚函数表,因此效率相对较低。

    GP:泛型编程,也被称为"静态多态",多种数据类型在同一种算法或者结构上皆可操作,其效率与针对某特定数据类型而设计的算法或者结构相同, 具体数据类型在编译期确定,编译器承担更多,代码执行效率高。在 STL 中利用 GP 将 methods 和 datas 实现了分而治之。

    而 C++ STL 库的整个实现采用的就是 GP ( Generic Programming ),而不是 OOP ( Object Oriented Programming )。而 GOF 设计模式采用的就是继承关系实现的,因此,相对来讲,C++ STL 的实现效率会相对较高,而且也更有利于维护。

    在 STL 编程结构里面,迭代器其实也是一种模板 class ,迭代器在 STL 中得到了广泛的应用,通过迭代器,容器和算法可以有机的绑定在一起,只要对算法给予不同的迭代器,比如 vector::iterator 、list::iterator,std::find() 就能对不同的容器进行查找,而无需针对某个容器来设计多个版本。

    这样看来,迭代器似乎依附在容器之下,那么,有没有独立而适用于所有容器的泛化的迭代器呢?这个问题先留着,在后面我们会看到,在 STL 编程结构里面,它是如何把迭代器运用的炉火纯青。

    2 、智能指针

    STL 是泛型编程思想的产物,是以泛型编程为指导而产生的。具体来说,STL 中的迭代器将范型算法 (find, count, find_if) 等应用于某个容器中,给算法提供一个访问容器元素的工具,iterator 就扮演着这个重要的角色。

    稍微看过 STL 迭代器源码的,就明白迭代器其实也是一种智能指针,因此,它也就拥有了一般指针的所有特点 能够对其进行 * -> 操作。

    template<typename T> class ListIterator {//mylist 迭代器 public: ListIterator(T *p = 0) : m_ptr(p){} //构造函数 T& operator*() const { return *m_ptr;} //取值,即 dereference T* operator->() const { return m_ptr;} //成员访问,即 member access //... }; 

    但是在遍历容器的时候,不可避免的要对遍历的容器内部有所了解,所以,干脆把迭代器的开发工作交给容器的设计者,如此以来,所有实现细节反而得以封装起来不被使用者看到,这也正是为什么每一种 STL 容器都提供有专属迭代器的缘故。

    比如笔者自己实现的 list 迭代器在这里使用的好处主要有:

    • (1) 不用担心内存泄漏(类似智能指针,析构函数释放内存);
    • (2) 对于 list ,取下一个元素不是通过自增而是通过 next 指针来取,使用智能指针可以对自增进行重载,从而提供统一接口

    3 、template 参数推导

    参数推导能帮我们解决什么问题呢?

    在算法中,你可能会定义一个简单的中间变量或者设定算法的返回变量类型,这时候,你可能会遇到这样的问题,假如你需要知道迭代器所指元素的类型是什么,进而获取这个迭代器操作的算法的返回类型,但是问题是 C++ 没有 typeof 这类判断类型的函数,也无法直接获取,那该如何是好?

    注意是类型,不是迭代器的值,虽然 C++ 提供了一个 typeid() 操作符,这个操作符只能获得型别的名称,但不能用来声明变量。要想获得迭代器型别,这个时候又该如何是好呢?

    function template 的参数推导机制是一个不错的方法。

    例如:

    如果 I 是某个指向特定对象的指针,那么在 func 中需要指针所指向对象的型别的时候,怎么办呢?这个还比较容易,模板的参数推导机制可以完成任务,

    template <class I> inline void func(I iter) { func_imp(iter, *iter); // 传入 iter 和 iter 所指的值,class 自动推导 } 

    通过模板的推导机制,就能轻而易举的获得指针所指向的对象的类型。

    template <class I, class T> void func_imp(I iter, T t) { T tmp; // 这里就是迭代器所指物的类别 // ... 功能实现 } int main() { int i; func(&i);//这里传入的是一个迭代器(原生指针也是一种迭代器) } 

    上面的做法呢,通过多层的迭代,很巧妙地导出了 T ,但是却很有局限性,比如,我希望 func() 返回迭代器的 value type 类型返回值, 函数的 "template 参数推导机制" 推导的只是参数,无法推导函数的返回值类型。万一需要推导函数的返回值,好像就不行了,那么又该如何是好?

    这就引出了下面的内嵌型别。

    4 、声明内嵌型别

    上述所说的 迭代器所指对象的型别,称之为迭代器的 value type

    尽管在 func_impl 中我们可以把 T 作为函数的返回值,但是问题是用户需要调用的是 func

    如果在参数推导机制上加上内嵌型别 (typedef) 呢?为指定的对象类型定义一个别名,然后直接获取,这样来看一下实现:

    template<typename T> class MyIter { public: typedef T value_type; //内嵌类型声明 MyIter(T *p = 0) : m_ptr(p) {} T& operator*() const { return *m_ptr;} private: T *m_ptr; }; //以迭代器所指对象的类型作为返回类型 //注意 typename 是必须的,它告诉编译器这是一个类型 template<typename MyIter> typename MyIter::value_type Func(MyIter iter) { return *iter; } int main(int argc, const char *argv[]) { MyIter<int> iter(new int(666)); std::cout<<Func(iter)<<std::endl; //print=> 666 } 

    上面的解决方案看着可行,但其实呢,实际上还是有问题,这里有一个隐晦的陷阱:实际上并不是所有的迭代器都是 class type ,原生指针也是一种迭代器,由于原生指针不是 class type ,所以没法为它定义内嵌型别

    因为 func 如果是一个泛型算法,那么它也绝对要接受一个原生指针作为迭代器,下面的代码编译没法通过:

    int *p = new int(5); cout<<Func(p)<<endl; // error 

    要解决这个问题,Partial specialization (模板偏特化)就出场了。

    5 、Partial specialization (模板偏特化)

    所谓偏特化是指如果一个 class template 拥有一个以上的 template 参数,我们可以针对其中某个(或多个,但不是全部)template 参数进行特化,比如下面这个例子:

    template <typename T> class C {...}; //此泛化版本的 T 可以是任何类型 template <typename T> class C<T*> {...}; //特化版本,仅仅适用于 T 为“原生指针”的情况,是泛化版本的限制版 

    所谓特化,就是特殊情况特殊处理,第一个类为泛化版本,T 可以是任意类型,第二个类为特化版本,是第一个类的特殊情况,只针对原生指针。

    5.1 、原生指针怎么办?特性 “萃取” traits

    还记得前面说过的参数推导机制+内嵌型别机制获取型别有什么问题吗?问题就在于原生指针虽然是迭代器但不是class ,无法定义内嵌型别,而偏特化似乎可以解决这个问题。

    有了上面的认识,我们再看看 STL 是如何应用的。STL 定义了下面的类模板,它专门用来“萃取”迭代器的特性,而value type 正是迭代器的特性之一:

    traitsbits/stl_iterator_base_types.h 这个文件中:

    template<class _Tp> struct iterator_traits<_Tp*> { typedef ptrdiff_t difference_type; typedef typename _Tp::value_type value_type; typedef typename _Tp::pointer pointer; typedef typename _Tp::reference reference; typedef typename _Tp::iterator_category iterator_category; }; 
    template<typename Iterator> struct iterator_traits { //类型萃取机 typedef typename Iterator::value_type value_type; //value_type 就是 Iterator 的类型型别 } 

    加入萃取机前后的变化:

    template<typename Iterator> //萃取前 typename Iterator::value_type func(Iterator iter) { return *iter; } //通过 iterator_traits 作用后的版本 template<typename Iterator> //萃取后 typename iterator_traits<Iterator>::value_type func(Iterator iter) { return *iter; } 

    看到这里也许你会问了,这个萃取前和萃取后的 typename:iterator_traits::value_typeIterator::value_type 看起来一样啊,为什么还要增加 iterator_traits 这一层封装,岂不是多此一举?

    回想萃取之前的版本有什么缺陷:不支持原生指针。而通过萃取机的封装,我们可以通过类模板的特化来支持原生指针的版本!如此一来,无论是智能指针,还是原生指针,iterator_traits::value_type 都能起作用,就解决了前面的问题。

    //iterator_traits 的偏特化版本,针对迭代器是原生指针的情况 template<typename T> struct iterator_traits<T*> { typedef T value_type; }; 

    看到这里,我们不得不佩服的 STL 的设计者们,真秒啊!我们用下面这张图来总结一下前面的流程:

    stl-iterator-09.png

    5.2 、const 偏特化

    通过偏特化添加一层中间转换的 traits 模板 class,能实现对原生指针和迭代器的支持,有的读者可能会继续追问:对于指向常数对象的指针又该怎么处理呢?比如下面的例子:

    iterator_traits<const int*>::value_type // 获得的 value_type 是 const int,而不是 int 

    const 变量只能初始化,而不能赋值(这两个概念必须区分清楚)。这将带来下面的问题:

    template<typename Iterator> typename iterator_traits<Iterator>::value_type func(Iterator iter) { typename iterator_traits<Iterator>::value_type tmp; tmp = *iter; // 编译 error } int val = 666 ; const int *p = &val; func(p); // 这时函数里对 tmp 的赋值都将是不允许的 

    那该如何是好呢?答案还是偏特化,来看实现:

    template<typename T> struct iterator_traits<const T*> { //特化 const 指针 typedef T value_type; //得到 T 而不是 const T } 

    6 、traits 编程技法总结

    通过上面几节的介绍,我们知道,所谓的 traits 编程技法无非 **就是增加一层中间的模板 class**,以解决获取迭代器的型别中的原生指针问题。利用一个中间层 iterator_traits 固定了 func 的形式,使得重复的代码大量减少,唯一要做的就是稍稍特化一下 iterator_tartis 使其支持 pointerconst pointer

    #include <iostream> template <class T> struct MyIter { typedef T value_type; // 内嵌型别声明 T* ptr; MyIter(T* p = 0) : ptr(p) {} T& operator*() const { return *ptr; } }; // class type template <class T> struct my_iterator_traits { typedef typename T::value_type value_type; }; // 偏特化 1 template <class T> struct my_iterator_traits<T*> { typedef T value_type; }; // 偏特化 2 template <class T> struct my_iterator_traits<const T*> { typedef T value_type; }; // 首先询问 iterator_traits<I>::value_type,如果传递的 I 为指针,则进入特化版本,iterator_traits 直接回答;如果传递进来的 I 为 class type,就去询问 T::value_type. template <class I> typename my_iterator_traits<I>::value_type Func(I ite) { std::cout << "normal version" << std::endl; return *ite; } int main(int argc, const char *argv[]) { MyIter<int> ite(new int(6)); std::cout << Func(ite)<<std::endl;//print=> 6 int *p = new int(7); std::cout<<Func(p)<<std::endl;//print=> 7 const int k = 8; std::cout<<Func(&k)<<std::endl;//print=> 8 } 

    上述的过程是首先询问 iterator_traits::value_type,如果传递的 I 为指针,则进入特化版本, iterator_traits 直接回答T;如果传递进来的 Iclass type ,就去询问 T::value_type

    通俗的解释可以参照下图:

    总结:核心知识点在于 模板参数推导机制+内嵌类型定义机制, 为了能处理原生指针这种特殊的迭代器,引入了偏特化机制traits 就像一台 “特性萃取机”,把迭代器放进去,就能榨取出迭代器的特性。

    这种偏特化是针对可调用函数 func 的偏特化,想象一种极端情况,假如 func 有几百万行代码,那么如果不这样做的话,就会造成非常大的代码污染。同时增加了代码冗余。

    7 、迭代器的型别和种类

    7.1 迭代器的型别

    我们再来看看迭代器的型别,常见迭代器相应型别有 5 种:

    • value_type:迭代器所指对象的类型,原生指针也是一种迭代器,对于原生指针 int*,int 即为指针所指对象的类型,也就是所谓的 value_type 。

    • difference_type: 用来表示两个迭代器之间的距离,对于原生指针,STL 以 C++ 内建的 ptrdiff_t 作为原生指针的 difference_type 。

    • reference_type: 是指迭代器所指对象的类型的引用,reference_type 一般用在迭代器的 * 运算符重载上,如果 value_type 是 T,那么对应的 reference_type 就是 T&;如果 value_type 是 const T,那么对应的 reference_type 就是 const T&。

    • pointer_type: 就是相应的指针类型,对于指针来说,最常用的功能就是 operator* 和 operator-> 两个运算符。

    • iterator_category: 的作用是标识迭代器的移动特性和可以对迭代器执行的操作,从 iterator_category 上,可将迭代器分为 Input Iterator 、Output Iterator 、Forward Iterator 、Bidirectional Iterator 、Random Access Iterator 五类,这样分可以尽可能地提高效率。

      template<typename Category, typename T, typename Distance = ptrdiff_t, typename Pointer = T*, typename Reference = T&> struct iterator //迭代器的定义 { typedef Category iterator_category; typedef T value_type; typedef Distance difference_type; typedef Pointer pointer; typedef Reference reference; }; 

    iterator class 不包含任何成员变量,只有类型的定义,因此不会增加额外的负担。由于后面三个类型都有默认值,在继承它的时候,只需要提供前两个参数就可以了。这个类主要是用来继承的,在实现具体的迭代器时,可以继承上面的类,这样子就不会漏掉上面的 5 个型别了

    对应的迭代器萃取机设计如下:

    tempalte<typename I> struct iterator_traits {//特性萃取机,萃取迭代器特性 typedef typename I::iterator_category iterator_category; typedef typename I::value_type value_type; typedef typeanme I:difference_type difference_type; typedef typename I::pointer pointer; typedef typename I::reference reference; }; //需要对型别为指针和 const 指针设计特化版本看 

    7.2 、迭代器的分类

    最后,我们来看看,迭代器型别 iterator_category 对应的迭代器类别,这个类别会限制迭代器的操作和移动特性。 除了原生指针以外,迭代器被分为五类:

    • Input Iterator: 此迭代器不允许修改所指的对象,是只读的。支持 ==、!=、++、*、-> 等操作。
    • Output Iterator:允许算法在这种迭代器所形成的区间上进行只写操作。支持 ++、* 等操作。
    • Forward Iterator:允许算法在这种迭代器所形成的区间上进行读写操作,但只能单向移动,每次只能移动一步。支持 Input Iterator 和 Output Iterator 的所有操作。
    • Bidirectional Iterator:允许算法在这种迭代器所形成的区间上进行读写操作,可双向移动,每次只能移动一步。支持 Forward Iterator 的所有操作,并另外支持 操作。
    • Random Access Iterator:包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种 Iterator 的所有操作,并另外支持 [n] 操作符等操作。

    那么,这里,小贺想问大家,为什么我们要对迭代器进行分类呢?迭代器在具体的容器里是到底如何运用的呢?这个问题就放到下一节在讲。

    最最后,我们再来回顾一下六大组件的关系:

    这六大组件的交互关系:container (容器) 通过 allocator (配置器) 取得数据储存空间,algorithm (算法)通过 iterator (迭代器)存取 container (容器) 内容,functor (仿函数) 可以协助 algorithm (算法) 完成不同的策略变化,adapter (配接器) 可以修饰或套接 functor (仿函数)。

    参考文章: 《 STL 源码剖析-侯捷》

    https://zhuanlan.zhihu.com/p/85809752 https://wendeng.github.io/2019/05/15/c++%E5%9F%BA%E7%A1%80/%E3%80%8ASTL%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E3%80%8B%E7%AC%AC3%E7%AB%A0%20%E8%BF%AD%E4%BB%A3%E5%99%A8%E4%B8%8Etraits%E7%BC%96%E7%A8%8B%E6%8A%80%E6%B3%95/

    作者简介

    宇宙中心五道口狗厂程序员,爱生活,好读书

    热爱分享: 公众号『 herongwei 』

    个人博客:herongwei.com

    微信:icoredump

    有问题欢迎知乎 @herongwei

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2672 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 37ms UTC 09:39 PVG 17:39 LAX 01:39 JFK 04:39
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86