这是stl源码阅读系列的第二篇,这一篇来看看stl中迭代器traits的实现。
在stl中,有三个彼此关联的组件:
Containers(即我们常用的vector, list, deque等)装载数据结构,Algorithms装载数据操作,两者相互独立,Iterator作为中间人,将两者联系起来。iterator提供访问容器数据成员的接口,隐藏了每个容器的具体存储实现(即迭代器设计模式)。每个容量都会提供一份iterator实现,所以在分析容器前,先看看iterator。
另外,整个stl可以看做泛型编程的最佳实践,所以在本篇中也会介绍一些泛型(模板)编程的概念。
1. 什么是泛型/模板?·
考虑一个场景,如果要实现一个取两个值中的最大值的函数,可以做如下实现:
1 | inline int Max(int a, int b) { |
上面对int类型做了实现,如果现在的需求是对int
,float
,double
等类型都实现该函数,若采用函数重载,那必然要写非常多的类似的代码。且每增一种类型,都要重新实现一次。
为了解决这种需求,可以采用泛型编程。如下例子:
1 |
|
当上面的代码被编译和执行时,它会产生下列结果:
1 | Max(i, j): 39 |
在编译器编译时,发现调用Max模板函数的类型有 int, double , string, 则会自动生成这种重载函数,这叫做模板实例化:
1 | template<> |
如此一来,这些重复的工作由编译器实现,c++程序员们只需关注他们的核心代码即可。
2. 模板特化·
前面对模板作了简单说明,但是现在思考问题,如果说我要对某种类型的模板做专门处理,该如何处理?比如我有个数据结构:
1 | struct Student{ |
现在我想在这个 Student
上做 Max
运算,即比较两个学生的大小,显然之前简单的 a < b ? b : a
是不支持的,一种解决方式是对 Student
重载<
运算符,不过今天要介绍的是另一种方式,模板特化:
1 | template<> |
是不是和前文的编译后的 模板实例化 很类似。是的,只不过是我们人为告诉编译器,对于这种Student
类型,请按照这种方式处理。
实际上,这种方式的全称为 全特化, 还有种特化方式,称为 偏特化。模板参数可以不只一个,全特化代表指定了所有模板参数,而偏特化则是只指定了部分参数。例子如下:
1 |
|
3. 什么是迭代器设计模式·
**迭代器模式(Iterator Pattern)是一种行为设计模式,它允许客户端通过迭代器逐个访问聚合对象的元素,而不需要暴露其底层表示。**下面是一个例子:
1 |
|
可以看到,采用Iterator的方式,无需关注IntVector内部是如何存储数据的(它可以是用数组,可以是链表,二叉树等任意数据结构),只用通过 hasNext()
查看是否还有数据可访问, 通过next()
得到一个value
值。
4. Iterator Tratis·
ok,前面把背景知识都铺垫了,现在开始步入正题。stl的每个容器都是模板容器,每个容器也实现了各自的迭代器。现在问一个问题:算法族函数是如何透过迭代器知道容器内部元素的类型的? 比如:
1 |
|
对于random_one算法来说,它只知道自己接收的是一个iterator类,但是由于不知道Iterator内代表元素类型是什么,我们无法写出注释处的代码(实际上c++ 14/17之后是可以有办法写出的,后文会给出答案,这里专注老版本的写法),那该怎么做?
1. 内嵌类型声明·
为了解决上述问题,需要利用到 内嵌类型声明的trick。听起来挺高级的术语,但给个例子就懂了:
1 | class MyClass { |
实际上不过是利用了 typedef
(新版可改用using
关键字)在类中声明了一个类型罢了。现在把上面的例子换成模板:
1 |
|
现在 Iterator<int>::value_type
可以用来定义 int
类型对象, Iterator<Student>::value_type
可以用来定义Student
类型对象。
有了 内嵌类型声明,算法自然就知道返回值类型了:
1 | template<RandomAccessIterator> |
2. 特化·
有了内嵌类型声明,我们的random_one
算法已经能够处理一些标准容器的迭代器了。但是如果是如下代码:
1 |
|
编译器依然会报错,为什么?因为编译将把RandomAccessIterator
实例化为 int *
, 而对于 int *
这种类型来说,它没有 value_type
的内嵌类型声明。此时该如何处理。
一种容易想到的解决方案是,使用特例化方式,为 int *
类型生成特例化版本的random_one
算法。但是难道我们要为所有类型都生成一个重载吗? 这也是不现实的。
stl是如何处理的? stl的做法是引入了一个中间层,称为 iterator_traits
1 | template <class _Iterator> |
然后将 random_one
改写为:
1 | template<class RandomAccessIterator> |
是不是看起来没啥用,还引入了额外复杂度? 毕竟现在写:
1 | int main() { |
编译器还是会报错。那是因为我只贴出了iterator_traits
的模板定义,还有个特例化版本:
1 | // 针对原生指针(native pointer)而设计的 traits 偏特化版 |
这种特例化匹配所有指针类型,这样当使用 random_one(arr, arr + 5);
时, _Tp
为 int
, 此时value_type
也就为int
。而如果是非指针类型,则按照普通模板处理,此时value_type
也代表该类。至此, random_one
函数既可以接受class
类型的迭代器,也可以接受 pointer
类型的迭代器了。
然而事情还是没有结束,如果现在一个算法为:
1 | template<class Iterator> |
这个函数没有什么功能,只是故意写了个tmp_value
,使它等于迭代器的解引用值,然后返回。考虑如下代码:
1 |
|
此时typename iterator_traits<Iterator>::value_type
被推导为const int
, 而 tmp_value = *iter;
这句话是无法完成编译的,typename iterator_traits<Iterator>::value_type tmp_value;
也必须完成处理化。
那该怎么办?老版本,还是特例化:
1 | // 针对原生之 pointer-to-const 而设计的 traits 偏特化版 |
完整代码如下:
1 |
|
Ok. 支持,一个能够handle各种情况的iterator_traits
完成了。来回忆一遍:
- 为了让算法知道iterator解引用后的类型,引入了 内嵌类型声明
- 为了解决指针类型无内嵌类型声明问题,引入了中间层
iterator_traits
和指针版本特例化。 - 为了解决
const pointer
所带来的问题,引入了const pointer
特例化。
这就是iterator_traits
, 借用侯捷老师的图再次说明其作用。
3.stl iterator traits定义·
前面介绍完了iterator traits
的由来,来看看stl内是怎么定义的吧。
1 | // traits 获取各个迭代器的特性(相应类型)-----类型特性类 |
看起来多了不少内嵌类型声明。分别解释下这几个类型声明:
-
value_type
:迭代器所指对象的类型,原生指针也是一种迭代器,对于原生指针int*
,int
即为指针所指对象的类型,也就是所谓的value_type
。 -
difference_type
: 用来表示两个迭代器之间的距离,对于原生指针,STL 以 C++ 内建的ptrdiff_t
作为原生指针的 difference_type。 -
reference
: 是指迭代器所指对象的类型的引用,reference
一般用在迭代器的 * 运算符重载上,如果value_type
是T
,那么对应的reference
就是T&
;如果value_type
是const T
,那么对应的reference_type 就是const T&
。 -
pointer
: 就是相应的指针类型。 -
iterator_category
: 的作用是标识迭代器的移动特性和可以对迭代器执行的操作,从iterator_category
上,可将迭代器分为Input Iterator、Output Iterator、Forward Iterator、Bidirectional Iterator、Random Access Iterator
五类,为什么需要定义这么多种类的迭代器?主要是为了算法可以针对特定的迭代器使用特定的算法,从而提高算法效率。在stl中,分别定义为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54// iterator_category 五种迭代器类型
// 标记
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
// The base classes input_iterator, output_iterator, forward_iterator,
// bidirectional_iterator, and random_access_iterator are not part of
// the C++ standard. (They have been replaced by struct iterator.)
// They are included for backward compatibility with the HP STL.
template <class _Tp, class _Distance> struct input_iterator {
typedef input_iterator_tag iterator_category;
typedef _Tp value_type;
typedef _Distance difference_type;
typedef _Tp* pointer;
typedef _Tp& reference;
};
struct output_iterator {
typedef output_iterator_tag iterator_category;
typedef void value_type;
typedef void difference_type;
typedef void pointer;
typedef void reference;
};
template <class _Tp, class _Distance> struct forward_iterator {
typedef forward_iterator_tag iterator_category;
typedef _Tp value_type;
typedef _Distance difference_type;
typedef _Tp* pointer;
typedef _Tp& reference;
};
template <class _Tp, class _Distance> struct bidirectional_iterator {
typedef bidirectional_iterator_tag iterator_category;
typedef _Tp value_type;
typedef _Distance difference_type;
typedef _Tp* pointer;
typedef _Tp& reference;
};
template <class _Tp, class _Distance> struct random_access_iterator {
typedef random_access_iterator_tag iterator_category;
typedef _Tp value_type;
typedef _Distance difference_type;
typedef _Tp* pointer;
typedef _Tp& reference;
};简单说明这几个 iterator 的用途与区别:
Input Iterator
: 此迭代器不允许修改所指的对象,是只读的。支持 ==、!=、++、*、-> 等操作。Output Iterator
:允许算法在这种迭代器所形成的区间上进行只写操作。支持 ++、* 等操作。Forward Iterator
:允许算法在这种迭代器所形成的区间上进行读写操作,但只能单向移动,每次只能移动一步。支持 Input Iterator 和 Output Iterator 的所有操作。Bidirectional Iterator
:允许算法在这种迭代器所形成的区间上进行读写操作,可双向移动,每次只能移动一步。支持 Forward Iterator 的所有操作,并另外支持 – 操作。Random Access Iterator
:包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种 Iterator 的所有操作,并另外支持 [n] 操作符等操作。
一张图总结:
5. iterator·
说了这么多的iterator traits, 最终都是为了iterator
服务,stl的iterator
定义为:
1 | // 用户自己的写的迭代器最好继承此 std::iterator |
这是迭代器的基类, 可以以看到这里定义了iterator_traits
中要使用的各种类型,符合前文的分析。同时注意,Distance = ptrdiff_t, class _Pointer = _Tp*, class _Reference = _Tp&
这三个模板是有默认值的。所以之后使用时,可以只传入 _Category
与_Tp
即可。
6. 总结·
本文介绍了什么是模板,模板特例化,同时从算法从如何透过迭代器知道迭代器解引用后的元素类型 问题出发,一步步引出了 iterator traits
的实现。
补充·
前文提到了如下模板在c++ 14/17后可以直接书写出来:
1 | template<class RandomAccessIterator> |
答案是:
1 |
|