不同于OOP(Object Oriented Programming)将数据(datas)和行为(methods)组织在一起的思想,STL的中心思想GP(Generic Programming)在于:将数据容器(containers)和算法(algorithms)分开,彼此独立设计,最后再以胶着剂将它们撮合在一起。那么之间的胶着剂就是迭代器(iterator)。
GP的思路具体为:containers和algorithms团队可以各自闭门造车,其间以iterator进行沟通。algorithms通过iterators确定操作范围,并通过iterators提领取用containers中的数据。那么如何设计好迭代器是一个难题。
智能指针:在c++中,动态内存的管理是通过new
和delete
来完成的,实际上new和delete运算符最终调用的是malloc()
和free()
两个函数。对于动态内存的管理很容易出问题,想要确保在正确时间释放内存考验功力。若是忘记释放会导致内存泄漏,若是提早释放,即尚有指针引用的情况下就释放内存,会产生引用非法内存的指针。针对这一问题,应运而生出了智能指针的概念,即其行为类似常规指针,但是它能够负责自动释放所指向的对象,形式通常为一个struct或class的自定义数据类型所实例化出的对象。新标准库提供了三种智能指针,分别是shared_ptr
(允许多个指针指向同一对象)和unique_ptr
(独占所指向的对象),还有weak_ptr
(伴随类,弱引用,指向shared_ptr所管理的对象)。
迭代器是一种行为类似指针的对象(智能指针对象),其行为包括内容提领(deference)和成员访问(member access),所以迭代器要对operator&()
和operator*()
进行重载,同时要兼顾自身的有效性(析构)的问题。
以上概念明确之后,可以尝试为单向链表设计一迭代器。
//定义single linked list的基本数据结构
template <typename T>
class List {
public:
void insert_front(T value);
void insert_end(T value);
void display(std::ostream &os = std::out) const;
//...
private:
ListItem<T>* _end();
ListItem<T>* _front();
long _size;
};
template <typename T>
class ListItem {
public:
T value() const { return _value; }
ListItem<T>* next const { return _next; }
//...
private:
T _value;
ListItem<T>* _next;
};
//------------------------------------------------------------------------------------
//List的iterator实现
//主要功能为:
//1.deference迭代器时返回ListItem对象;2.递增迭代器时,返回_next节点
//为使迭代器适应多种内嵌数据类型的List,将其设计为class template
template <class Item> //Item可以是单向链表节点或是双向链表节点
struct ListIter { //此处的迭代器只为链表服务,因为其operator++的功能
Item* ptr; //保持与容器的一个联系(keep a reference to Container)
ListIter(Item* p = 0) : ptr(p) {} //default ctor
//不必实现copy ctor,因为编译器提供的缺省行为已足够
//不必实现operator=,因为编译器提供的缺省行为已足够
Item& operator*() const { return *ptr; }
Item* operator->() const { return ptr; }
//以下两个operator++遵循标准做法
//(1)pre-increment operator
ListIter& operator++() { ptr = ptr->next(); return *this; }
//(2)post-increment operator
ListIter operator++(int) { ItemIter tmp = *this; ++*this; return tmp; }
bool operator==(const ListIter& i) { return ptr == i.ptr; }
bool operator!=(const ListIter& i) { return ptr != i.ptr; }
//...
};
从以上实现中,可以看到为完成ListIter的设计,其中暴露了许多List的实现细节,包括数据成员ListItem及其成员函数。对于调用者而言,这些都应该是屏蔽掉的。但是要设计出ListIter,就难免暴露出这些实现细节,也就是说要对List有丰富的了解。因此,惯常做法是由容器的设计者开发该容器的迭代器。如此,所有实现细节对使用者屏蔽,这也是为什么每种STL容器都有专属迭代器的原因。
在实际的算法中,在运用迭代器时,会用到迭代器所指对象中的相应类型(associate type)。
那么算法实现中该如何满足 声明一个以“迭代器所指对象(中)的类型”为类型的成员/参数,或返回值是“迭代器所指对象(中)的类型”的类型 的需求呢?
可分为以下三种情况:
对应的运用以下三种方法解决:
注意:这三种方法是逐步整合为最终的实现的方案就是 iterator_traits萃取机 机制,它包含了函数模板的参数推导,声明内嵌类型和偏特化所有内容,也同时解决了以上的三个场景的实现需求。
|
其中typename用法见typename关键字 |
需要注意的是,并不是所有的迭代器都是class type的。比如说原生指针(naive pointer),就无法将其定义为内嵌类型。所以还要对上述的一般化概念中的特殊情况做特殊化处理。
③偏特化(template partial specification):如果class template拥有一个以上的template参数,可以对其中某个(或数个,但非全部)template进行特化工作,其中偏特化有两种形式:
两种形式见下图所示:
偏特化其实就可以理解为“针对任何template参数进一步地进行条件限制所设计出的特化版本”。那么对应的,全特化(full specification) 就是指对模板参数不做任何限制。
如上,利用对(常量)指针类型的偏特化,解决了内嵌类型无法解决的迭代器所指对象是原生指针类型的问题。
在《Effective C++》条款47:请使用traits classes表现类型信息中对萃取机制也有详细解答,先抛开这些,从头开始讲起。
结合以上三种情况和三种解决方法,最终设计出了迭代器萃取机这一中间层,其作用是萃取出迭代器的相关特性(也即是相应类型的取用),以屏蔽迭代器实现对算法实现的影响。如左下图所示:
|
|
在图中列出了原生指针(pointer),还单独列出了常量指针(pointer-to-const)(对于const关键字的解析见:const限定符)。对于常量指针,泛化版本的iterator::traits
会返回一个const int,也即返回值是一个无法进行赋值的临时变量,所以针对常量指针的value_type
类型应当特化为非常量类型,如下:
template <class T>
struct iterator_traits<const T*> { //偏特化版本——当迭代器是一个pointer-to-const时
typedef T value_type; //萃取出的类型应当是T而非常量const T
//...
};
//附上指针的偏特化萃取实现
struct iterator_traits<T*> {
typedef T value_type;
//...
};
所以只要迭代器的实现者在实现过程中,将内嵌类型和原生类型遵循约定,以内嵌类型(nested typedef)的方式定义(typedef)出STL迭代器萃取机所规定的相关类型,就可以与STL标准实现兼容,实现算法实现和容器实现的剥离。
如右上图(iterator_traits对常用内嵌类型的typedef)所示,迭代器中最常用到的迭代器类型有五种,整理出如下表格:
|
|
为符合规范,任何迭代器都应提供五个内嵌相应类别,以利于traits萃取。STL为此提供了一个iterators class,也即std::iterator
如下,如果每个新设计的迭代器继承自它,就可保证STL所需之规范:
template <class Item>
struct ListIter : public std::iterator<std::forward_iterator_tag, Item> {
//...
};
设计适当的迭代器相应内嵌类型,是迭代器的责任;同时设计适当的迭代器,是容器设计者的责任。
唯有容器本身,才知道应当设计怎样的迭代器来遍历自己,还有如何运用相关的运算符;
有了traits技巧(其利用内嵌类型的编程技巧和template的参数推导功能),增强了c++未能提供的关于类型辨别方面的能力(c++是弱类型语言,能够进行隐式类型转换),完成了算法和容器间的解耦;其实个人感觉,traits技巧很像平台中间件的概念;