STL中deque詳解

主流的STL容器的數據結構都比較常規,類似List就是實現了鏈表的數據結構,數據以一個node接連串接一個node的形式存儲;vector則是一個連續空間存儲的變長數組,當空間用完後則申請一倍的空間並老數據拷貝到新分配空間中;map和set則是紅黑樹,一種特殊的二叉平衡搜索樹,保證根到葉節點最大的層數差距在兩倍以內,以此保證搜索的速度和維護樹結構的性能平衡;unordered_map和unordered_set則使用哈希表,有不同的哈希函數,也有不同的桶實現方法,在此不予細究;唯獨deque的實現略“非主流”,這裏詳細結合STL中的實現解析一下。

基本原理概述

侯捷的《STL源碼剖析》中有詳細的理論解析,只是示例代碼有點老,在此截圖一用:
在這裏插入圖片描述
上圖可以明顯的看出,deque對於數據的中的存儲並不是嚴格連續的,而是分成了很多個緩衝區,用一個map(並非STL中map,而只是一個簡單的連續數組)存儲所有緩衝區的地址指針,每個緩衝區都可以存儲多個數據,當一個緩衝區填滿以後,會開闢新的緩衝區存儲數據,並將其地址指針存入map,若map也已經存滿以後,會創建新的更大的map,並將舊的map數據拷貝進去並釋放舊的空間。

代碼分析

下面結合代碼來詳細看看細節(代碼來自gcc5.4自帶的STL,stl_deque.h):

  template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
    class deque : protected _Deque_base<_Tp, _Alloc>

deque繼承自_Deque_base類,空間分配器默認爲std標準分配器allocator(該版本底層實現爲new和delete的封裝,而並非一級二級配置器的模式),_Deque_base類主要完成了deque的內存管理相關的任務,拿一些主要的部分來看:

  template<typename _Tp, typename _Alloc>
    class _Deque_base
    {
    protected:
      typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
	rebind<_Tp>::other _Tp_alloc_type;  //確定_Tp空間分配器類型
      typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type>	 _Alloc_traits;         //alloc_traits會實際調_Tp_alloc_type來分配內存和完成構造和析構操作,這裏不詳細分析
      
      typedef typename _Alloc_traits::pointer		_Ptr;
      typedef typename _Alloc_traits::const_pointer	_Ptr_const;

      typedef typename _Alloc_traits::template rebind<_Ptr>::other
	_Map_alloc_type;  //確定_Map空間分配器類型
      typedef __gnu_cxx::__alloc_traits<_Map_alloc_type> _Map_alloc_traits;   //同上
	  ...

以上代碼主要用來確定空間分配器類型,之後的接口中會利用以上確定的空間分配器分配空間以及構造對象,再看看主要成員和接口:

	struct _Deque_impl
      : public _Tp_alloc_type
    {
	_Map_pointer _M_map;		//上文提到的map地址
	size_t _M_map_size;			//map大小
	iterator _M_start;			//首元素所在map節點迭代器
	iterator _M_finish;			//尾後元素所在map節點迭代器
	...
	}
	_Deque_impl _M_impl;

實現了一個內置類型,繼承自_Tp空間分配器類,定義一個其對象並作爲空間分配器,之後所有空間分配任務和構造析構任務都由這個對象完成,同時包含一些重要成員。

	  _Ptr
      _M_allocate_node()
      { 
	typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Traits;
	return _Traits::allocate(_M_impl, __deque_buf_size(sizeof(_Tp)));
      }

      void
      _M_deallocate_node(_Ptr __p) _GLIBCXX_NOEXCEPT
      {
	typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Traits;
	_Traits::deallocate(_M_impl, __p, __deque_buf_size(sizeof(_Tp)));
      }

_Deque_base僅僅只包涵了分配空間和回收的接口,這裏只拿了node(上圖中的緩衝器)相關的接口,還有map的接口,都大同小異,都是直接調用空間分配起來分配空間。最後提一下_Deque_base的構造中會用到的_M_initialize_map(size_t __num_elements)函數:

template<typename _Tp, typename _Alloc>
    void
    _Deque_base<_Tp, _Alloc>::
    _M_initialize_map(size_t __num_elements)
    {
      const size_t __num_nodes = (__num_elements/ __deque_buf_size(sizeof(_Tp))
				  + 1);           //根據初始化元素的個數確定需要的map大小

      this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,
					   size_t(__num_nodes + 2));      //根據上面確定map的大小決定(最小爲_S_initial_map_size=8)
      this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size);				//分配map的空間

      // For "small" maps (needing less than _M_map_size nodes), allocation
      // starts in the middle elements and grows outwards.  So nstart may be
      // the beginning of _M_map, but for small maps it may be as far in as
      // _M_map+3.

      _Map_pointer __nstart = (this->_M_impl._M_map
			       + (this->_M_impl._M_map_size - __num_nodes) / 2);    //因爲是雙端隊列map都是從中間開始分配空間,首元素向左生長,尾元素向右,這點比較重要
      _Map_pointer __nfinish = __nstart + __num_nodes;    
	
	//以下給緩衝器分配空間
      __try
	{ _M_create_nodes(__nstart, __nfinish); }
      __catch(...)
	{
	  _M_deallocate_map(this->_M_impl._M_map, this->_M_impl._M_map_size);
	  this->_M_impl._M_map = _Map_pointer();
	  this->_M_impl._M_map_size = 0;
	  __throw_exception_again;
	}
	  //以下就是空間分配完成後初始化_M_impl中各個關鍵元素,上文已經提過相應意義
      this->_M_impl._M_start._M_set_node(__nstart);
      this->_M_impl._M_finish._M_set_node(__nfinish - 1);
      this->_M_impl._M_start._M_cur = _M_impl._M_start._M_first;
      this->_M_impl._M_finish._M_cur = (this->_M_impl._M_finish._M_first
					+ __num_elements
					% __deque_buf_size(sizeof(_Tp)));
    }

到這裏_Deque_base的任務基本就完成了,它所有的任務就是提供創建、回收map和node的接口,方便後面的使用。
然後來看看deque的主要實現,主要就是完成元素的插入刪除,以插入爲例子:

	  void
      push_back(const value_type& __x)
      {
	if (this->_M_impl._M_finish._M_cur
	    != this->_M_impl._M_finish._M_last - 1) 
	  {
	    _Alloc_traits::construct(this->_M_impl,
	                             this->_M_impl._M_finish._M_cur, __x);
	    ++this->_M_impl._M_finish._M_cur;
	  }
	else
	  _M_push_back_aux(__x);
      }

邏輯還是比較簡單的,if判斷該緩衝區還有沒有空間,有就直接創建對象返回,沒有就調用_M_push_back_aux(__x),看看它的實現:

      void
      deque<_Tp, _Alloc>::
      _M_push_back_aux(_Args&&... __args)
      {
	_M_reserve_map_at_back();   // 1
	*(this->_M_impl._M_finish._M_node + 1) = this->_M_allocate_node();  // 2
	__try
	  {
	    _Alloc_traits::construct(this->_M_impl,     // 3
	                             this->_M_impl._M_finish._M_cur,
			             std::forward<_Args>(__args)...);
	    this->_M_impl._M_finish._M_set_node(this->_M_impl._M_finish._M_node
						+ 1);
	    this->_M_impl._M_finish._M_cur = this->_M_impl._M_finish._M_first;
	  }
	__catch(...)
	  {
	    _M_deallocate_node(*(this->_M_impl._M_finish._M_node + 1));
	    __throw_exception_again;
	  }
      }

_M_push_back_aux(__x)也並不複雜,因爲目前尾元素所在緩衝區空間不夠了,因此需要申請新的空間,1、_M_reserve_map_at_back()實現的主要功能就是檢查map空間是否足夠,不夠就申請新的空間,並將數據移動過去,並更新_M_impl中存儲關鍵元素的信息;2、然後根據尾元素所在節點申請新的緩衝區空間;3、最後構造元素。
push_front()基本完全一致,只不過其生長方向與back完全相反,可以簡單理解爲front和back從初始的中間位置分別向兩端生長,一旦所在緩衝區空間不夠就申請新的空間,並在新的空間繼續生長,最後看下pop_back函數,front類似:

	  void
      pop_back() _GLIBCXX_NOEXCEPT
      {
	if (this->_M_impl._M_finish._M_cur
	    != this->_M_impl._M_finish._M_first)
	  {
	    --this->_M_impl._M_finish._M_cur;
	    _Alloc_traits::destroy(this->_M_impl,
	                           this->_M_impl._M_finish._M_cur);
	  }
	else
	  _M_pop_back_aux();
      }

可以看到,基本就是push_front反過來,if檢查當前元素是否到達當前緩衝區最前端,不在則直接pop,否則調用_M_pop_back_aux(),_M_pop_back_aux()與_M_push_back_aux()也是幾乎完全相反,需要注意的是它會回收緩衝區內存,其他的這裏就不再贅述了。

總結

總體來看,deque由於結構的原因,它整體的實現比vector要複雜很多,緩衝區有點類似於一個每個“節點”都是一個大block的List,只不過它的操作主要在頭尾兩端,頭尾節點一旦空間不夠了就新建“節點”,pop空了就刪除“節點”,因此如果不想要頻繁在頭部操作時,選擇vector要遠遠優於deque;但如果有大量頭部操作時(例如FIFO)還是老老實實用deque吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章