STL源码学习之迭代器(iterators)
迭代器的设计思维 - STL的关键所在
不论是泛型编程或是STL的实际应用,迭代器都扮演着重要角色。STL的中心思想在于:将数据容器(containers
)和算法(algorithms
)分开,彼此独立设计,最后再以一贴胶着剂将他们撮合在一起。
template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value) {
// 修改*first!=value
while (first!=last && *first!=value)
// while (first!=last && (*first).value()!=value)
++first;
return first;
}
迭代器是一种 smart point
迭代器是一种类似指针的对象,而指针里各种行为最常见也是最重要的便是内容提领(dereference)
和成员访问(member access)
,因此,迭代器最重要的编程工作就是对 operator*
和 operator->
进行重载工作。
template <class Item>
struct ListIter {
Item *ptr; // 保持与容器的联系
// default ctor
ListIter(Item* p =0) : ptr(p) {}
// 解引用 dereference
Item& operator*() const {
return *ptr;
}
// member access
Item* operator->() const {
return ptr;
}
// prefix increment, 返回对象
ListIter& operator++() {
ptr = ptr->next();
return *this;
}
// postfix increment, 返回值(新对象)
// int为占位符,提示编译器这是后自增
ListIter operator++(int) {
ListIter temp = *this;
++*this; // 调用前面的前自增
return temp;
}
bool operator==(const ListIter& i) const {
return ptr == i.ptr;
}
bool operator!=(const ListIter& i) const {
return ptr != i.ptr;
}
};
template <typename T>
class List {
// 定义public以供访问
public:
void insert_front(T value);
void insert_end(T value);
void display(std::ostream &os = std::cout) const;
ListItem<T> * front() {
return _front;
}
List();
private:
ListItem<T> *_end;
ListItem<T> *_front;
long _size;
};
//为了完成一个针对 List而设计的迭代器,我们无可避免曝露了太多 List细节:
// 在 main()为了制作 begin和 end两个迭代器,我们曝露了 ListItem;
// 在 ListIter class 为了达成 operator++的目的,我们曝露了 ListItem的操作函式 next()。
// 如果不是为了迭代器,ListItem 原本应该完全隐藏起来不曝光的。
// 换句话说,要设计出 ListIter,首先必须对 List 的细节有非常丰富的了解。
// 既然这无可避免,干脆就把迭代器的开发工作交给 List的设计者好了,
// 如此一来所有细节反而得以封装起来不被使用者看到。
// 这正是为什么每一种 STL 容器都提供有专属迭代器的缘故。
// 如果不写全局比较函数,则需要更改find函数判定
template <typename T> bool operator!=(const ListItem<T>& item, T n) {
return item.value() != n;
}
迭代器相应型别
迭代器所指之物就是相应型别的一种
Trait编程技法 - STL源代码密钥
迭代器所指对象的型别,称为该迭代器的value type
。上述的参数型别推导技巧虽可用于value type
,却非全面可用;万一 vaue type
必须用于函数的传回值,就束手无策了。
我们需要其他方法,声明内嵌型别是个好主意
template <class T>
struct MyIter {
typedef T value_type; // 内嵌型别声明
T* ptr;
MyIter(T* p = 0) : ptr(p) { }
T& operator*() const {return *ptr;}
// ...
};
/*
func 的返回值必须加上 typename,因为 T 是一个 template 参数
typename 告诉编译器这是一个型别,才能通过编译
*/
template <class I>
typename I::value_type // 这一行是 func 的返回值型别
func (I ite)
{ return *ite; }
Myiter<int> ite(new int(8));
cout << func(ite);
在STL实现中,traits编程技术得到大量的运用,它利用了“内嵌类型”的编程技巧与C++的template参数推导功能,弥补了C++类型识别方面的不足。通过traits,算法可以原汁原味的将迭代器的属性萃取出来,帮助算法正确高效的运行。
tempalte<typename I>
struct iterator_traits
{
typedef typename I::iterator_category iterator_category;
typedef typename I::value_type value_type;
typedef typename I::difference_type difference_type;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
};
1、 迭代器相应型别之一:value type
value type即迭代器所指对象的类型。
2、 迭代器相应型别之二:difference type
两个迭代器之间的距离,因此它可以表示一个容器的最大容量;如果一个泛型算法提供计数功能,如count(),其返回值就必须使用迭代器的difference type;
定义difference_type:
// 泛化版本
template<class T>
struct iterator_traits{
...
typedef typename I::difference_type difference_type;
}
// 针对原生指针的偏特化版本(非const)
template<class T>
struct iterator_traits<T*>{
...
typedef ptrdiff_t difference_type;
}
// 针对原生指针的偏特化版本(const)
template<class T>
struct iterator_traits<const T*>{
...
typedef ptrdiff_t difference_type;
}
3 迭代器相应型别之三:reference type
根据迭代器所指对象能否改变,迭代器被分为两类:
不允许改变:constant iterators,如const int* pic;
允许改变:mutable iterators,如int* pi。
左值 如 i = 5 + 1,i 在运算符的左边,称为左值。
在C++中,如果函数要传回左值,都是以by reference的方式进行的:
如果p是一个对象值能改变的迭代器,设它的value type是T,那么p的类型不能是T,而应该是T&;
如果p是一个对象值不能改变的迭代器,设它的value type是T,那么p的类型不能是const T,而应该是const T&。
4 迭代器相应型别之四:pointer type
我们可以传回一个pointer,指向迭代器所指之物。
5 迭代器相应型别之五:iterator_category
根据移动特性与实施操作,迭代器被分为五类:
Input Iterator:这种迭代器所指的对象,不允许外界改变,即read only;
Output Iterator:write only;
Forward Iterator:允许写入型算法在此种迭代器所形成的区间上进行读写操作;
Bidirectional Iterator:可双向移动,即可以正向遍历,也可以反向遍历;
Random Access Iterator:前四种迭代器只支持部分指针算术能力(前三种支持++,第四种在此基础上支持–),但是第五种则涵盖所有指针算术能力,如p+n、p-n、p[n]、p1-p2、p1<p2。
在编写代码的时候最好能够在编译期就能决定使用哪个版本,如果在运行期使用 if else 来判断,会影响效率,因此推荐使用重载函数来解决(不同的迭代器类型会有不同的计算法方式,以达到效率的最大化)。
STL算法命名规则: 以算法所能接受之最低阶迭代器类型,来为其迭代器型别参数命名(即最低层次,最泛化的那一个)。
std::iterator 的保证
STL提供了一个超类iterator,如果每个设计的迭代器类都继承自它,就可以保证符合STL的规范,iterators class不含任何成员函数,纯粹只是型别定义(因为如果设计的迭代器不含五个内嵌相应型别,traits就无法萃取它,它也会独立于整个STL之外)。iterator类如下:
template<class Category, // 没有默认值,需传参
class T,// 没有默认值,需传参
// 以下三个参数都有默认值
class Distance = ptrdiff_t,
class Pointer = T*,
class Reference = T&>
struct iterator {
typedef Category iterator_category;
typedef T value_type;
typedef Distance difference_type;
typedef Pointer pointer;
typedef Reference reference;
}
因此,声明一个迭代器类只需要继承它,然后指定 Category 和 T 的值即可:
template<class Item>
struct ListIter : public std::iterator<std::forward_iterator_tag,Item>
总结:
-
迭代器的责任:设计相应的型别(包含traits(泛化版、针对原生指针的偏特化版、针对const的偏特化版))
-
容器的责任:设计适当的迭代器,只有容器本身才知道如何设计一个迭代器来遍历自己,并执行迭代器该有的各种行为(如前进、后退、取值、取用成员等等)。
-
算法:则完全独立于容器和迭代器之外自行发展,只要设计时以迭代器为对外接口即可。
SGI STL 的私房菜:__type_traits
type_traits: 负责萃取迭代器的特性
__type_traits: 负责萃取型别的特性,即判断这个型别是否具有 non-trivial 默认的构造函数、是否具备 non-trivial 拷贝构造函数、 non-trivial 赋值操作符、 non-trivial dtor(析构),如果不含有这些函数,那么在对这个类型进行构造、析构、拷贝、赋值等操作时,就可以采用最有效率的、直接针对内存的操作,如malloc()、memcpy()等等,从而获得最高的效率。
根据 iterator_traits 的实现方法,我们可以类比实现出 __type_traits (用来返回类型的特性):
typedef __type_traits::has_trivial_default_constructor; // 构造函数
typedef __type_traits::has_trivial_copy_constructor; // 拷贝构造函数
typedef __type_traits::has_trivial_assignment_operator; // 赋值运算符
typedef __type_traits::has_trivial_destructor; // 析构
typedef __type_traits::is_POD_type; // Plain Old Data
我们希望上面能够返回带true或者false的对象,因为我们希望根据该对象进行参数推导,因此我们应该传回(即一个类的对象):
struct __true_type {}; // 表示可以用最快的方法(即针对内存的方式)来进行拷贝或辅助操作
struct __false_type {};
__type_traits具体的一个实现:
template<class type>
struct __type_traits{
typedef __true_type this_dummy_member_must_be_first; // 不要删除该行,避免冲突
typedef __false_type has_trivial_default_constructor; // 构造函数
typedef __false_type has_trivial_copy_constructor; // 拷贝构造函数
typedef __false_type has_trivial_assignment_operator; // 赋值运算符
typedef __false_type has_trivial_destructor; // 析构
typedef __false_type is_POD_type; // Plain Old Data
}
保守起见,默认的所有对象都是 __false_type 的,即不能直接使用内存进行操作,然后通过定义特化版本的 __type_traits ,可以解决这个问题,如:
// 针对基本数据类型的偏特化版本
// __STL_TEMPLATE_NULL 是类模板的显示(excplicit)特化(specialization)
__STL_TEMPLATE_NULL struct __type_traits<char/int/double/...>{
typedef __true_type has_trivial_default_constructor; // 构造函数
typedef __true_type has_trivial_copy_constructor; // 拷贝构造函数
typedef __true_type has_trivial_assignment_operator; // 赋值运算符
typedef __true_type has_trivial_destructor; // 析构
typedef __true_type is_POD_type; // Plain Old Data
}
// 针对原生指针的偏特化版本
template<class T>
struct __type_traits<T*>{
typedef __true_type has_trivial_default_constructor; // 构造函数
typedef __true_type has_trivial_copy_constructor; // 拷贝构造函数
typedef __true_type has_trivial_assignment_operator; // 赋值运算符
typedef __true_type has_trivial_destructor; // 析构
typedef __true_type is_POD_type; // Plain Old Data
}
什么时候一个class该有自己的non-trivial-xxx呢:
如果class内含指针成员,并且对它进行内存动态配置,那么这个class就需要实现出直接的non-trivial-xxx。
写一个迭代器类要做的事情: 指定迭代器五大基本类型(或继承iterator基类,就只需要指定前两个类型)、定义好别名(STL编码规范)、构造函数、重载运算符等等