STL迭代器的"特性萃取機"-----Traits

LLZK_發表於2016-12-17

Traits就像一臺”特性萃取機“,它可以毫無偏差的榨取各個迭代器的特性。在講Traits之前,我們先要把迭代器的特性搞清楚。

什麼是迭代器的特性?

迭代器的特性就是迭代器的型別,最常用的迭代器的5中種型別(這5種型別都為內嵌型別),每一種迭代器都有以下五種型的定義:
1.value_type
迭代器所指向物件的型別

2.difference_type
表示兩個迭代器之間的距離,可以用來表示一個容器的最大容量。對於連續空間的容器而言,頭尾之間的距離就是最大容量。
針對不同的迭代器,都有不同的difference_type,在Traits(迭代器特性萃取機)中有針對原生指標而寫的特化版本,它以c++標準庫中ptrdiff _t(標頭檔案<stddef.h>或<cstddef>)作為原生指標的difference_type。原則上原生指標也是一種迭代器。
原始碼:
////////////////////////////////////////////////////////////
//原版(未特化版本)
template<class I>// I為一個迭代器型別
struct iterator_traits
{
          ...
          typedef typename I::difference_type difference_type;
};
////////////////////////////////////////////////////////////
//針對原生指標 T* 設計的偏特化版本
template<class T>
struct iterator_traits<T*>
{
          ...
          typedef ptrdiff_t difference_type;
};
////////////////////////////////////////////////////////////
//針對原生指標 const T* 設計的偏特化版本
template<class T>
struct iterator_traits<const T*>
{
          ...
          typedef ptrdiff_t difference_type;
};

ptrdiff _t:c/c++標準庫中定義的一個與機器相關的資料型別,ptrdiff _t型別變數通常用來儲存兩個指標加減法操作得到的結果,通常被定義成long int型別。

3.reference__type
迭代器指向物件型別的引用 ,比如我們在實現對operator*的過載時:
Item& operator*()const//Item是迭代器指向物件的型別
{
          return *ptr;
}
Item&就是一個reference__type。

4.pointer_type
迭代器指向物件型別的指標,比如在實現operator->()過載時:
Item* operator*()const//Item是迭代器指向物件的型別
{
          return ptr;
}
Item*就是一個pointer_type

在Traits中原生指標和const原生指標的偏特化版本中同樣對pointer_type和reference__type做出了自己的定義。
////////////////////////////////////////////////////////////
//原版(未特化版本)
template<class I>// I為一個迭代器型別
struct iterator_traits
{
          ...
          typedef typename I::reference__type reference__type;
          typedef typename I::pointer_type pointer_type;
};
////////////////////////////////////////////////////////////
//針對原生指標 T* 設計的偏特化版本
template<class T>
struct iterator_traits<T*>
{
          ...
          typedef T* reference;
          typedef T* pointer;
};
////////////////////////////////////////////////////////////
//針對原生指標 const T* 設計的偏特化版本
template<class T>
struct iterator_traits<const T*>
{
          ...
          typedef const T* reference;
          typedef const T* pointer;
};

5.iterator_category 
這個型別我們可以認為是迭代器的型別。總的來說,迭代器可分為5種型別:
①InputIterator 只讀;
②OutputIterator 只寫;
③ForwardIterator 可讀寫,但是隻能前向移動;
④BidrectionalIterator 可讀寫,可雙向移動;
⑤RandomAccessIterator 這個迭代器涵蓋以上4種的所有能力再加上p+n,p-n,p[n],p1-p2,p1<p2;

這些迭代器的分類和從屬關係可以用下圖表示(下圖只是概念和強化的關係,並不是C++中的繼承關係):

從上圖可以看出,假如你需要一個ForwardIterator,我把一個RandomAccessIterator給你,也是可以用的,每一個RandomAccessIterator 都是一個ForwardIterator 。但是這並不是最佳的選擇。要知道,STL是非常注重程式碼的效率的,我們要嚴記這一點。所以我們需要iterator_category這個型別來標明迭代器的型別,以供我們使用。

每種迭代器都會有以上5中型別的定義,以下為stl_iterator.h中的實現:
template <class T, class Distance> struct input_iterator {
  typedef input_iterator_tag iterator_category;
  typedef T                  value_type;
  typedef Distance           difference_type;
  typedef T*                 pointer;
  typedef T&                 reference;
};
struct output_iterator {
  typedef output_iterator_tag iterator_category;
  typedef void                value_type;
  typedef void                difference_type;
  typedef void                pointer;
  typedef void                reference;
};
template <class T, class Distance> struct forward_iterator {
  typedef forward_iterator_tag iterator_category;
  typedef T                    value_type;
  typedef Distance             difference_type;
  typedef T*                   pointer;
  typedef T&                   reference;
};
template <class T, class Distance> struct bidirectional_iterator {
  typedef bidirectional_iterator_tag iterator_category;
  typedef T                          value_type;
  typedef Distance                   difference_type;
  typedef T*                         pointer;
  typedef T&                         reference;
};
template <class T, class Distance> struct random_access_iterator {
  typedef random_access_iterator_tag iterator_category;
  typedef T                          value_type;
  typedef Distance                   difference_type;
  typedef T*                         pointer;
  typedef T&                         reference;
};

為了符合規範,任何迭代器都應該提供五個內嵌相應型別,以利於traits萃取,否則就是有別於整個STL框架,就可能無法與其他STL元件順利搭配。為了避免程式設計師寫程式碼時出現漏寫的情況,STL提供了一個iterator class,令每一個新設計的迭代器都繼承它,來保證STL的規範。iterator class不含任何成員,僅僅是型別的定義,繼承它並不會造成任何的額外負擔。
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;
};


迭代器Triats程式設計,簡單來說利用內嵌型別和template模板引數的自動推導能力,萃取出不同迭代器不同的特性(內嵌型別)。

為什麼需要Traits?
很簡單,解決效率問題。在上文中我提到過,用一個RandomAccessIterator替代一個ForwardIterator是一種很不明智的做法。大大降低了STL的效率。再來一個更明顯的例子。
Advance:假如我們現在需要使一個迭代器it向後移動n歩,但是我們並不知道迭代器的型別。我們應該怎麼做?
採用迴圈加法?
while (n>0)
{
          it++;
          n--;
}
如果it是上面所講的前3中迭代器的任何一種,這種方法都是唯一的選擇。但是如果it是RandomAccessIterator型別,我們本來採用O(1)的演算法直接令it+n就可以達到目的,為什麼還要使用O(N)的演算法呢?
更可怕的是,如果it是一個雙向迭代器BidrectionalIterator,並且n<0,那麼這個迴圈根本就不會進行。it也就不會移動。
所以在一些情況下我們必須知道迭代器的型別。所以就需要Traits。

Traits是如何實現的?
首先我們要給五種迭代器型別定義一個標籤
原始碼:
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};

在上文介紹型別的時候我說過,迭代器這5中型別並不是c++中的繼承關係,那為什麼這裡我們用到了繼承呢?這裡就是STL的一個巧妙設計,我們放到後文中講。

實現:以advance函式為例
函式定義:
template<class InputIterator,class Distance>
inline void advance(InputIterator& i,Distance n )
作用:令迭代器 i 向前移動n歩。
我們知道,對於不同型別的迭代器,我們都有各自不同的移歩操作,那麼我是不是從新設計一個__advance,將迭代器的型別作為引數傳入,令advance遞迴呼叫__advance就可以了呢?移步操作交給__advance來做。如下:
template<class InputIterator, class Distance>
inline void advance(InputIterator& i, Distance n)
{
          __advance(i, n, 迭代器型別);
}
STL就是這麼做的,如下:
template<class InputIterator, class Distance>
inline void advance(InputIterator& i, Distance n)
{
          __advance(i, n, iterator_traits<InputIterator>::iterator_category);
}
不知道你有沒有發現,這個實現的第一個模板引數就是我們的第一個迭代器的型別只讀迭代器InputIterator,這不符合我們程式設計時的習慣的命名規範,其實不是,這是STL演算法的一個命名規則:以演算法所能接受的最低階的迭代器型別,來為其迭代器型別引數命名。

回頭來說,STL是通過一個iterator_traits來將迭代器的型別以引數傳入的。iterator_traits是個什麼呢?
原始碼定義:
template <class Iterator>
struct iterator_traits {
  typedef typename Iterator::iterator_category iterator_category;
  typedef typename Iterator::value_type        value_type;
  typedef typename Iterator::difference_type   difference_type;
  typedef typename Iterator::pointer           pointer;
  typedef typename Iterator::reference         reference;
};

typename的作用:Iterator是一個模板型別引數(template),在它被編譯器具現化之前,編譯器對Iterator一無所知,換句話說:編譯器此時並不知道Iterator::iterator_category是什麼,關鍵詞typename的用意在於告訴編譯器這是一個型別,這樣才能順利通過編譯)

我們看到,iterator_traits就是一個struct定義的一個模板結構體類型別,模板引數Iterator為一個迭代器型別。在iterator_traits內部,它將迭代器的五種內嵌型別全部重定義了一番。就是當我們訪問iterator_traits的五種型別時,就是訪問對應模板引數Iterator的五種型別。五種迭代器的型別實現看上文程式碼。原生指標T*和const原生指標const T*也算是兩種迭代器,我們要對這兩個指標進行特殊的偏特化處理
/////////////////////////////////////////////////////////////////
//這兩個指標我們認為是RandomAccessIterator型別
//////////////////////////////////////////////////////////////////
//T*
template <class T>
struct iterator_traits<T*> {
  typedef random_access_iterator_tag iterator_category;
  typedef T                          value_type;
  typedef ptrdiff_t                  difference_type;
  typedef T*                         pointer;
  typedef T&                         reference;
};
//////////////////////////////////////////////////////////////////
//const T*
template <class T>
struct iterator_traits<const T*> {
  typedef random_access_iterator_tag iterator_category;
  typedef T                          value_type;
  typedef ptrdiff_t                  difference_type;
  typedef const T*                   pointer;
  typedef const T&                   reference;
};

得到迭代器的型別後我們就可以利用函式過載實現不同版本的__advance就可以了。這時就用到我們上面說的繼承關係了。
我們先看一個例子:
#include<iostream>
using namespace std;
struct A{};
struct B :public A{};
struct C :public B{};
template<class T>
void show(T& p,A)
{
          cout << "A" << endl;
}
int main()
{
          int *p;
          show(p,A());
          show(p,B());
          show(p,C());
          system("pause");
          return 0;
}

A,B,C有如圖的繼承關係:

程式碼的執行結果如圖所示:


我們可以看到,雖然我們並沒有過載實現B,C的show函式,但是因為B繼承自A,它自動“傳遞呼叫”了A的show函式。他正好符合我們將要寫的__advance的過載。我們正好可以將A,B對應到我們的迭代器型別。



(OutputIterator只寫,不能移動)

所以__advance過載函式我們一共有以下三個版本:
template<class InputIterator,class Distance>
inline void __advancce(InputIterator &i, Distance, InputIteratorTag)
{
          while (n--)
          {
                   ++i;
          }
}
template<class BidirectionIterator,class Distance>
inline void __advance(BidirectionIterator &i, Distance n, BidirectionalIteratorTag)
{
          if (n >= 0)
          {
                   while (n--)//如果是前置--就會少走一步
                   {
                             ++i;
                   }
          }
          else
          {
                   while (n++)
                   {
                             --i;
                   }
          }
}
template<class RandomAccessIterator,class Disance>
inline void __advance(RandomAccessIterator& i, Distance n, RandomAccessIteratorTag)
{
          i += n;
}
當我們傳入的模板引數是ForwardIterator時,它會自己呼叫InputIterator的過載函式。

advance的Traits呼叫過程圖:



同理,迭代器的Distance函式(求兩個迭代器之間的距離)也是如此實現。下面是實現程式碼,大家可以自己推導一下:
根據繼承屬性,__distance實現兩個版本就可以了,為什麼?
template<class InputIterator>
inline typename IteratorTraits<InputIterator>::DifferenceType
void Distance(InputIterator first, InputIterator last)
{
          return __Distance(first, last, IteratorTraits<InputIterator>::IteratorCategory());
}
template<class RandomAccessIterator>
inline typename IteratorTraits<RandomAccessIterator>::DifferenceType
void __Distance(RandomAccessIterator first, RandomAccessIterator last, RandomAccessIteratorTag)
{
          return last - first;
}
template<class InputIterator>
inline typename IteratorTraits<InputIterator>::DifferenceType
void __Distance(InputIterator first, InputIterator last, InputIteratorTag)
{
          int n = 0;
          while (first != last)
          {
                   ++first;
          }
          return n;
}


總結
設計什麼樣的型別時迭代器的責任,設計什麼樣的迭代器是容器的責任。只有容器本身才知道該設計什麼樣的迭代器來維護自己。Traits大量用於STL實現品中,它裡用了內嵌型別和編譯器的模板template引數推導功能,增強了C++關於型別認證方面的能力,彌補了C++不為強型別語言的遺憾。Traits程式設計使我們必不可少的一門程式設計技巧。







相關文章