STL之迭代器(iterator)与traits编程技法深入学习

学习STL就不得不学习iterator,我们知道STL的中心思想——将数据容器和算法分开,彼此独立设计,再以一贴胶着剂将它们撮合在一起。

对容器和算法的泛化我们可以使用class template和function template。那么,谁来充当这个“胶着剂”呢?对咯,就是iterator。

一,迭代器是一种smart pointer

(1)迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的便是内容提领(dereference)和成员访问(member access)。
(2)因此,迭代器最重要的编程工作就是对operator*和operator->进行重载(overloading)工作。
(3)根据移动特性与实施操作,迭代器被分为五类(iterator_category):

Input Iterator:只读。
Output Iterator:只写。
Forward Iterator:允许“写入型”算法(如replace())在此种迭代器所形成的区间上进行读写操作。
Bidirectional Iterator:可双向移动,某些算法需要逆向走访某个迭代器区间。
Random Access Iterator:前四种迭代器只供应一部分指针算术能力(前三中支持operator ++, 第四种支持operator–),第五种则涵盖所有指针算术能力,包括p+n, p-n, p[n], p1-p2, p1<p2。

二,迭代器相应的类型(associated types)

(1)参数推导机制
我们知道,在算法中使用迭代器时,很可能会用到迭代器相应的类型(即迭代器所指对象的类型)。假设某个算法中需要声明一个变量,以迭代器所指对象的类型为类型,那该怎么办呢?毕竟C++只支持sizeof(),并未支持typeof()。
这里需要注意一点:上述的变量类型指的是迭代器所指对象类型,而不是迭代器类型。明白这一点,接着往下看该如何取出这个对象的类型呢?
解决办法是:利用function template的参数推导机制。我看如下示例:
(1)我们以func()为对外接口,把实际操作全部置于func_impl()之中。
(2)由于func_impl()是一个function template,一旦被调用,编译器会自动进行template参数推导。

三, Traits编程技法——STL源代码的门钥

(1)声明内嵌类型

迭代器所指对象的类型称为该迭代器的value type。上述的参数推导技巧虽然可用于value type,却非全面可用:万一value type必须用于函数的传回值,就束手无策了。毕竟template参数推导机制推而导之的只是参数,无法推导函数的返回类型。所以我们需要其他办法,申明内嵌类型可以吗?

(a)func()的返回类型必须加上关键字typename,因为T是一个template参数,在编译器具体实现之前并不知道MyIter<T>::value_type代表的是类型或是member function还是data member。
(b)关键字typename的作用就是告诉编译器这是一个类型,才能顺利通过编译。

以为这就完了吗?NO, 图样图森破!
现在问题又来了,并不是所有的迭代器都是class type,原生指针就不是哦。所以,不是class type就无法定义它的内嵌类型
但是STL必须能接受原生指针作为一种迭代器,所以上面这样还不够,我们继续往下谈(蓝色香菇)…

(2)Partial Specialization(偏特化)

什么是Partial Specialization?
Partial Specialization的大致意义是:如果class template拥有一个以上的template参数,我们可以针对其中某个(或数个,但非全部)template参数进行特化工作。换句话说,我们可以在泛化设计中提供一个特化版本(也就是将泛化版本中的某些template参数赋予明确的指定)。
比如,我们现在有如下的class template:

我们便很容易的接受它的一个形式如下的partial specialization:

ok,有了这么一个东西之后,我们就能解决前面“内嵌类型”未能解决的问题了。我们现在可以针对“迭代器的template参数为指针”的情况,设计特化版的迭代器了。

(3)iterator_traits来萃取value_type

下面这个class template专门用来萃取迭代器特性,而value type正是迭代器的特性之一:

这个所谓的traits,其意义是,如果I定义有自己的value type,那么通过这个traits的作用,萃取出来的value_type就是I::value_type。换句话说,如果I定义有自己的value type,先前那个func()可以改写成这样:

与之前的版本对比,仅仅只是加了iterator_traits这一中间层而已?其实不然,有了这一层中间层后,我们就可以在这个中间层做一些手脚了。
比如,iterator_traits可以拥有特化版本:

于是,原生指针int*虽然不是一种class type,也可通过traits取出其value_type。解决了前一个问题。

以为这就完了吗?NO, 图样图森破!

(4)原生指针是指向常数对象的指针(pointer-to-const)

如果指针是指向常数对象的指针,即使萃取出来之后,也无法对其进行赋值等操作也是枉然。例如:
iterator_traits <const int*>::value_type
这该怎么处理呢?
简单,我们在设计一个特化版本,直接在iterator_traits中间层将其const属性丢掉即可,具体如下:

终于大功告成,现在不管面对的是迭代器MyIter,或是原生指针int*或是const int*,都可以通过traits取出正确的value_type。

小结:参数推导机制–>声明内嵌类型–>Partial Specialization–>iterator_traits萃取出int*–>萃取const int*

最后附上traits工作机制的图,辅助理解traits的工作原理:STL源码图3-1

掌握了iterator_traits之后,我们便可以愉快的学习iterator了。

常用的迭代器类型有上图所示的五种,类似于value_type的提取,其他四种如下:

总结:traits编程技法大量运用于STL实现品中,它利用“内嵌类型”的编程技巧与编译器的template参数推导功能,增强C++未能提供的关于类型认证方面的能力,弥补C++不为强类型语言的遗憾。

四,SGI STL的私房菜:__type_traits

了解了traits编程技法之后,我们再来介绍最后一个知识点——__type_traits。这是SGI把这种编程技术进一步扩大到了迭代器以外的世界。
这里有一点说明:单底线前缀指的是STL内部使用的;双底线前缀指的是SGI内部所用的东西。

(1)iterator_traits负责萃取迭代器的特性,__type_traits则负责萃取类型的特性。
(2)此处我们关注的特性是指:这个类型是否具备non-trivial default ctor?是否具备non-trivial copy ctor?是否具备non-trivial assignment operator?是否具备non-trivial dtor?
(3)如果答案是否定的,我们在对这个类型进行构造、析构、拷贝、赋值等操作时,就可以采用最有效的措施(例如根本不调用那些construct,destructor),而采用内存直接处理操作如malloc(), memcpy()等等,获得最高效率。
(还记得在配置器析构函数的处理机制吗?先利用value_type()获得迭代器所指对象类型,再利用__type_traits<T>判断该类型的析构函数是否是无关痛痒的)

配置器中关于destroy()函数运用__type_traits机制的部分内容转到:http://dulishu.top/allocator-of-stl-2/

完…

发表评论

电子邮件地址不会被公开。 必填项已用*标注