转自:http://blog.sina.com.cn/s/blog_93b45b0f01014o9d.html
迭代器类别问题
iterator头文件根下面:
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 {};
这是五种迭代器类别的标志,可以看出,这些结构只是标志,没有任何内容。从语义上讲,它基本可以使用一个长变量来表示。但是种表达方式有它的好处,在定义普通迭代器的时候,我们只需要在迭代器内部typedef其中一个类型,那么将来就可以通过类型萃取器,来得到这个迭代器的类型信息(进而可以做相应的优化)。
为什么不使用常变量呢?为什么不让迭代器继承这些标志来表达类别呢?
为什么不使用常变量
我们就在迭代器定义使用一个static const的常变量,这样很容易标识一个迭代器的所属类别。这种模式我认为有两个弊端;
首先这捣乱了我们所说通过萃取器(trait)来获得迭代器的相关类型信息,如果使用static const变量来确定,那将会是一个脱离于类型萃取器之外的独立方式:
If(T::category==2)//说明是前进型迭代器。
为什么要大费奏章的使用一个int变量来表达类型的信息呢?为什么不使用类型来表达类型信息呢?这就是,为什么在iterator定义这些“空类型”,他们虽然是空的,但是它还是表达出一种类别。
其次,使用这种方式,可以很好的表达这些类别之间的包容关系。
通过继承,可以表达,forward也属于input,bidirectional也属于forward。而使用static const(或者说C语言中常中的#define方法),这种表示都不方便。
为什么不让迭代器继承这些标志来表达类别呢?
我想说的是,首先,根本没有什么东西可以继承的;其次,继承之后,也只是给迭代器加入了父亲信息。那么算法如何提取这个信息呢?这其实是一个很困难的问题,我认为,C++没有这样的源语机制,但是我们似乎可以做到:
Class A
{
};
Class B: public A
{
};
Fun(B, b)
{
}
上面就完成这我们需要的过程。也就是说,如果想通过继承的方法来给类设置类别,就需要这么干!而既然你都这么干了,那么还不如直接使用前面我们所设计的方法。并且,我们有更重要的东西需要让迭代器继承(trait规定的标准),而不是继承这些空壳子。
一个结论
这里我们得到一个结论:在C++中继承的主要目的是用来更好、更快的构建一个新类,应该说他与composition是有着一样的目的。(所以我们才会将inheritance和composition比较起来,如果不具有类似性是不会进行比较的)。那么子类是否保留了它的父类信息呢?(这样说不准确,而是说,从使用子类的程序员角度来说,他是否能知道该子类属于哪个父类呢?)。可以说,有一点点,例如我们可以将子类的引用(指针或引用)付给父类引用这就是一定程度的表达了这种父子关系。我们这里认为如果你不在定义父子类的做特殊设定(构造函数),将父类对象直接转换成子类对象,以及反向转换都是错误的(需要实验验证)。于是,我们有一个这样的结论:
从子类的角度上看,你是分不清楚这个类是怎么行成的(raw,inheritance,composition)。
五种迭代器类别
首先,引进迭代器是为了在算法与容器之间隔开,可以让两者独立发展。这样势必要求迭代器需要像算法展现统一的接口,也就是说,从算法的角度出发,迭代器的功能接口是一样的,算法无法直接看到容器的,而是通过迭代器简介管理操作容器,这样可以推到出:算法眼里容器都是一样的。而显然这并不合理。
因为,即使各种容器有统一的基本特性——“聚集”同一类型的数据元素。但是由于不同的容器聚集的方式不同,导致表现出来的许多功能特性不同。(例如vector,deque表现可以很好的支持random access性质,但是你叫list也支持这个,这将是非常不明智的)。所以,即使我们尽量希望向外部展现一个统一的容器接口(迭代器),我们仍然需要区分对待,我们在迭代器上区分对待,就可以充分发挥各种容器的个性特点。算法也可以根据相应的不同,优化对待不同的容器。这就是出现不同的类型容器的原因。
Iterator Category | Ability | Providers |
Input iterator | Reads forward | istream |
Output iterator | Writes forward | ostream, inserter |
Forward iterator | Reads and writes forward | |
Bidirectional iterator | Reads and writes forward and backward | list, set, multiset, map, multimap |
Random access iterator | Reads and writes with random access | vector, deque string, array |
可以看到,不同类别的迭代器,所能支持的功能是不同的,定义容器的选择定义哪个类别的应不同的容器特性而定。在设计算法的时候,算法会通过类型萃取获得该迭代器的类别。并根据不同的类别视机做特定的算法实现优化。
Input iterator
Operations of Input Iterators | |
Expression | Effect |
*iter | Provides read access to the actual element |
iter ->member | Provides read access to a member (if any) of the actual element |
++iter | Steps forward (returns new position) |
iter++ | Steps forward (returns old position) |
Iter1 == iter2 | Returns whether two iterators are equal |
Iter1 != iter2 | Returns whether two iterators are not equal |
TYPE(iter) | Copies iterator (copy constructor) |
这个迭代器用于,从容器中依次读取数据(只读,并且只能前进)。但是还有一个会让你匪夷所思的特性是,这种迭代器不能重复读某个元素两次。
这是一个很奇怪但是有很合理的规定,最常见的需要用输入迭代器的就是标准输入口,不管有几个迭代器指向这个标准输入口,语义上不容许同一个元素被读两次。
但是,我们奇怪的是,这个意思的就是说,读动作是与迭代器相前进一步的动作是一起发生的,那么这种迭代器就不需要向前进一步的功能的必要了,但这里提供了。
事实上我认为,上面的要求只是一种语义上的要求。告诉你,如果你在某个容器定义迭代器的时候选择了输入迭代器,那么这个迭代器所应该提供的功能模式应该需要保持“不容许同一个元素被读两次”,我们知道STL对各种类别的区别只是用tags来区别的。语法上没有写死语义上的“建议”。
还有一个原因是,下面有几种迭代器,是以这种迭代器为基础的,所以如果你不提供这种基本的操作(向前去!),那他们如何提供。
Output iterator
Operations of Output Iterators | |
Expression | Effect |
*iter = value | Writes value to where the iterator refers |
++iter | Steps forward (returns new position) |
iter++ | Steps forward (returns old position) |
TYPE (iter) | Copies iterator (copy constructor) |
与input iterator 想对应的就是output iterator,他们有很多相似点,并且,也有与前面类似的奇怪规定,这导致两个指向同一个容器的写迭代器,写的过程不会出现覆盖写。其中一个有名的例子就是屏幕输出。
并且,你会发现写迭代器没有比较操作(读模式中就有)。这是因为写的时候,是一直往外写的,语义上以没有终点限制的。有一个特殊的(也是我们将要介绍的)迭代器,就是inserter。
Forward iterator
Operations of Forward Iterators | |
Expression | Effect |
*iter | Provides access to the actual element |
iter-> member | Provides access to a member of the actual element |
++iter | Steps forward (returns new position) |
iter++ | Steps forward (returns old position) |
iter1 == iter2 | Returns whether two iterators are equal |
iter1 != iter2 | Returns whether two iterators are not equal |
TYPE() | Creates iterator (default constructor) |
TYPE(iter) | Copies iterator (copy constructor) |
iter1 = iter2 | Assigns an iterator |
从iterator中表达的迭代器tags定义可以看出:
struct forward_iterator_tag : public input_iterator_tag {};
forward iterator 是以input iterator为基础的。这就是给我们一个奇怪的错觉。语义上forward只限制了向前,不限制读写,这样forward iterator应该继承input iterator和output iterator两种。但是它只支持一种。
这里我们提出两个原因:
首先,iterator中迭代器tags的规定只是语义上的规定,它对最终的具体实现没有什么限制,(当然你不按照这种方式实现,那将是一个不道德的行为)。所以这种定义,从本质实现上并不能影响我们许多。
其次,权威的书籍中所阐述的原因是,output中,由于语义上不提供比较功能(这就表明不判断是否越界)。所以forward不能全部继承output的语义功能。
最后,我说一句,这里的规定始终都是语义上的规定,所以基本上,forward iterator继承谁,都是一句屁话。这是标准,但是对于实际实现的程序员来说,这些东西都是屁。
Bidirectional iterator
Additional Operations of Bidirectional Iterators | |
Expression | Effect |
-- iter | Steps backward (returns new position) |
iter-- | Steps backward (returns old position) |
可以看出,双向迭代器只是在forward iterator上面加上一个向后一步的功能。我们事实上我们知道我们通常见到的迭代器都这种,以及后面一种Random iterator.
Random iterator
Additional Operations of Random Access Iterators | |
Expression | Effect |
iter[n] | Provides access to the element that has index n |
iter+=n | Steps n elements forward (or backward, if n is negative) |
iter-=n | Steps n elements backward (or forward, if n is negative) |
iter+n | Returns the iterator of the nth next element |
n+iter | Returns the iterator of the nth next element |
iter-n | Returns the iterator of the nth previous element |
iter1-iter2 | Returns the distance between iter1 and iter2 |
iter1<iter2 | Returns whether iter1 is before iter2 |
iter1>iter2 | Returns whether iter1 is after iter2 |
iter1<=iter2 | Returns whether iter1 is not after iter2 |
iter1>=iter2 | Returns whether iter1 is not before iter2 |
可以看出,Random iterator的功能主要体现在,在forward iterator 的基础上提供了随机存储的功能,这个功能连带提供了迭代器算数功能(指针算数功能)以及比较功能。
我们常见的这种类型的迭代器由下面容器提供:
·
·
·
类型萃取器问题
前面提到,类型萃取器用于给算法使用,同过它你可以得到有关你所操纵的迭代器的几乎所有有用的相关类型。
算法可以使用如下语句得到相关类型:
typename std::iterator_traits<T>::value_type
有些书上说,这种设置有两种好处:
首先,它规定了每种迭代器在定义的时候都需要提供者几种类型信息以供被使用,从某种角度上讲,它提供一种定义迭代器有关相关属性的标准。
为了强行对这一标准进行规定,在iterator头文件中有一个结构(struct iterator)用于给用户定义迭代器的时候继承的(就是接口),但是这并非是语法规定。事实上,如果你如果能够保证一定会提供traits结构中所需求的那些类型信息,不继承这个也是可以的:
template <class Category, class T, class Distance = ptrdiff_t,
struct iterator {
};
事实上,对于我们系统自定义的那五种迭代器,我们使用的比较多的仍然是tags。但是系统为了用户子定义迭代器方便,仍然按照各自的特点,给他们各自定义了类似于iterator的、用于管理traits所指明的类型信息。
例如,定义特定类别迭代器的时候继承特定结构体,你就可以不同管trait规定的那些东西了。
template <class T, class Distance> struct input_iterator {
};
struct output_iterator {
};
template <class T, class Distance> struct forward_iterator {
};
template <class T, class Distance> struct bidirectional_iterator {
};
template <class T, class Distance> struct random_access_iterator {
};
其次,它让普通指针也被列为迭代器的一种。
如上面所说,通过模板偏特化技术,使得当算法获得了普通指针作为迭代器的时候,需要同样知道那些类型(事实上,这种情况下获得还是比较简单的,但是需要和普通迭代器进行统一,以实现泛型编程)。