STL通过抽象了基于数据结构之上的普遍行为,形成了独特的STL算法体系。在STL中,这些数据结构称为容器,在容器和算法之间多了一个关键的中间体:迭代器。迭代器是容器和算法之间的纽带,它降低了数据结构和算法之间的耦合,并提供了容器和算法的独立性、弹性、互操作性。本文通过分析SGI STL的部分源码,简单地阐述STL的基本理念。
关键词 STL,C++,理解,SGI,GP,泛型,迭代器,容器,算法,templates,模板
1. 概述
2. STL和标准
3. STL体系结构
4. 空间配置器
5. 迭代器
6. 容器
6.1. 序列容器
6.2. 关联容器
7. 算法
8. 适配器
9. 函数对象
参考文献
候捷老师在对STL的应用和本质作了深入的理解之后,提出了学习STL的三种境界:
n 第0种境界,深入了解C++ Template
n 第1种境界,运用STL
n 第2种境界,了解泛型技术的内涵和STL的学理,不但要理解STL的概念分类学和抽象概念库,而且需要对STL组建作一番深刻的理解
n 第3种境界,扩充STL
且不论候捷老师提出这些东西是否适合我们,但是他提出的这几点已经足够反应出来了对某方面知识的由浅入深的一个普遍的过程。对于第0和第1种境界,我们不多说。我们假设你已经对STL足够熟悉。
STL在标准化的前后出现多了多个版本,其中包括:
n HP版本。STL的鼻祖,由于STL之父和Meng Lee在HP实验室编写。
n SGI(Silicon graphics Computer System Inc.)。这个是C++鬼才Stepnov为开源世界提供的一个的性能卓著的STL的版本。
n VC++(PJ P.J.Plauger)。这是P.J.Plauger的个人作品,目前被用于VC++编译器。
n C++ Builder(RW,Rogue Wave Software Inc.)。这个版本目前被用于C++ Builder编译器。
STL是数据结构和算法在C++世界中的标准,最早出现在C++ 98标准中,即将出来的C++ 09标准也对STL作了很大的改进,比如加入了正则表达式库等。标准的作用在于规范实现,提供软件的复用性。标准的实现由各大编译器完成,而因为各种各样的原因,很多时候编译器对于标准的支持并不是很好。
STL通过抽象了基于数据结构之上的普遍行为,形成了独特的STL算法体系。在STL中,这些数据结构称为容器,在容器和算法之间多了一个关键的中间体:迭代器。迭代器是容器和算法之间的纽带,它降低了数据结构和算法之间的耦合,并提供了容器和算法的独立性、弹性、互操作性。
因此,STL的价值在于:首先它提供了一套抽象概念库,如iterator、traits;其次STL通过组织这些抽象概念库构建了一个有机的、独立的系统,极大地提高了软件的复用性。
STL的优点在于提出并建立了一个抽象概念库,而在这个抽象概念库中,处于核心的地位的六大部件,它们分别是:
1. 空间配置器(Allocator)
2. 迭代器(Iterator)
3. 容器(Containter)
4. 算法(Algorithm)
5. 适配器(Adaptor)
6. 函数对象/仿函数(Functor)
它们之间的关系如下:
图一 STL六大部件关系图
图解:Container通过Allocator来管理空间(主要是内存),Algorithm通过Iterator来存取Container中的数据,Functor扩展Algorithm,Adaptor来包装Container、Iterator、Functor
从上图我们不难看出容器和算法处于STL的核心位置,迭代器通过一种抽象的方式实现了算法和容器之间的关联,不但很好地融合了孤立的容器和算法,而且通过添加一种间接层的方式降低了容器和算法之间的耦合。
为了抽象容器中的元素的空间分配模型,于是就有了空间配置器,它负责为容器中的元素或辅助结构提供空间分配上的帮助。
和空间分配器处于平等地位的就是函数对象。为了抽象算法中元素之间的关系模型,如比较大小等,于是也就有了函数对象,它负责确立算法所作用的元素之间的关系。
最后一个就是适配器。适配器在STL中可以说是可有可无的。它“可有“是因为它通过某种方式实现了一种的新的容器、算法和迭代器;它”可无“是因为它提供的东西可以通过操作底层的容器、算法和迭代器就能实现。那么它的意义在于何处呢?它的意义在于它提供了基于基础容器的更高层次的抽象,也就是说它在另外的一个高度实现了软件的抽象和复用。
这里面提到的空间可以内存也可以是硬盘,或其它。空间配置器在各个STL版本的实现中基本都是对C++的new和delete的浅层封装,并没有体现出它的真实意图。
它的目的在于:提高性能和提高程序的适应性。
性能主要体现在两个方面:
1. 支持并发
在一般的STL实现中都添加了对并发的支持,但同时也提供了宏开关来关闭对并发的支持。
2. 对于小内存对象的处理
在一般的编译器中,对于C++的new和delete基本是直接向系统的Heap要求和释放空间。这对于大的内存块没有问题,但是如果通过这种方面来操作小块内存,如8byte、16Byte内存等,不但会导致内存碎片,而且会间接地导致程序性能方面的降低。因此某些STL版本的实现上就对此作了特殊的处理。
如在SGI STL中,空间配置器将的空间的分配策略分为两个级别:
n 一级空间配置器
处理大块的内存(>= 128bytes)的分配,对malloc,free,realloc的简单封装。
n 二级空间配置器
处理小块的内存(< 128bytes)的分配,避免太多小额区块造成的内存碎片, 设置16级free list:8,16,24…
STL的中心思想在于将容器和算法分开,彼此独立设计。迭代器实质上是一种smart pointer,它重载operator->,operator*。可以利用iterator_traits来萃取iterator的value type、reference type、pointer type、difference type、iterator category。
因为不同的算法对于不同的迭代器的实现对于性能的影响比较大,将迭代器进行分类是非常有必要的。迭代器的关系分类如下图:
图二 迭代器分类关系
那么为什么要这样地对迭代器进行分类,即这几类迭代器的意义在何处:
n Input Iterator
某些算法,如std::for_each只需要只读访问容器的内容,那么提供这种只读的迭代器,不但给算法本身提供了概念上的约束,同时可以提高程序的可读性。
n Output Iterator
某些算法,如std::transform是需要改变容器的内容的。因此需要提供这么一种迭代器来充当改变容器内容的代理。
n Forward Iterator
有些算法只需要前向地访问容器。
n Bidirectional Iterator
有些算法需要前向和后向地访问容器。
n Random Access Iterator
有些算法需要随机地访问容器。
容器可分为序列容器和关联容器。两者之间的关键区别在于元素的组织形式,序列容器中的元素是按照某种序列放到内存中的,而关联式容器需要一个key来找到容器中的实际元素。
vector的iterator的类别为radom-access-iterator。因此一般的算法都可作用于vector。在vector中插入或删除元素其时间复杂度为O(n)。因为在vector插入或删除元素需要移动插入或删除位置后面元素,因此有可能造成原有的iterator失效。
vector区别array的是,它的空间是可以动态增长的,而增长的规则何如呢?当空间不够时,vector的容量,即capacity为原来的capacity的2倍以上。
因此vector的实现技术关键在于对大小的控制以及重新配置时的数据移动效率(空间复杂度+时间复杂度)。
list的iterator的类别为bidrectional iterator。在list中插入或删除元素为O(1),无capacity。因为在vector插入或删除元素不需要移动插入或删除元素,因此插入/删除操作不会造成原有的list iterator失效。
deque的iterator的类别为radom-access-iterator。在deque开头和末尾插入或删除元素为O(1)。因为在deque插入或删除元素需要移动插入或删除位置后面元素,因此有可能造成原有的iterator失效。
关联容器本身是一颗平衡二叉树,平衡二叉树的实现形式有多种,如AVL-Tree、RB-Tree等。SGI STL版本中使用的是被广泛采用的RB-Tree。它是set和map的底层数据结构。
set的iterator的类别为bidrectional iterator。因为在set插入或删除元素需要移动插入或删除位置后面元素,因此有可能造成原有的iterator失效。
map的iterator的类别为bidrectional iterator。因为在map插入或删除元素需要移动插入或删除位置后面元素,因此有可能造成原有的iterator失效。
multimap和map基本相同,所不同的是,一个multimap中的key可以重复。multimap中的迭代器的种类为双向存取迭代器(bidirectional-access-iterator)。
multiset和set基本相同,所不同的是,一个multiset中的元素是可以重复的。multiset中的迭代器的种类为双向存取迭代器(bidirectional-access-iterator)。
C++ 98的STL算法是STL提案中的算法的精选部分,大多是一些实用性很强的算法。算法抽象了对数据结构的操作行为,可以说是独立于容器的另外一个世界。关于STL算法实现可以参考SGI STL。
适配器(Adaptor)是为了增强功能或/和添加约束,对容器、迭代器、算法重新进行了包装的模板类。设计模式中的Decorate模式的基本思想是实现适配器(Adaptor)的理论/实践基础。在STL中,适配器可分为三类:容器适配器、迭代器适配器以及算法适配器。
函数对象适配器是对某类特定函数对象进行Decorate而形成的新的模板类。
1. STL源码剖析/候捷;武汉:华中科技大学出版社,2002.6
2. Effective STL /Scott Douglas Meyers著
3. The Standard Template Library Tutorials, Johannes Weidl,1994.4
4. C++ Primer第三版/(美)Stanley B.Lippman,Josee Lajoie著;潘爱民,张丽译;北京:中国电力出版社
5. 泛型编程与STL/(美)奥斯腾著;候捷译. -北京:中国电力出版社,2003