C++標準模板庫(STL)迭代器的原理與實現

Sudouble發表於2020-12-09

引言

迭代器(iterator)是一種抽象的設計理念,通過迭代器可以在不瞭解容器內部原理的情況下遍歷容器。除此之外,STL中迭代器一個最重要的作用就是作為容器(vector,list等)與STL演算法的粘結劑,只要容器提供迭代器的介面,同一套演算法程式碼可以利用在完全不同的容器中,這是抽象思想的經典應用。

使用迭代器遍歷不同容器

如下所示的程式碼演示了迭代器是如何將容器和演算法結合在一起的,其中使用了三種不同的容器,.begin()和.end()方法返回一個指向容器第一個元素和一個指向容器最後一個元素後面一個位置的迭代器,也就是說begin()和end()返回的迭代器是前閉後開的一般用[begin,end)表示。對於不同的容器,我們都使用同一個accumulate函式,原因就在於accumulate函式的實現無需考慮容器的種類,只需要容器傳入的begin()和end() 迭代器能夠完成標準迭代器的要求即可。

    std::vector<int>vec{ 1, 2, 3 };
    std::list<int>lis{ 4, 5, 6 };
    std::deque<int>deq{ 7, 8, 9 };

    std::cout << std::accumulate(vec.begin(), vec.end(), 0) << std::endl \
        << std::accumulate(lis.begin(), lis.end(), 0) << std::endl \
        << std::accumulate(deq.begin(), deq.end(), 0) << std::endl;
        //6
        //15
        //24

迭代器的實現

迭代器的作用就是提供一個遍歷容器內部所有元素的介面,因此迭代器的內部必須儲存一個與容器相關聯的指標,然後過載各種運算操作來方便遍歷,其中最重要的就是<script type="math/tex" id="MathJax-Element-1">*</script>運算子和->運算子,以及++,–等可能需要的運算子過載。實際上這和C++標準庫中的智慧指標(smart pointer)很像,智慧指標也是將一個指標封裝,然後通過引用計數或是其它方法完成自動釋放記憶體的功能,為了達到和原有指標一樣的功能,也需要對*,->等運算子進行過載,下面參照智慧指標實現了一個簡單vector的迭代器,其中幾個typedef暫時不用管,我們後面會提到。vecIter主要作用就是包裹一個指標,不同容器內部資料結構不相同,因此迭代器操作符過載的實現也會不同。比如++操作符,對於線性分配記憶體的陣列來說,直接對指標執行++操作即可,但是如果容器是List就需要採用元素內部的方法,比如ptr->next()之類的方法訪問下一個元素。因此,STL容器都實現了自己的專屬迭代器。

template<class Item>
class vecIter{
    Item *ptr;
public:
    typedef std::forward_iterator_tag iterator_category;
    typedef Item value_type;
    typedef Item* pointer;
    typedef Item& reference;
    typedef std::ptrdiff_t difference_type;
public:
    vecIter(Item *p = 0) :ptr(p){}
    Item& operator*()const{
        return *ptr;
    }
    Item* operator->()const{
        return ptr;
    }
    //pre
    vecIter& operator++(){
        ++ptr;
        return *this;
    }
    vecIter operator++(int){
        vecIter tmp = *this;
        ++*this;
        return tmp;
    }

    bool operator==(const vecIter &iter){
        return ptr == iter.ptr;
    }
    bool operator!=(const vecIter &iter){
        return !(*this == iter);
    }

};
int main(){
int a[] = { 1, 2, 3, 4 };
    std::cout << std::accumulate(vecIter<int>(a), vecIter<int>(a + 4), 0);//輸出 10

}

迭代器的相應型別

我們都知道type_traits 可以萃取出型別的型別,根據不同型別可以執行不同的處理流程。那麼對於迭代器來說,是否有針對不同特性迭代器的優化方法呢?答案是肯定的。拿一個STL演算法庫中的distance函式來說,distance函式接受兩個迭代器引數,然後計算他們兩者之間的距離。顯然對於不同的迭代器計算效率差別很大。比如對於vector容器來說,由於記憶體是連續分配的,因此指標直接相減即可獲得兩者的距離;而list容器是鏈式表,記憶體一般都不是連續分配,因此只能通過一級一級呼叫next()或其他函式,每呼叫一次再判斷迭代器是否相等來計算距離。vector迭代器計算distance的效率為O(1),而list則為O(n),n為距離的大小。

因此,根據迭代器不同的特性,將迭代器分為5類:

  • Input Iterator:這種迭代器所指的物件為只讀的。
  • Ouput Iterator: 所指物件只能進行一次寫入操作。
  • Forward Iterator: 允許”讀寫型”演算法在迭代器區間內進行讀寫操作,比如說replace函式需要讀取區間內容,根據所讀內容決定是否寫入
  • Bidirectional Iterator : 可雙向移動。某些演算法需要反向遍歷某個迭代器區間
  • Random Access Iterator : 前四種迭代器只提供部分指標算數能力(前三種支援++運算子,後一種還支援–運算子),第五種則支援所有指標的算術運算,包括p + n,p - n,p[n],p1 - p2,p1 < p2

這五種迭代器的繼承關係如下所示。

這裡寫圖片描述

瞭解了迭代器的型別,我們就能解釋vector的迭代器和list迭代器的區別了。顯然vector的迭代器具有所有指標算術運算能力,而list由於是雙向連結串列,因此只有雙向讀寫但不能隨機訪問元素。故vector的迭代器種類為Random Access Iterator,list 的迭代器種類為Bidirectional Iterator。我們只需要根據不同的迭代器種類,利用traits程式設計技巧萃取出迭代器種類,然後由C++的過載機制就能夠對不同型別的迭代器採用不同的處理流程了。為此,對於每個迭代器都必須定義型別iterator_category,也就是上文程式碼中的typedef std::forward_iterator_tag iterator_category; 實際中可以直接繼承STL中定義的iterator模板,模板後三個引數都有預設值,因此繼承時只需要指定前兩個模板引數即可。如下所示,STL定義了五個空型別作為迭代器的標籤。

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

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{};

利用迭代器種類更有效的實現distance函式

回到distance函式,有了前面的基礎,我們可以根據不同迭代器種類實現distance函式。

    template<class InputIterator>
    inline typename std::iterator_traits<InputIterator>::difference_type distance(InputIterator first, InputIterator last){
        return __distance(first, last, std::iterator_traits<InputIterator>::iterator_category());
    }
    template<class InputIterator>
    inline typename std::iterator_traits<InputIterator>::difference_type __distance(InputIterator first, InputIterator last, std::input_iterator_tag){
            std::iterator_traits<InputIterator>::difference_type n = 0;
            while (first != last){
                ++first; ++n;
            }
            return n;
        }

    template<class InputIterator>
    inline typename std::iterator_traits<InputIterator>::difference_type \
        __distance(InputIterator first, InputIterator last, std::random_access_iterator_tag){
            return last - first;
        }


int main(){
int a[] = { 1, 2, 3, 4 };
    std::vector<int> vec{ 1, 2, 3, 4 };
    std::list<int> lis{ 1, 2, 3, 4 };
    std::cout<<"vec distance:"<<WT::distance(vec.begin(), vec.end())<<std::endl;
    std::cout << "list distance:" << WT::distance(lis.begin(), lis.end())<<std::endl;
    std::cout << "c-array distance:" << WT::distance(a,a + sizeof(a) / sizeof(*a)) << std::endl;
        //輸出 vec distance:4
        //    list distance:4
        //    c-array distance:4
}

這裡通過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::pointer pointer;
        typedef typename Iterator::reference reference;
        typedef typename Iterator::difference_type difference_type;

    };
    template <class T>
    struct iterator_traits<T*>{
        typedef std::random_access_iterator_tag iterator_category;
        typedef T value_type;
        typedef T* pointer;
        typedef T& reference;
        typedef ptrdiff_t difference_type;
    };

    template <class T>
    struct iterator_traits<const T*>{
        typedef std::random_access_iterator_tag iterator_category;
        typedef T value_type;
        typedef const T* pointer;
        typedef const T& reference;
        typedef ptrdiff_t difference_type;
    };

小結

STL使用迭代器將演算法和容器結合,利用迭代器型別可以針對不同迭代器編寫更加高效的演算法,這裡一點很重要的思想就是:利用C++過載機制和引數推導機制將執行期決議問題提前到編譯期決議,也就是說,我們不需要在執行時判斷迭代器的型別,而是在編譯期就已經決定。這很符合C++模板程式設計的理念。在後續的STL學習中,我們會實現自己的各種容器,也必須實現各種各樣的迭代器,因此迭代器的學習還遠沒有停止。

相關文章