STL源码剖析——iterators与trait编程#1 尝试设计一个迭代器
STL的中心思想在于:将数据容器与算法分开,独立设计,再用一帖粘着剂将它们撮合在一起。而扮演粘着剂这个角色的就是迭代器。容器和算法泛型化,从技术角度来看并不困难,C++的模板类和模板函数可分别达成目标,但如何设计出两者之间良好的粘着剂,才是大难题。
我们可以来尝试一下自己设计一个迭代器,看途中会遇到一些什么样的难题,同时看看SGI STL是怎么解决它们的。不过在此之前,我们可以先使用一下迭代器,看看其有什么样的功能,以find()函数为例:
1 //来自SGI <stl_algo.h> 2 template <class InputIterator, class T> 3 InputIterator find(InputIterator first, InputIterator last, const T& value) { 4 while (first != last && *first != value) ++first; 5 return first; 6 }
只要给予不同的迭代器,find()便能够对不同的容器进行查找操作,那么按照这个说法,迭代器似乎是依附在容器之下的,而且在函数中提取了迭代器指向的内容(*firsst),这给我们提供一点设计思路:
迭代器是一种行为类似指针的对象,而指针的各种行为中最常见的就也是最重要的便是内容提取和成员访问,因此,迭代器最重要的工作就是对operator *和operator ->进行重载。关于这一点,我们可以参考一下C++98智能指针auto_ptr是怎么设计的,虽然是已经被摒弃掉的玩意,但是看看也无妨,至于智能指针是干嘛用的,这里就不在介绍。
略做简化:
1 template <class X> 2 class auto_ptr { 3 private: 4 X* ptr; 5 public: 6 typedef X element_type; 7 explicit auto_ptr(X* p = 0) __STL_NOTHROW : ptr(p) {} 8 auto_ptr(const auto_ptr& a) __STL_NOTHROW : ptr(a.ptr) {} 9 template <class T> 10 auto_ptr& operator=(const auto_ptr<T>& a) __STL_NOTHROW { 11 if (&a != this) { 12 reset(a.release()) 13 } 14 return *this; 15 } 16 ~auto_ptr() { 17 delete ptr; 18 } 19 X& operator*() const __STL_NOTHROW { return *ptr; } 20 X* operator->() const __STL_NOTHROW { return ptr; } 21 X* get() const __STL_NOTHROW { return ptr; } 22 };
有了模仿对象,现在我们可以来为list设计一个迭代器,但并不是STL里那个复杂的list,而是自己简单设计的一个list:
1 template <typename T> 2 class List 3 { 4 void insert_front(T value); 5 void insert_end(T value); 6 void display(std::ostream &os = std::cout)const; 7 //... 8 private: 9 ListItem<T>* _end; 10 ListItem<T>* _front; 11 long _size; 12 }; 13 14 template <typename T> 15 class ListItem 16 { 17 public: 18 T value()const { return _value; } 19 ListItem* next()const { return _next; } 20 //... 21 22 private: 23 T _value; 24 ListItem* _next; //单向链表 25 };
如果我们想对List这个容器使用find()函数,就要为其设计一个迭代器。当我们提取这个迭代器的内容时,返回的应该是一个ListItem对象;当我们递增迭代器时,它应该指向下一个ListItem对象。为了让该迭代器适用于任何类型的结点,而不只限于ListItem对象,我们可以将它设计为一个模板类:
1 template<typename Item> 2 struct ListIter 3 { 4 Item *ptr; 5 6 ListIter(Item* p = 0) :ptr(p); 7 8 Item& operator*()const { return *ptr; } 9 Item* operator->()const { return ptr; } 10 11 ListIter& operator++() 12 { 13 ptr = ptr->next(); return *this; 14 } 15 ListIter& operator++(int) 16 { 17 ListIter tmp = *this; 18 ++*this; 19 return tmp; 20 } 21 bool operator==(const ListIter& i)const 22 { 23 return this->ptr == i.ptr; 24 } 25 bool operator!=(const ListItem& i)consts 26 { 27 return !*this == i; 28 } 29 };
其实可能从上面就能看出一些问题了,看似是针对List而设计的迭代器,但在实际使用时,我们必须把List的结点类(ListItem)而非List本身作为类型参数传给ListIter才能正常操作:
1 void main() 2 { 3 List<int> mylist; 4 5 for (int i = 0; i < 5; i++) 6 { 7 mylist.insert_front(i); 8 mylist.insert_end(i + 2); 9 } 10 mylist.display(); 11 12 ListIter<ListItem<int>>begin(mylist.front()); 13 ListIter<ListItem<int>>end; 14 ListIter<ListItem<int>>iter; 15 16 iter = find(being, end, 3); 17 if (iter == end) 18 cout << "not found" << endl; 19 else 20 cout << "found: " << iter->value() << endl; 21 22 iter = find(begin, end, 7); 23 if (iter == end) 24 cout << "not found" << endl; 25 else 26 cout << "found: " << iter->value() << endl; 27 }
为了完成一个针对List而设计的迭代器,我们无可避免地暴露了太多List的实现细节:在main()之中为了制作begin和end两个迭代器,我们暴露了链表结点类ListItem;在迭代器类的设计中我们暴露了ListItem的操作函数next()。如果不是为了迭代器,结点类应该完全隐藏起来,于真正的List而言,我们也没见过其背后结点类的任何细节。换句话说,要设计出ListIter,首先必须对List的实现细节有非常丰富的了解。既然如此,干脆就把迭代器的开发工作交给List的设计者好了,如此一来,所有的实现细节都能够被封装起来不被使用者看到,这也是为什么每一种STL容器都提供了专属的迭代器。
本节参考了智能指针auto_ptr设计了一个针对List的迭代器,发现了如果要想在迭代器的使用中不暴露容器的实现细节,就必须把迭代器设计工作交给容器的开发者。日后在学习各容器时都会对其特定的迭代器做一定的总结。