STL源码学习

Posted by AJW on May 25, 2017

C++的书也看了几本了,除了最开始的《C++ Primer》让自己初步了解了C++的基本语法,《Effective C++》和《深度探索C++对象模型》基本都没记住太多东西,感觉还不是很适合我这种入门的门槛还没跨过去的人,这些书应该适合有一定代码量之后再去学习。所以,自己有了非常强烈的意愿想去写点C++的代码,或者看看别人的C++代码到底是怎么写的。知乎上看到有人推荐自己实现一遍STL的源码,正好之前也想看《STL源码剖析》这本书,所以就从图书馆借来看了一些,发现SGI STL的源代码没有想象的那么晦涩,结构还是很清晰的,感觉可以写起来哇!那就开篇博文专门来记录一下看书和源码过程中的收获,顺便也自己实现一个STL 的子集吧,开搞开搞!

空间配置器

STL的空间配置器用来进行内存的申请与释放,SGI采用的是一个特殊的两级空间配置器,第一级直接采用malloc和free来申请和释放内存空间,第二级则采用了内存池技术。具体来说,如果申请的区块大于128 bytes,那么就直接交由第一级配置器来配置,否则,第二级配置器维护了一个内存空间的链表free list,分别用来管理大小为8、16、24、32、40……128的内存区块。每次申请会把申请空间对齐为8个倍数,然后从对应的链表下取出相应大小的空间,释放时再将空间还回,这样就节省了大量的malloc操作。

空间配置器

当对应的链表下没有可用的空间时,会从内存池内取出若干个对应大小的空间来填充链表,当然这里就比较复杂了:

  1. 如果内存池里的空间足够,那么直接取出;

  2. 如果内存池内的空间不足,那么检查是否能先返回至少1个空间块(例如申请了20个32 bytes的空间,但内存池只能提供10个,那么就先返回10个),

  3. 如果连一个都提供不了,那么就只能调用malloc来申请新的空间

  4. 如果malloc失败,那就调用第一级的配置器,那里有申请失败的一些处理函数

Traits编程技法

traits这个东西在《Effective C++》里就看到了,当时没有理解,感觉也没什么作用,而在STL这里发现被大量使用,甚至被作者称为STL源代码门钥。traits编程最主要的作用是在迭代器上,用来“榨取”迭代器的类型信息。

考虑如下代码:

template<typename T>class Myiter{
  typedef T value_type;
  ...
};

template<typename I> typename I::value_type func(I ite){
  return *ite;
}

通过在Myiter中内嵌一个类别信息,就可以在模板函数func中使用类型T的信息了,但是在STL中,如果func是一个以迭代器为输入参数的函数的话,它也因该支持内置的指针类型,而内置指针并不是一个类,无法定义内嵌类型,这个时候怎么办呢?

我们可以利用偏特化来解决这个问题,专门来设计一个类来“萃取”迭代器的类型信息

template<class I> class iterator_traits{
  typedef typename I::value_type value_type;
}

这样func可以写成:

template<typename I> 
	typename iterator_traits<I>::value_type func(I ite){
  return *ite;
}

而对于内置的指针类型,我们可以定义interator_traits的一个偏特化版本

template<class I> class iterator_traits<T*>{
  typedef T value_type;
}

template<class I> class iterator_traits<const T*>{
  typedef T value_type;
}

这样函数在迭代器和内置指针类型上就都可以正常运行了!

所以,为了符合STL的标准,每个迭代器都要定义一些相应类型,才能兼容哦,这些类型包括value_type, difference_type, pointer, reference, iterator_category, 而inerator_traits就会“萃取”出这些类型。

template<class I> class iterator_traits{
  typedef typename I::value_type value_type;
  typedef typename I::difference_type difference_type;
  typedef typename I::pointer pointer;
  typedef typename I::reference reference;
  typedef typename I::iterator_category iterator_category;
}

为了以防万一你忘记定义这些东西,STL提供了一个iterator class,每一个新的迭代器都继承于它,

template <class Category,
		  class T,
		  class Distance = ptrdiff_t,
		  class Pointer = T*,
		  class Reference = T&>
struct interator{
  typedef Category iterator_category;
  typedef T value_type;
  typedef Distance defference_type;
  typedef Pointer pointer;
  typedef Reference reference;
};

由于后三个都有默认值,所以在定义新的迭代器时,只需要继承该类并提供category和T的信息就好。

序列式容器

vector

对于每个容器来说,个人感觉内存管理总是最重要的。vector的元素在内存中全部都是连续存放的,所以vector在中间插入元素效率可能会很慢,因为可能会涉及到整块内存的移动。当空间不够的时候,就会重新申请原空间两倍大小的新内存区域,然后把数据整体迁移过去。所以插入元素后,vector的迭代器可能会失效哦。

说到vector的迭代器,其实没有单独的vector_iterator的struct,直接采用的就是内置类型的指针,所以它的类型是random access itrator。

vector的其他方面应该都还好,仔细地写好就好了。但是目前只实现了一小部分功能,还有其他的一些例如:const interator,reverse interator这些,都还没有加入,可以之后继续完善。

list

list的内存结构和vector有所不同,它不是连续存储的,而是一个类似双向链表的结构。所以,有一个单独的list_node的struct,里边存储了数据,前向指针prev和后向指针next。

list的内部其实是一个循环双向链表,其中存储了一个空的list_node作为哨兵节点(或者说头节点应该也可以吧)。当list是空的时候,这个node的prev和next都指向自身。

list的迭代器是一个bidirectional iterator,里边封装了一个指向list_node的指针。需要注意的是在重载迭代器的 “->”操作符时,具体的写法哦。

reference operator*() { return node->data; }
pointer operator->() { return &(operator*()); }

list有一个比较反常理的地方是它的size函数,是O(n)复杂度的,而不是O(1)的,作者的解释说是,为了在执行splice操作的时候,不用去遍历一遍需要接合部分的长度。所以,当list长度过大的时候,size()的性能可能会有些差,也许是个隐藏的坑!

list中有一个sort函数,《STL源码剖析》里边说是quick_sort,看了半天没看懂,最后查了一些资料,其实是一个merge sort。

template<class T, class alloc>
	void list<T, alloc>::sort() {
		list<T, alloc> carry;
		list<T, alloc> counter[64];
		int fill = 0;
		while (!empty()) {
			carry.splice(carry.begin(), *this, begin());
			int i = 0;
			while (i < fill && !counter[i].empty()) {
				counter[i].merge(carry);
				carry.swap(counter[i++]);
			}
			carry.swap(counter[i]);
			if (i == fill) ++fill;
		}

		for (int i = 1; i < fill; ++i)
			counter[i].merge(counter[i-1]);
		swap(counter[fill-1]);
	}

函数中用counter数组记录2^n个元素merge排序后的结果,所以其实是一个非递归版本的merge sort。

deque

deque也是一个连续空间的容器,但是他与vector不一样的是,它是双向开口的,即在两端都可以在常数时间内进行元素插入和删除操作。而且,deque没有所谓容量的概念,它的连续空间实际上是由多段连续空间组合而成的,可以随时增加一段新的空间连接起来,因此,deque不会像vector那样,在空间不足时,申请更大的空间,然后复制元素,再释放旧空间。另外,deque有自己的迭代器,虽然是Random Access Iterator,但是比起vector的普通指针效率还是差了很多。

deque的内存管理

deque中存在一个“中控器”,来管理deque的内存。这个中控器其实是个数组,类似于整个STL的内存池的数组,数组的每个元素也是一个指针,指向deque存储的数据类型的数据。deque的数据会尽量放置在中控器的中间部位,这样给deque的两端都留下一些空闲位置,可以用来插入新的数据。当空间不够的时候,将会申请更大的一个中控器。

deque_map