章节总结
第三章 迭代器(iterators)概念与traits编程技法
3.1 迭代器设计思维——STL关键所在
STL中心思想在于:将数据容器和算法分开,彼此独立设计,最后再以一帖胶着剂将他们撮合在一起。迭代器就是算法和容器之间的胶着剂。
3.2 迭代器是一种smart pointer
迭代器是一种行为类似指针的对象,而指针的各种行为中最常见的也是最重要的便是内容提领和成员访问,因此迭代器最终要的编程工作就是对
operator*
和operator->
进行重载工作。
3.3 迭代器响应型别(associated types)
在STL算法中运用迭代器时,很可能会用到相应型别。什么是相应型别?迭代器所指之物的型别便是其一。比如
vector<int>nums
中的int
就是其响应型别。
- 假设有必要声明一个变量,以“迭代器所指对象的型别”为型别?怎么做
- 利用模板参数。比如:
template <typename I,typename T> void func_impl(I iter,T t){ T temp; // 这里就解决了问题 }
- 迭代器响应型别不只是“迭代器所指对象型别”一种,一共有五种,并非任何情况下任何一种都可以通过上诉的模板参数的方式实现;
3.4 Traits(特性)编程技法——STL源代码门钥
- 上述3.3中所说的“迭代器所指对象的型别”称为该迭代器的value_type.
- 当value_type必须用于函数的返回值,上述3.3做法不能使用。怎么操作?
- 声明模板内嵌类型
可以完成class type的问题,但是如果不是class type,就没办法定义内嵌类型。但STL绝对必须接受原生指针作为一种迭代器,所以上面的设计还不够template <class T> struct MyIter{ typedef T value_type; T * ptr; MyIter(T* p = 0):ptr(0){} T & operator*()const{return *ptr;} }; template <class I> typename I::value_type func(I iter){ return * iter; }
- 利用模板的Partial Specialization(偏特化)解决上述基础类型没办法内嵌的问题。此处引入iterator_traits。
- iterator_traits
上述函数template <class I> struct iterator_traits{ typedef typename I::value_type value_type; }
func
变成如下
这比之前多了一层间接性,好处是traits可以拥有特化版本,可以用于解决上述基础类型没办法定义内嵌类型的问题。template <class I> typename iterator_traits<I>::value_type func(I iter){ return * iter; }
- 利用iterator_traits解决基础类型不能定义内嵌类型的问题:
- T *
template <class T> struct iterator_traits<T*>{ typedef T value_type; };
- const T *
template <class T> struct iterator_traits<const T*>{ typedef T value_type; };
- 迭代器相应型别有五种
- value_type
- difference_type
- pointer
- reference
- iterator_catagory
template <class I> struct iterator_traits{ typedef typename I::iterator_catagory iterator_catagory; typedef typename I::value_type value_type; typedef typename I::difference_type difference_type; typedef typename I::pointer pointer; typedef typename I::reference reference; }
- value_type
所谓value_type,是指迭代器所指对象的型别。任何一个打算与STL算法有完美搭配的class,都应该定义自己的value_type内嵌型别。
-
difference_type
difference_type用来表示两个迭代器之间的距离,因此它也可以用来表示一个容器的最大容量,因为对于连续空间的容器而言,头尾之间的距离就是其最大容量。
//针对原生指针设计的“偏特化”版本 template <class I> struct iterator_traits<T*>{ ···· typedef ptrdiff_t difference_type; //ptrdiff_t为C++内建类型,在<cstddef>中 };
-
reference type
-
pointer type
//针对原生指针设计的“偏特化”版本 template <class T> struct iterator_traits<T*>{ ···· typedef T* pointer; typedef T& reference; }; //针对原生的pointer-to-const指针设计的“偏特化”版本 template <class T> struct iterator_traits<const T*>{ ···· typedef const T* pointer; typedef const T& reference; };
-
iterator_catagory(迭代器类别)
迭代器根据移动特性与施行操作,分为五类:- Input Iterator:这种迭代器所指对象,不允许外界改变。只读。
- Output Iterator:只写。
- Forword Iterator:允许“写入型”算法( 例如replace())在这种迭代器所形成的区间上进行读写操作。
- Bidirectional Iterator:可双向移动。
- Random Access Itertor:最强大,涵盖前面所有迭代器的功能。
-
设计算法时,针对上图的某种迭代器提供一个明确定义,并针对更强化的的某种迭代器提供另一种,这样才能在不同的情况下提供最大效率;
-
针对上述“提供最大效率”提供一个例子:
//针对Input Iterator template<class InputIterator,class Distance> void advance_II(InputIterator & i,Distance n) { //单向,逐一前进 while(n--)++i; } //针对Bidirectional Iterator template<class BidirectionalIterator,class Distance> void advance_BI(BidirectionalIterator & i,Distance n) { //双向,逐一前进 if(n>=0) while(n--)++i; else while(n++)++i; } //针对Random Access Iterator template<class BidirectionalIterator,class Distance> void advance_RAI(RandomAccessIterator & i,Distance n) { //双向,跳跃前进 i += n; }
现在程序调用advance()时,该选择那一份函数定义?
- 选择advance_II(),对RandomAccessIterator而言极度缺乏效率,O(1)–>O(n);
- 选择advance_RAI(),则它无法接受Input Iterator;
- 给出一种解决办法
template<class InputIterator,class Distance> void advance(InputIterator & i,Distance n) { if(is_random_access_iterator(i)) //此函数有待设计 advance_RAI(i,n); else if(is_bidrectional_iterator(i)) //此函数有待设计 advance_BI(i,n); else advance_II(i,n); }
-
上述解决办法有一个缺陷——这样的执行期才决定使用哪一个版本,会影响程序效率。最好是在编译器就能选择正确的版本。
-
解决上诉执行期选择损失效率的方法是:函数重载
前述三个advance_XX()都有两个函数参数,型别都未定(因为都是template参数)。为了令其同名,形成重载函数,我们必须加上一个型别已经确定的函数参数,使函数重载机制得以使用。
- 设计考虑如下:如果traits有能力萃取出迭代器的种类,我们便可以利用这个“迭代器类型”相应型别作为advanced()的第三个参数。
这个相应型别一定必须是一个class type,不能只是数值号码类的东西,因为编译器需要依赖它(一个型别)来进行重载决议。 - 定义五个classes,代表五种迭代器类型:
//五个作为标记用的型别(tag types) struct input_iterator_tag{}; struct output_iterator_tag{}; struct forword_iterator_tag:public input_iterator_tag{}; struct bidirectional_iterator_tag:public forword_iterator_tag{}; struct random_access_iterator_tag:public bidirectional_iterator_tag{};
- 设计考虑如下:如果traits有能力萃取出迭代器的种类,我们便可以利用这个“迭代器类型”相应型别作为advanced()的第三个参数。
-
重新设计上述“提供最大效率”的advance():
//函数第三个参数只用来激活重载机制,其他的什么也不做 template <class InputIterator,class Distance> inline void __advance(InputIterator & i,Distance n,input_iterator_tag) { //单向,逐一前进 while(n--)++i; } template <class ForwordIterator,class Distance> inline void __advance(ForwordIterator & i,Distance n,forword_iterator_tag) { //单纯的传递调用 _advance(i,n,input_iterator_tag()); } template <class InputIterator,class Distance> inline void __advance(BidirectionalIterator & i,Distance n,bidirectional_iterator_tag) { //双向,逐一前进 if(n>=0) while(n--)++i; else while(n++)--i; } template <class RandomAccessIterator,class Distance> inline void __advance(RandomAccessIterator & i,Distance n,random_access_iterator_tag) { //双向,跳跃前进 i += n; }
- 到此,还需要一个对外开放的上层控制接口,调用各个重载函数__advance()。这一上层接口只需要两个参数,当这一接口准备将工作转给上述的__advance()时,才自行加上第三个参数:迭代器类型。
- 这个上层接口必须有能力从它所获得的迭代器中推导出其类型(利用traits机制):
为了满足上述行为,traits必须再增加一个相应的型别template <class InputIterator,class Distance> inline void advance(InputIterator& i,Distance n) { //iterator_traits<InputIterator>::iterator_catagory()将产生一个暂时对象(如int()) //其型别对应于前述四个迭代器(I,F,B,R) __advance(i,n,iterator_traits<InputIterator>::iterator_catagory()); }
template<class I> struct iterator_traits{ ··· typedef typename I::iterator_catagory iterator_catagory; }; //针对原生指针而设计的“偏特化的版本”,原生指针是一种Random Access Iterator template<class T> struct iterator_traits<T*>{ ··· typedef random_access_iterator_tag iterator_catagory; }; //针对pointer-to-const而设计的“偏特化版本” template<class T> struct iterator_traits<const T*>{ ··· typedef random_access_iterator_tag iterator_catagory; };
-
仔细观察advance()既然可以接受各种类型的迭代器,就不应该将其型别参数命名为InputIterator。
template <class InputIterator,class Distance> inline void advance(InputIterator& i,Distance n);
这是STL算法的一个命名规则:以算法所能接受之最低阶迭代器类型,来为迭代器型别参数命名。
-
消除“单纯传递调用的函数”(上述advance() ForwordIterator版)
以class来定义迭代器的各种分类标签,不仅可以促成重载机制的成功运作,另一个好处是,通过继承,我们可以不必写“单纯只做传递调用”的函数。
#include<iostream> using namespace std; struct B{}; struct D1 : public B{}; struct D2 : public D1{}; template <class I> func(I& p,B) {cout<<"B version"<<endl;} template<class I> func(I& p,D2) {cout<<"D2 version"<<endl;} int main() { int * p; func(p,B());//参数完全吻合。输出:“B version” func(p,D1());//参数未能完全吻合;因继承关系而转自动调用,输出“B version”; func(p,D2()); //参数完全吻合。输出:"D2 version"; }
3.5 std::iterator的保证
-
std::iterator关于对用户自定义iterator的规定
为了符合规范,任何迭代器都应该提供五个内嵌相应型别,以利于traits萃取,否则便是自别于整个STL架构,无法与其他组件顺利搭配。
STL提供了一个iterators class如下,如果每个新设计的迭代器都继承自它,就可保证符合STL所需之规范:
template<class Category, class T, class Distance = ptrdiff_t, class Pointer = T*, class Reference = T&> struct iterator{ typedef Category iterator_catagory; typedef T value_type; typedef Distance difference_type; typedef Pointer pointer; typedef Reference reference; }
-
迭代器负责设计适当的相应的型别。容器负责设计适当的迭代器。算法独立于容器和迭代器之外,只要设计时以迭代器为对外接口就行。
-
traits编程技法大量运用于STL实现品中。
它利用“内嵌型别”的编程技巧与编译器的template参数推导功能,增强C++未能提供的关于型别认证方面的能力,弥补C++不为强型别语言的遗憾。
3.6 iterator源代码完整序列
3.7 SGI STL的私房菜:__type_traits
- traits编程技法适度弥补了C++语言本身的不足。STL只对迭代器加一规范,制定出iterator_traits。SGI把这种技法进一步扩大到迭代器以外的世界,于是就有了__type_traits。(不再STL标准,在SGI STL内部)
- iterator_traits负责萃取迭代器的特性,__type_traits则负责萃取型别的特性。
- 此处我们所关注的型别特性是指:这个型别是否具备:
- non-trivial default ctor(不是无用的构造函数)
- non-trivial copy ctor(不是无用的赋值构造函数)
- non-trivial assignment operator(不是无用的赋值重载函数)
- non-trivial dtor(不是无用的析构函数)
- 如果答案是否定的,我们在对这个型别进行构造、拷贝、赋值、析构等操作时,就可以采用最优效率的措施。
- 此处我们所关注的型别特性是指:这个型别是否具备:
- 定义于SGI<type_traits.h>中的__type_traits,提供了一种机制,允许不同的型别属性,在编译时期完成函数派送决定。
- 根据iterator_traits的来的经验,我们希望,程序中可以这样运用__type_traits,T为任意类别:
__type_traits<T>::has_trivial_default_constructor; __type_traits<T>::has_trivial_copy_constructor; __type_traits<T>::has_trivial_assignment_operator; __type_traits<T>::has_trivial_destructor; __type_traits<T>::is_POD_type;
- 我们希望利用其响应结果来进行参数推导,所以其结果应该是个有着真假值的“对象”,所以设计如下:
struct __true_type{}; struct __false_type{};
- 根据iterator_traits的来的经验,我们希望,程序中可以这样运用__type_traits,T为任意类别:
- 为了达成上述的五个式子,__type_traits内必须定义一些typedefs,其值不是__ture_type就是__false__type。下面给出SGI的做法:
template <class type> struct __type_traits{ typedef __true_type this_dummy_member_must_be_first; 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; }
- <type_traits.h>对所有C++标量性别所定义的__type_traits特化版本
总结
- STL中的iterator就是利用“内嵌型别”的编程技巧和编译器的template参数推导功能,来实现作为STL中算法和容器的胶着剂。我们可以把迭代器视为一个广义的指针(或者智能指针),其中最重要的就是operator和*operator->**重载。
- 为了实现对容器template的更好的贴合,迭代器规定内部必须含有特定的五个型别:
- iterator_category
- value_type
- difference_type
- pointer
- reference
为了可以支持原始指针作为一种迭代器,STL新引入了iterator_traits,实现对原始指针可能的“偏特化版本”。
- 目前STL内部有五种迭代器类别,不同的迭代器同一个算法选择的实现会不一样(为了获得最大效率)
- Input Iterator:这种迭代器所指对象,不允许外界改变。只读。
- Output Iterator:只写。
- Forword Iterator:允许“写入型”算法( 例如replace())在这种迭代器所形成的区间上进行读写操作。
- Bidirectional Iterator:可双向移动。
- Random Access Itertor:最强大,涵盖前面所有迭代器的功能。
- 迭代器分类后,可以利用traits特性萃取出当前迭代器类型,并选择最符合当前迭代器类型的同一算法下的不同实现,以达到最好效率。(通过函数重载)
- 从iterator_traits扩展出的 __type_traits 可以萃取型别的特性。可以通过__type_traits判断对象是否有该判断属性,以便采用不同的策略,以提供更好的效能。