1. 程式人生 > >Effective STL:04迭代器

Effective STL:04迭代器

26:iterator優先於const_iterator、reverse_iterator 以及 const_reverse_iterator(before C++11)

STL中的所有標準容器都提供了4種迭代器型別。iterator的作用相當於T*,而const_iterator則相當於const T*;對一個iterator或者const_iterator進行遞增則可以移動到容器中的下一個元素,通過這種方式可以從容器的頭部一直遍歷到尾部;reverse_iterator與const_reverse_iterator同樣分別對應於T*和const T*,所不同的是,對這兩個迭代器進行遞增的效果是由容器的尾部反向遍歷到容器頭部。

在C++11之前,容器的成員函式如insert、erase等僅接受iterator型別的引數,而不是const_iterator、reverse_iterator 以及 const_reverse_iterator,而C++11之後,這些函式就只接受const_iterator了。下面是這幾種迭代器之間的轉換關係:

 

從iterator到const_iterator之間,或者從iterator到reverse_iterator之間,或者從 reverse_iterator到const_reverse_iterator之間都存在隱式轉換。並且,通過呼叫 reverse_iterator的 base 成員函式,可以將reverse_iterator轉換為 iterator。類似地,const_reverse_iterator也可以通過base成員函式被轉換為const_iterator。然而,通過base得到的迭代器也許並非你所期待的迭代器,條款28中詳細討論這一點。

從圖中還可以看出,沒有辦法從const_iterator轉換得到iterator,也無法從const_reverse_iterator得到reverse_iterator。

為什麼應該儘可能使用iterator,而避免使用const或者reverse型的迭代器,主要有下面的原因:

有些版本的insert和erase函式要求使用iterator,const和reverse型的迭代器不能滿足這些函式的要求。

要想隱式地將一個const_iterator轉換成iterator是不可能的;

從reverse_iterator轉換而來的iterator在使用之前可能需要相應的調整,條款28討論為什麼需要調整以及何時進行調整;

 

由此得出結論,應儘量使用iterator而不是const或reverse型的迭代器,可以使容器的使用更為簡單而有效,並且可以避免潛在的問題。但是該條款在C++11之後就不合適了。

 

 

27:使用 distance 和 advance 將容器的 const_iterator 轉換成iterator

下面的程式碼試圖把一個 const_iterator 強制轉換成iterator:

typedef deque<int> IntDeque; 
typedef lntDeque::iterator Iter;
typedef lntDeque::const_iterator Constlter;
Constlter ci; // ci is a const_iterator
…
Iter i(ci); // error! no implicit conversion from const_iterator to iterator
Iter i(const_cast<lter>(ci)); // still an error! can't cast a const_iterator to an iterator

 這裡只是以deque為例,但是用其他容器類(list、set、multiset、map、multimap)得到的結果也是一樣的。也許在vector或string類的情形下,強制轉換的程式碼行能夠通過編譯,但這是非常特殊的情形,我們稍後再考慮。

包含顯式型別轉換的程式碼不能通過編譯的原因在於,對於這些容器型別,iterator和 const_iterator是完全不同的類。試圖將一種型別轉換為另一種型別是毫無意義的,這就是const_cast轉換被拒絕的原因。reinterpret_cast、static_cast甚至C語言風格的型別轉換也不能勝任。

不過,對於vector和string容器來說,以上包含const_cast的程式碼也許能夠通過編譯。 因為通常情況下,大多數STL實現都會利用指標作為vector和string容器的迭代器。對於這樣的實現而言,vector<T>::iterator和vector<T>::const_iterator分別被定義為T*和const T*, string::iterator和string::const_iterator則分別被定義為 char*和 const char*。因此,對於這樣的實現,從const_iterator到iterator的const_cast轉換被最終解釋成從const T*到T* 的轉換,因而可以通過編譯。然而,即便在這樣的STL實現中,reverse_iterator和 const_reverse_iterator仍然是真正的類,所以不能直接將const_reverse_iterator通過const_cast強制轉換成reverse_iterator。

如果你有一個const_iterator並且可以訪問它所在的容器,那麼這裡有一條安全的、可移植的途徑能得到對應的iterator,而且用不著涉及型別系統的強制轉換。下面是這種方案的本質:

typedef deque<int> IntDeque; //as before
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator Constlter;

IntDeque d;
Constlter ci;
… // make ci point into d
Iter i(d.begin()); // initialize i to d.begin()
advance(i, distance<ConstIter>(i, ci)) //move i up to where ci is

 首先建立一個新的iterator,將它指向容器的起始位置,然後取得const_iterator 距離容器起始位置的偏移量,並將iterator向前移動相同的偏移量即可。這項任務是通過<iterator>中宣告的兩個函式模板來實現的:distance用以取得兩個迭代器之間的距離;advance則用於將一個迭代器移動指定的距離。如果i和ci指向同一個容器,則表示式advance(i, distance<ConstIter>(i, ci))會使i和ci指向容器中相同的位置。

呼叫distance時之所以要指定<ConstIter>,是因為i和ci的型別不相同,如果不指定,會導致模板實參推導失敗,下面是distance的宣告:

template<typename InputIterator〉
typename iterator_traits<InputIterator>::difference_type distance(Inputlterator first, Inputlterator last);

 這種實現的效率取決於你所使用的迭代器。對於隨機訪問的迭代器(如vector、string和deque產生的迭代器)而言,它是一個常數時間的操作;對於雙向迭代器(所有其他標準容器的迭代器,以及某些雜湊容器實現的迭代器)而言,它是一個線性時間的操作。

實際上,你應該重新審視你的設計,是否真的需要從const_iterator到iterator的轉換呢?

 

 

28:正確理解由reverse_iterator的base()成員函式所產生的iterator的用法

呼叫reverse_iterator的base成員函式可以得到與之相對應的iterator,但是該iterator和reverse_iterator實際上指向不同的位置,在reverse_iterator與對應的base()產生的iterator之間存在偏移,這段偏移也正好勾畫出了rbegin和rend()與對應的begin()和end()之間的偏移。

如果希望在reverse_iterator指定的位置上插入或刪除元素,因為insert或erase函式不接受reverse_iterator作為引數,只能轉換為iterator才行。這就需要根據實際情況,考慮到插入和刪除的具體位置,在對reverse_iterator.base得到的iterator做調整。比如如果要刪除reverse_iterator指向的位置,則將其轉換為iterator之後,應該是刪除該iterator前面的元素,而非iterator指向的元素。

 

 

29:對於逐個字元的輸入請考慮使用istreambuf_iterator

假如想把一個文字檔案的內容拷貝到一個string物件中,以下的程式碼看上去是一種合理的解決方案: 

ifstream inputFile("interestingData•txt");
string fileData((istream_iterator<char>(inputFile)), istream_iterator<char>()); 

 但是這段程式碼並沒有把檔案中的空白字元拷貝到string物件中。因為istream_iterator使用operator>>函式來完成實際的讀操作,而預設情況下operator>>函式會跳過空白字元。

如果要保留空白字元,那麼所需要做的工作是改寫這種預設行為,只要清除輸入流的skipws標誌即可:

ifstream inputFile("interestingData.txt");
inputFile.unsetf(ios::skipws); 
string fileData((istream_iterator<char>(inputFile)), istream_iterator<char>());

 現在,inputFile中的所有字元都會被拷貝到fileData中。然而,你可能會發現整個拷貝過程遠不及你希望的那般快。istream_iterator內部使用的 operator>>函式實際上執行了格式化的輸入,這意味著你每呼叫一次operator>>操作符,它都要執行許多附加的操作。有一種更為有效的途徑,那就是使用istreambuf_iterator。

istreambuf_iterator的使用方法與istream_iterator大致相同,但是istream_iterator物件使用 operator>>從輸入流中讀取單個字元,而istreambuf_iterator<char>則直接從流的緩衝區中讀取下一個字元。