STL源码学习——迭代器(iterators)与traits编程技法
2016-03-05 17:18
483 查看
今天反复看了侯捷的《STL源码剖析》第三章,来回翻了3,4遍,终于基本上看懂了,在此作整理和总结。
迭代器在STL中得到了广泛的应用,通过迭代器,容器和算法可以有机的粘合在一起,只要对算法给予不同的迭代器,就可以对不同容器进行相同的操作。也就是说,数据容器和算法是分开的。
下面以算法find为例,展示容器、算法和迭代器之间的合作:
只要给予不同的迭代器,比如
find()就能对不同的容器进行查找,而无需针对某个容器来设计多个版本。
从上面看来,迭代器似乎依附在容器之下,有没有独立而范用的迭代器?
这个问题先留着,后面自有答案。
智能指针在这里使用的好处:
不用担心内存泄漏;
对于list,取下一个元素不是通过自增而是通过next指针来取,这样子暴露了太多东西,使用智能指针可以对自增进行重载,从而提供统一接口(见下面代码)
下面我们就从设计一个独立的list迭代器谈起。
首先先设计一个list:
下面是List迭代器的简单实现:
下面用ListIter将List和find()粘合起来:
需要注意的是,算法find是通过*first != value用来判断元素是否符合要求,而上面测试代码中,first的类型为ListItem< int >,而value的类型为int,两者之间并没有可用的operator!=函数,因此,需要另外声明一个全局的operator!=重载函数,代码如下:
可以看到,上述实现暴露了ListItem和ListItem的函数Next()。
为了实现迭代器ListIter,我们在很多地方暴露了容器List的内部实现ListItem,这违背一开始说的迭代器模式中不暴露某个容器的内部表现形式情况下,使之能依次访问该容器中的各个元素的定义。
可见,独立的迭代器并不能满足我们的要求,所以STL将迭代器的实现交给了容器,每种容器都会以嵌套的方式在内部定义专属的迭代器。各种迭代器的接口相同,内部实现却不相同,这也直接体现了泛型编程的概念。
(回答了上一节的问题 :) )
总结:
迭代器依附于具体的容器,即不同的容器有不同的迭代器实现。
对于泛型算法find,只要给它传入不同的迭代器,就可以对不同的容器进行查找操作。迭代器的穿针引线,有效地实现了算法对不同容器的访问。
现在我们就来看看这个东西!
看看下面这种情况:
如果函数func传入一个迭代器:
而在这个函数里面,我们需要用得到迭代器的value type,以下做法是否可行?
事实上,以上代码是编译失败的,C++并不提供这个支持。
一种解决方法是使用function template的参数推导机制:
上面做法确实很巧妙地导出了T,但是却很有局限性,比如,我的func()希望返回迭代器的value type类型返回值,那么上面的做法就无能为力了。
一种解决方案是,使用内嵌类型(这也是STL采用的方法):
上面的解决方案近乎完美了,可是刚刚我就说过,原生指针也是一种迭代器。由于原生指针不是class type,所以没法为它定义内嵌型别。
要解决这个问题,Partial specialization(模板偏特化)就出场了。
关于模板特化,具体请移步我的另一篇博文:
模板特化和偏特化
在这里我们只特意再说明下指针的特化:
所谓特化,就是特殊情况特殊处理,第一个类为泛化版本,T可以是任意类型,第二个类为特化版本,是第一个类的特殊情况。
有了上面的认识,我们再看看STL是如何应用的:
STL定义了下面的类模板,它专门用来“萃取”迭代器的特性,而value type正是迭代器的特性之一:
我们看看加入萃取机前后的变化:
看到这里也许你会晕了,iterator_traits::value_type跟Iterator::value_type完全是同一个东西,为什么还要增加iterator_traits这一层封装,是不是多此一举?
回想萃取之前的版本有什么缺陷:不支持原生指针。
而通过萃取机的封装,我们可以通过类模板的特化来特化出原生指针的版本!
如此一来,无论是智能指针,还是原生指针,iterator_traits::value_type都能起作用,这就解决了前面的问题。
在此,还有一个特殊情况,对于指向常数对象的指针:
这里获得的是const int,我们知道,const变量只能初始化,而不能赋值(这两个概念必须区分清楚)。
这将带来问题:
可以看到,我们的本意是获取int,而事实上获取到的是const int,这将造成误会!
所以需要进行特化:
现在,无论是智能指针,还是原生指针 int * 和 const int *,都可以通过traits取出正确(我们期待的)value type了!
总结:
traits就像一台“特性萃取机”,把迭代器放进去,就能榨取出迭代器的特性。
常见迭代器相应型别有5种:
5中迭代器型别的简单介绍:
1.value_type:迭代器所指对象的类型,原生指针也是一种迭代器,对于原生指针int*,int即为指针所指对象的类型,也就是所谓的value_type。
2.difference_type用来表示两个迭代器之间的距离,对于原生指针,STL以C++内建的ptrdiff_t作为原生指针的difference_type。
3.reference_type是指迭代器所指对象的类型的引用,reference_type一般用在迭代器的*运算符重载上,如果value_type是T,那么对应的reference_type就是T&;如果value_type是const T,那么对应的reference_type就是const T&。
4.pointer_type就是相应的指针类型,对于指针来说,最常用的功能就是operator*和operator->两个运算符。
iterator_category的作用是标识迭代器的移动特性和可以对迭代器执行的操作,从iterator_category上,可将迭代器分为Input Iterator、Output Iterator、Forward Iterator、Bidirectional Iterator、Random Access Iterator五类,这样分可以尽可能地提高效率。
下面是迭代器的定义:
类iterator不包含任何成员变量,只有类型的定义,因此不会增加额外的负担。由于后面三个类型都有默认值,在继承它的时候,只需要提供前两个参数就可以了。
这个类主要是用来继承的,在实现具体的迭代器时,可以继承上面的类,这样子就不会漏掉上面的5个型别了。
除了原生指针以外,迭代器被分为五类:
Input Iterator
此迭代器不允许修改所指的对象,即是只读的。支持==、!=、++、*、->等操作。
Output Iterator
允许算法在这种迭代器所形成的区间上进行只写操作。支持++、*等操作。
Forward Iterator
允许算法在这种迭代器所形成的区间上进行读写操作,但只能单向移动,每次只能移动一步。支持Input Iterator和Output Iterator的所有操作。
Bidirectional Iterator
允许算法在这种迭代器所形成的区间上进行读写操作,可双向移动,每次只能移动一步。支持Forward Iterator的所有操作,并另外支持–操作。
Random Access Iterator
包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种Iterator的所有操作,并另外支持it + n、it - n、it += n、 it -= n、it1 - it2和it
等操作。
迭代器的分类和从属关系可用下面的图表示(注意,这里的箭头并不代表继承关系,而是一种概念上的联系):
分类的原因:
设计算法时,如果可能,我们尽量针对上面某种迭代器提供一个明确定义,并针对更强化的某种迭代器提供另一种定义,这样才能在不同情况下提供最大效率。
比如,有个算法可接受Forward Iterator,但是你传入一个Random Access Iterator,虽然可用(Random Access Iterator也是一种Forward Iterator),但是不一定是最佳的,因为Random Access Iterator可能更加臃肿,效率不一定高。
对于一个算法,它该调用哪个类型的迭代器,我们可以简单的在内部使用if…else在执行时选择,但是这样却降低了效率,如果能在编译时选择就再好不过了。STL使用了重载函数机制达成了这个目标,此处就不再深入讨论了~有兴趣的可以参加《STL源码剖析》3.4。
花了一个下午终于理完了思路,虽然这里很多内容都是从书中摘录下来的,不过自己摘录一遍,并适当调整顺序和语言,更适合自己理解,印象也深刻了许多。虽说看懂了,但是要真正化为己用,大概还需要多磨练吧。
迭代器
迭代器模式的定义:提供一种方法,在不需要暴露某个容器的内部表现形式情况下,使之能依次访问该容器中的各个元素。迭代器在STL中得到了广泛的应用,通过迭代器,容器和算法可以有机的粘合在一起,只要对算法给予不同的迭代器,就可以对不同容器进行相同的操作。也就是说,数据容器和算法是分开的。
下面以算法find为例,展示容器、算法和迭代器之间的合作:
[code]template<typename InputIterator, typename T> InputIterator find(InputIterator first, InputIterator last, const T &value) { while (first != last && *frist != value) ++first; return first; }
只要给予不同的迭代器,比如
[code]vector<int>::iterator list<int>::iterator ……
find()就能对不同的容器进行查找,而无需针对某个容器来设计多个版本。
从上面看来,迭代器似乎依附在容器之下,有没有独立而范用的迭代器?
这个问题先留着,后面自有答案。
迭代器是一种smart pointer
迭代器是一种智能指针,是一种行为类似指针的对象,它内部封装了一个原始指针,并重载了operator*() 和operator->()等操作。智能指针在这里使用的好处:
不用担心内存泄漏;
对于list,取下一个元素不是通过自增而是通过next指针来取,这样子暴露了太多东西,使用智能指针可以对自增进行重载,从而提供统一接口(见下面代码)
[code]//对于数组的实现 template<typename T> Iterator& operator++() { ++m_ptr; retrun *this; } //对于链表的实现 template<typename T> Iterator& operator++() { m_ptr = m_ptr->next();//next()用于获取链表的下一个节点 return *this; }
下面我们就从设计一个独立的list迭代器谈起。
首先先设计一个list:
[code]//List节点的结构 template<typename T> class ListItem { public: ListItem() { m_pNext = 0;} ListItem(T v, ListItem *p = 0) { m_value = v; m_pNext = p;} T value() const { return m_value;} ListItem* next() const { return m_pNext;} private: T m_value; //存储的数据 ListItem* m_pNext; //指向下一个ListItem的指针 }; //List表的结构 template<typename T> class List { public: //从链表尾部插入元素 void Push(T value) { m_pTail = new ListItem<T>(value); m_pTail = m_pTail->next(); } //返回链表头部指针 ListItem<T>* begin() const { return m_pHead;} //返回链表尾部指针 ListItem<T>* end() const { return m_pTail;} //其它成员函数 private: ListItem<T> *m_pHead; //指向链表头部的指针 ListItem<T> *m_pTail; //指向链表尾部的指针 long m_nSize; //链表长度 };
下面是List迭代器的简单实现:
[code]template<typename T> class ListIter { public: ListIter(T *p = 0) : m_ptr(p){} //解引用,即dereference T& operator*() const { return *m_ptr;} //成员访问,即member access T* operator->() const { return m_ptr;} //前置++操作 ListIter& operator++() { m_ptr = m_ptr->next(); //暴露了ListItem的东西 return *this; } //后置++操作 ListIter operator++(int) { ListIter temp = *this; ++*this; return temp; } //判断两个ListIter是否指向相同的地址 bool opeartor==(const ListIter &arg) const { return arg.m_ptr == m_ptr;} //判断两个ListIter是否指向不同的地址 bool operator!=(const ListIter &arg) const { return arg.m_ptr != m_ptr;} private: T *m_ptr; };
下面用ListIter将List和find()粘合起来:
[code]int main(int argc, const char *argv[]) { List<int> mylist; for (int i = 0; i < 5; ++i) { mylist.push(i); } //暴露了ListItem ListIter<ListItem<int> > begin(mylist.begin()); ListIter<ListItem<int> > end(mylist.end()); ListIter<ListItem<int> > iter; iter = find(begin, end, 3);//从链表中查找3 if (iter != end) cout<<"found"<<endl; else cout<<"not found"<<endl; }
需要注意的是,算法find是通过*first != value用来判断元素是否符合要求,而上面测试代码中,first的类型为ListItem< int >,而value的类型为int,两者之间并没有可用的operator!=函数,因此,需要另外声明一个全局的operator!=重载函数,代码如下:
[code]template<typename T> bool operator!=(const ListItem<T> &item, T n) { return item.Value() != n; }
可以看到,上述实现暴露了ListItem和ListItem的函数Next()。
为了实现迭代器ListIter,我们在很多地方暴露了容器List的内部实现ListItem,这违背一开始说的迭代器模式中不暴露某个容器的内部表现形式情况下,使之能依次访问该容器中的各个元素的定义。
可见,独立的迭代器并不能满足我们的要求,所以STL将迭代器的实现交给了容器,每种容器都会以嵌套的方式在内部定义专属的迭代器。各种迭代器的接口相同,内部实现却不相同,这也直接体现了泛型编程的概念。
(回答了上一节的问题 :) )
总结:
迭代器依附于具体的容器,即不同的容器有不同的迭代器实现。
对于泛型算法find,只要给它传入不同的迭代器,就可以对不同的容器进行查找操作。迭代器的穿针引线,有效地实现了算法对不同容器的访问。
traits编程技法
还记得上一篇文章中,我们使用traits来判断某类型的构造和析构函数是否平凡的,以此来进行优化吗?现在我们就来看看这个东西!
迭代器相应型别
迭代器所指对象的型别,称为该迭代器的value type,比如int*的value type为int。看看下面这种情况:
如果函数func传入一个迭代器:
[code]template<typename Iterator> void func(Iterator iter) { //函数体 }
而在这个函数里面,我们需要用得到迭代器的value type,以下做法是否可行?
[code]template<typename Iterator> void func(Iterator iter) { *Iterator var;//ok? }
事实上,以上代码是编译失败的,C++并不提供这个支持。
一种解决方法是使用function template的参数推导机制:
[code]template<typename Iterator, typename T> void func_impl(Iterator iter, T t) { T temp;//这里就解决了问题 //这里做原本func()的工作 } template<typename Iterator> void func(Iterator iter) { func_impl(iter, *iter);//这里通过传递*iter,让func_impl去推导 } int main(int argc, const char *argv[]) { int i; func(&i); //这里传入的是一个迭代器(原生指针也是一种迭代器) }
上面做法确实很巧妙地导出了T,但是却很有局限性,比如,我的func()希望返回迭代器的value type类型返回值,那么上面的做法就无能为力了。
一种解决方案是,使用内嵌类型(这也是STL采用的方法):
[code]template<typename T> class Iterator { public: typedef T value_type; //内嵌类型声明 Iterator(T *p = 0) : m_ptr(p) {} T& operator*() const { return *m_ptr;} //... private: T *m_ptr; }; template<typename Iterator> //以迭代器所指对象的类型作为返回类型 //注意typename是必须的,它告诉编译器这是一个类型 typename Iterator::value_type func(Iterator iter) { return *iter; } int main(int argc, const char *argv[]) { Iterator<int> iter(new int(10)); cout<<func(iter)<<endl; //输出:10 }
上面的解决方案近乎完美了,可是刚刚我就说过,原生指针也是一种迭代器。由于原生指针不是class type,所以没法为它定义内嵌型别。
要解决这个问题,Partial specialization(模板偏特化)就出场了。
关于模板特化,具体请移步我的另一篇博文:
模板特化和偏特化
在这里我们只特意再说明下指针的特化:
[code]template <typename T> class C {...}; //此泛化版本的T可以是任何类型 template <typename T> class C<T*> {...}; //特化版本,T为指针类型
所谓特化,就是特殊情况特殊处理,第一个类为泛化版本,T可以是任意类型,第二个类为特化版本,是第一个类的特殊情况。
有了上面的认识,我们再看看STL是如何应用的:
STL定义了下面的类模板,它专门用来“萃取”迭代器的特性,而value type正是迭代器的特性之一:
[code]template<typename Iterator> struct iterator_traits { typedef typename Iterator::value_type value_type; };
我们看看加入萃取机前后的变化:
[code]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; }
看到这里也许你会晕了,iterator_traits::value_type跟Iterator::value_type完全是同一个东西,为什么还要增加iterator_traits这一层封装,是不是多此一举?
回想萃取之前的版本有什么缺陷:不支持原生指针。
而通过萃取机的封装,我们可以通过类模板的特化来特化出原生指针的版本!
[code]//iterator_traits的偏特化版本,针对迭代器是原生指针的情况 template<typename T> struct iterator_traits<T*> { typedef T value_type; };
如此一来,无论是智能指针,还是原生指针,iterator_traits::value_type都能起作用,这就解决了前面的问题。
在此,还有一个特殊情况,对于指向常数对象的指针:
[code]iterator_traits<const int*>::value_type //获得的value_type是const int,并不是int
这里获得的是const int,我们知道,const变量只能初始化,而不能赋值(这两个概念必须区分清楚)。
这将带来问题:
[code]template<typename Iterator> typename iterator_traits<Iterator>::value_type func(Iterator iter) { typename iterator_traits<Iterator>::value_type tmp; tmp = *iter; //ok? } int val = 8; const int *p = &val; func(p); //这时函数里对tmp的赋值都将是不允许的
可以看到,我们的本意是获取int,而事实上获取到的是const int,这将造成误会!
所以需要进行特化:
[code]template<typename T> struct iterator_traits<const T*> { typedef T value_type; //得到T而不是const T }
现在,无论是智能指针,还是原生指针 int * 和 const int *,都可以通过traits取出正确(我们期待的)value type了!
总结:
traits就像一台“特性萃取机”,把迭代器放进去,就能榨取出迭代器的特性。
常见迭代器相应型别有5种:
[code]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指针设计特化版本
5中迭代器型别的简单介绍:
1.value_type:迭代器所指对象的类型,原生指针也是一种迭代器,对于原生指针int*,int即为指针所指对象的类型,也就是所谓的value_type。
2.difference_type用来表示两个迭代器之间的距离,对于原生指针,STL以C++内建的ptrdiff_t作为原生指针的difference_type。
3.reference_type是指迭代器所指对象的类型的引用,reference_type一般用在迭代器的*运算符重载上,如果value_type是T,那么对应的reference_type就是T&;如果value_type是const T,那么对应的reference_type就是const T&。
4.pointer_type就是相应的指针类型,对于指针来说,最常用的功能就是operator*和operator->两个运算符。
iterator_category的作用是标识迭代器的移动特性和可以对迭代器执行的操作,从iterator_category上,可将迭代器分为Input Iterator、Output Iterator、Forward Iterator、Bidirectional Iterator、Random Access Iterator五类,这样分可以尽可能地提高效率。
下面是迭代器的定义:
[code]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不包含任何成员变量,只有类型的定义,因此不会增加额外的负担。由于后面三个类型都有默认值,在继承它的时候,只需要提供前两个参数就可以了。
这个类主要是用来继承的,在实现具体的迭代器时,可以继承上面的类,这样子就不会漏掉上面的5个型别了。
迭代器的分类
前面提到迭代器的分类,下面我们就简单讨论下迭代器的分类。除了原生指针以外,迭代器被分为五类:
Input Iterator
此迭代器不允许修改所指的对象,即是只读的。支持==、!=、++、*、->等操作。
Output Iterator
允许算法在这种迭代器所形成的区间上进行只写操作。支持++、*等操作。
Forward Iterator
允许算法在这种迭代器所形成的区间上进行读写操作,但只能单向移动,每次只能移动一步。支持Input Iterator和Output Iterator的所有操作。
Bidirectional Iterator
允许算法在这种迭代器所形成的区间上进行读写操作,可双向移动,每次只能移动一步。支持Forward Iterator的所有操作,并另外支持–操作。
Random Access Iterator
包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种Iterator的所有操作,并另外支持it + n、it - n、it += n、 it -= n、it1 - it2和it
等操作。
迭代器的分类和从属关系可用下面的图表示(注意,这里的箭头并不代表继承关系,而是一种概念上的联系):
分类的原因:
设计算法时,如果可能,我们尽量针对上面某种迭代器提供一个明确定义,并针对更强化的某种迭代器提供另一种定义,这样才能在不同情况下提供最大效率。
比如,有个算法可接受Forward Iterator,但是你传入一个Random Access Iterator,虽然可用(Random Access Iterator也是一种Forward Iterator),但是不一定是最佳的,因为Random Access Iterator可能更加臃肿,效率不一定高。
对于一个算法,它该调用哪个类型的迭代器,我们可以简单的在内部使用if…else在执行时选择,但是这样却降低了效率,如果能在编译时选择就再好不过了。STL使用了重载函数机制达成了这个目标,此处就不再深入讨论了~有兴趣的可以参加《STL源码剖析》3.4。
花了一个下午终于理完了思路,虽然这里很多内容都是从书中摘录下来的,不过自己摘录一遍,并适当调整顺序和语言,更适合自己理解,印象也深刻了许多。虽说看懂了,但是要真正化为己用,大概还需要多磨练吧。
相关文章推荐
- c语言:在3*4的矩阵中,求元素的最大值
- 20145120 《Java程序设计》第1周学习总结
- leetcode:121. Best Time to Buy and Sell Stock(Java)解答
- 字段和属性(C#)
- Java中如何使用排序详解
- caffe训练灰度图像的时候识别率很高,但是在使用matlab接口重新测试的时候识别率却很低?
- C语言-函数
- java出现的原因
- java联合编译,类型转换,键盘输入
- vsftpd配置文件讲解
- struts2升级至2.3.24方法
- leetcode:Pow(x, n) 【Java】
- 详解struts2中配置action的方法
- 客户端python热更新
- SPRING IN ACTION 第4版笔记-第七章Advanced Spring MVC-003- 上传文件multipart,配置StandardServletMultipartResolver、CommonsMultipartResolver
- VB.net版机房收费-“未能添加对‘Factory’的引用”
- python基础0305
- Asp.Net页面传值的几种方式
- 适用于 php-5.2 的 php.ini 中文版
- 深入理解java垃圾回收机制