迭代器iterators及traits详解-stl源码剖析学习笔记

迭代器

迭代器是一个抽象的设计概念,现实程序语言中并没有直接对应这个概念的实物。《Design Patterns》提供的23个设计模式中,迭代器模式定义为:提供一种方法,使之能够依序寻访某个聚合物(容器)所含的各个元素,而又无需暴露该聚合物的内部表达方式。

不论是泛型思维还是STL的实际应用,迭代器都扮演着重要角色。STL的中心思想在于,将数据容器(containers)和算法(algorithms)分开,彼此独立设计,最终再以胶着剂方式将它们撮合在一起。

以算法find()为例,他接受两个迭代器和一个搜寻目标: 在这里插入图片描述
List和vector容器的find应用:

#include<vector> //vector容器
#include<list> //list容器
#include<iostream> //输入输出流
using namespace std;

void main()
{
	const int arraySize = 6;
	int ia[arraySize] = {0,1,2,3,4,5};

	vector<int> ivec(ia, ia+arraySize);
	list<int> ilist(ia, ia+arraySize);

	vector<int>::iterator vit = find(ivec.begin(), ivec.end(), 4);
	if (vit == ivec.end()) cout << "vector 4 not find." << endl;
	else cout << "vector 4 find." << endl;

	list<int>::iterator lit = find(ilist.begin(), ilist.end(), 4);
	if (lit == ilist.end()) cout << "list 4 not find." << endl;
	else cout << "list 4 find." << endl;
}

这个例子中迭代器似乎依赖容器之下,有没有独立而泛用的迭代器呢?我们又该怎样设计一个自己的迭代器呢?

迭代器是一种smart pointer

迭代器是一种行为类似指针的对象,指针中最常用也最重要的行为是内容提领(dereference)和成员访问(memeber access),因此迭代器的重要工作是对operator * 和operator - > 的重载。对于这一点,我们可以参考C++标准程序库的auto_ptr,这是一个包装原生指针的对象,其用法如下:
{auto_ptr ps(new string(“123”));
cout << * ps << endl; //输出123
cout<<ps -> size()<<endl //输出3
// 使用结束不需要delete,auto_ptr会自动释放
}

这里new动态配置了一个初值为123的临时string对象,并将该结果(一个原生指针)作为auto_ptr对象ps的初值。

auto_ptr内部实现了operator*,operator->以及operator=的重载,其代码如下:
在这里插入图片描述
我们可以根据这个例子写一个List,并为它设计一个类似指针的外衣,也就是迭代器,我们提领(dereference)这个迭代器时,传回一个ListItem对象,递增迭代器时,指向下一个ListItem对象。并且为了让该迭代器适用于任何类型的节点,而不局限于ListItem,我们可以写一个类模板。具体示例如下:

List及其节点:
在这里插入图片描述
迭代器:
在这里插入z图片描述
以上代码可以看到,针对List的迭代器暴露了List的很多实现细节,main函数中的begin,end迭代器暴露了ListItem,ListIter class的operator++中,暴露了ListItem内部的next()。如果不是为了迭代器,ListItem应该完全隐藏不曝光的,或者说,要实现List迭代器,必须对List的内部实现非常了解。既然这无可避免,就把迭代器的开发交给List的设计者好了,如此一来,所有细节实现都可以被封装而不被使用者看到。这也就是为什么每个STL容器都有其专属迭代器的缘故。

相应型别value_type及traits技术

我们在运用迭代器时,如果要用到其相应型别该怎么办呢,什么是相应型别呢?其中迭代器所指之物的型别就是其中一种。我们该怎么得出迭代器所指之物的型别呢?比如用其声明一个变量。

解决这个问题,可以利用函数模板的参数推导(argument deducation)机制。我们来看其具体操作:
在这里插入图片描述
这里func_impl是一个函数模板,一旦被调用,编译器会自动进行template推导,从而导出参数T。

这里迭代器所指之物的型别,又称value_type,上述的推导方式为参数推导,如果我们遇到value_type做函数返回值的情况呢?我们可以用声明内嵌型别的方式。看如下例子:
在这里插入图片描述
这里func的返回型别必须加上typename,因为T是一个模板参数,在编译器具现化之前,编译器对它一无所知,也就是说编译器并不知道MyIter::value_type是一个型别,或是一个成员函数,或者是数据成员。加上关键字typename,告诉编译器这是一个型别,可以顺利通过编译。

那么如果迭代器不是class type该怎么办呢?比如如果是一个原生指针,就无法声明内嵌型别,但是STL以及所有的泛型思维都必须可以接受原生指针作为迭代器,这里可以用偏特化模板实现。

偏特化(Partial Specialization)的意义

偏特化模板的意义是,如果一个类模板拥有一个以上template参数,我们可以针对某个或多个(但非全部)参数进行特化工作,也就是我们可以在泛化设计中提供一个特化版本,并且该特化版本本身仍为templatized。《泛型思维》中对偏特化的定义为:“针对任何template参数更进一步的条件限制所设计出来的一个特化版本”。比如:
template< typename T>
class C {}; //这个泛化版本允许T为任何型别
我们就很容易接受它有一个偏特化版本如下:
template< typename T>
class C<T *> {}; //这个特化版本仅适用于T为原生指针的情况
//T为原生指针就是T为任何型别的更进一步的条件限制。

那么我们就可以解决刚刚的内嵌型别的问题了,我们可以针对迭代器之template参数为指针设计一个特化版本的迭代器。

下面这个class template用来萃取迭代器的这一特性:
在这里插入图片描述
针对原生指针的特化版本:
在这里插入图片描述
如果是针对指向常数对象的指针呢,比如const int*,我们萃取到的型别如果是const int,不能进行赋值,并没有什么用,所以我们萃取其value_type的时候,希望是一个non const型别。该特化版本如下:
在这里插入图片描述
以上的traits扮演一个萃取机的角色,萃取各个迭代器的所指型别特性,要想萃取机有效运行,各个迭代器必须以内嵌型别方式定义出相应型别。

上面我们提到了迭代器所指之物的型别,实际上迭代器的相应型别有五种,分别是:value type, difference type, pointer, reference, iterator category。要想让萃取机正常运行,迭代器必须定义这五种相应型别。
在这里插入图片描述
下面我们对其余四种进行说明。

相应型别difference type

用来表示两个迭代器之间的距离,可以表示容器的最大容量。如果一个泛型算法提供计数器功能,那么它的返回值就必须是一个difference type。
在这里插入图片描述
以下是针对原生指针以及const原生指针的特化版本,以C++内建的ptrdiff_t为difference type:
在这里插入图片描述

相应型别reference type

根据所指之物是否允许改变,迭代器分为两种,可改变,如intp,和不可改变,如const int p,当我们对一个可改变的指针进行提领操作时,获得的是一个左值,因为右值不允许赋值操作。C++中,函数要传回一个左值,必须是引用传递(by reference)。所以如果p是一个可改变指针,其value type为T,那么p不是T,而是T&,那么如果p是一个不可改变指针,其value type为T,那么p为const T&。这里的*p型别即为reference type。

相应型别pointer type

pointer type与reference type密切相关,即传回一个左值,令它代表所指之物的地址。

这两个型别上文出现过:
在这里插入图片描述
两个偏特化版本:
在这里插入图片描述

相应型别iterator_category

根据移动特性和施行特性,迭代器分为五类:
input iterator,这种迭代器所指对象,不允许改变,只读。
output iterator,唯写。
forward iterator,允许读写。
bidirectional iterator,可双向移动,比如逆向操作。
random access iterator,涵盖所有指针算术能力,包括p+n,p-n,p[n],p1-p2,p1<p2。

其从属关系如下:
在这里插入图片描述
根据上述从属关系,我们设计某种迭代器时提供一个明确定义,设计另一种强化迭代器时,使用另一种定义,这样才能在不同情况提供最大效率。
比如对于一个函数内部将p累进n次的例子,三种类型的迭代器效率不同。
在这里插入图片描述
但是当程序调用时,我们应该选择哪儿个函数呢,可以加一个迭代器型别判断函数,不同的类型调用不同的函数。但是如此一来,需要在程序执行时才能决定使用哪儿个判断缺乏效率,我们可不可以在编译阶段就选择正确的版本呢?答案是可以的,我们可以利用重载来完成。

上面三个advance_xx都有两个参数,型别都未定(template参数),我们需要加一个已定的型别参数构成重载。这样我们就需要traits萃取出迭代器的种类作为advance的第三个参数。而且我们需要这个型别为class type,而不是一个数值号码类的东西,因为编译器需要仰赖它进行重载决议。

下面五个classes,代表五种迭代器类型:
在这里插入图片描述
这些classes只作为标记用,不需要任何成员。这样在刚刚的函数加上对应的参数即可构成重载,这里不一一列举。这样我们就可以写出上层控制接口,接口中可调用各重载函数。这个上层函数也必须可以推导出迭代器的类型,当然这个工作是交给traits来做。
在这里插入图片描述
这样的话traits就需要一个相应的型别iterator_category。其版本类同其余型别。
任何一个迭代器,其类型永远落在该迭代器所隶属类型中,最强化的那个。比如int*,即是random access iterator,又是bidirectional iterator,同时也是forward iterator,和input iterator,那么其类型归属于random access iterator。
你是否又注意到,上述advance中的迭代器命令类型为InputIterator,既然advance可以接收各类型参数,就不应该命名为InputIterator,这实际是STL算法的一个命名规则,以算法所能接收的最低阶迭代器类型,来为其迭代器型别参数命名。

以class来定义迭代器类型标签,一个好处是促成重载机制的决议,另一个好处是通过继承消除“单纯传递调用的函数”,如上述重载的forward版本:
在这里插入图片描述
从下面示例中可以看出如何消除:
在这里插入图片描述

规范

为了符合规范,任何迭代器都应该内嵌五个相应型别,以利于traits萃取。为了防止遗漏,STL提供了一个iterators class,每个新设计的迭代器都继承该class,就能保证符合规范。
在这里插入图片描述
iterator class不含任何成员,纯粹的型别定义,故继承该class不会有任何负担,且后三个参数有默认值,新迭代器只需要提供前两个即可。
总之,设计适当的相应型别,是迭代器的责任,设计适当的迭代器是容器的责任,只有容器本身,才知道该设计怎样的迭代器遍历自己,并执行迭代器的各种操作(前进,后退,取值…)。至于算法,独立于迭代器和容器,只要设计时以迭代器为对外接口就行。

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

__type_traits

在上一篇博客空间配置器的学习中,里边有大量__type_traits的应用,该应用就是traits扩展到迭代器以外的应用。__type_traits为萃取型别的特性,比如是否是无关紧要的默认构造函数,是否是无关紧要的拷贝构造函数,以及是否是无关紧要的赋值重载等等。如果答案是无关紧要的,那么在进行构造,拷贝,赋值等操作时,就可以采用最有效的措施,即不调用这些构造、拷贝、赋值函数,而直接进行malloc(),memcpy()等操作,这对于大规模操作频繁的容器,能显著的提高效率。

SGI定义有以下五种__type_traits。
在这里插入图片描述
同时SGI提供了针对C++标量型别的所有特化版本这里不再赘述。

对于SGI STL的用户,部分编译器可以针对自定义的class萃取出合适的_type_traits,而大部分萃取出的只有默认_false_type类型。那么我们什么时候需要自定义自己的_type_traits呢?如果一个class内含指针成员,并对它进行动态内存配置,那么这个class就需要实现出自己的non-trivial_xxx。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值