C++STL容器使用經驗總結

轉自:http://blog.csdn.net/zhuxianjianqi/article/details/7441144

第1條:慎重選擇容器類型。

標準STL序列容器:vector、string、deque和list。

標準STL關聯容器:set、multiset、map和multimap。

非標準序列容器slist和rope。slist是一個單向鏈表,rope本質上是一“重型”string。

非標準的關聯容器hash_set、hase_multiset、hash_map和hash_multimap。

vector<char> 作爲string的替代。(見第13條)

vector作爲標準關聯容器的替代。(見第23條)

幾種標準的非STL容器,包括數組、bitset、valarray、stack、queue和priority_queue。

你是否關心容器中的元素是如何排序的?如果不關心,選擇哈希容器.

容器中數據的佈局是否需要和C兼容?如果需要兼容,就只能選擇vector。(見第16條)

元素的查找速度是否是關鍵的考慮因素?如果是,就要考慮哈希容器、排序的vector和標準關聯容器-或許這就是優先順序。

對插入和刪除操作,你需要事務語義嗎?如果是,只能選擇list。因爲在標準容器中,只有list對多個元素的插入操作提供了事務語義。

deque是唯一的、迭代器可能會變爲無效(插入操作僅在容器末尾發生時,deque的迭代器可能會變爲無效)而指向數據的指針和引用依然有效的標準STL容器。

第2條:不要試圖編寫獨立於容器類型的代碼。

如果你想編寫對大多數的容器都適用的代碼,你只能使用它們的功能的交集。不同的容器是不同的,它們有非常明顯的優缺點。它們並不是被設計用來交換使用的。

  你無法編寫獨立於容器的代碼,但是,它們(指客戶代碼)可能可以。

第3條:確保容器中的對象拷貝正確而高效。

copy in,copy out,是STL的工作方式,它總的設計思想是爲了避免不必要的拷貝。使拷貝動作高效並且防止剝離問題發生的一個簡單辦法是使容器包含指針而不是對象。

第4條:調用empty而不是檢查size()是否爲0。

  理由很簡單:empty對所有的標準容器都是常數時間操作,而對一些list的實現,size耗費線性時間。

第5條:區間成員函數優先於與之對應的單元素成員函數。

區間成員函數寫起來更容易,更能清楚地表達你的意圖,而且它們表現出了更高的效率。

第6條:當心C++編譯器最煩人的分析機制。

把形參加括號是合法的,把整個形參的聲明(包括數據類型和形參名字)用括號括起來是非法的。

第7條:如果容器中包含了通過new操作創建的指針,切記在容器對象析構前將指針delete掉。

STL很智能,但沒有智能到知道是否該刪除自己所包含的指針所指向的對象的程度。爲了避免資源泄漏,你必須在容器被析構前手工刪除其中的每個指針,或使用引用計數形式的智能指針(比如Boost的sharedprt)代替指針。

第8條:切勿創建包含auto_ptr的容器對象。

拷貝一個auto_ptr意味着改變它的值。例如對一個包含auto_ptr的vector調用sort排序,結果是vector的幾個元素被置爲NULL而相應的元素被刪除了。

第9條:慎重選擇刪除元素的方法。

要刪除容器中指定值的所有對象:

如果容器是vector、string或deque,則使用erase-remove習慣用法。

SeqContainer<int> c;

c.erase(remove(c.begin(),c.end(),1963),c.end());

如果容器是list,則使用list::remove。

如果容器是一個標準關聯容器,則使用它的erase成員函數。

要刪除容器中滿足特定條件的所有對象:

如果容器是vector、string或deque,則使用erase-remove_if習慣用法。

如果容器是list,則使用list::remove_if。

如果容器是一個標準關聯容器,則使用remove_copy_if和swap,或者寫一個循環遍歷容器的元素,記住當把迭代器傳給erase時,要對它進行後綴遞增。

AssocCOntainer<int> c;

...

AssocContainer<int> goodValues;

remove_copy_if(c.begin(), c.end(), inserter(goodValues, goodValues.end()),badValue);

c.swap(goodValues);

for(AssocContainer<int>::iterator i = c.begin();i !=c.end();/* do nothing */){

if(badValue(*i)) c.erase(i++);

else ++i;

}

要在循環內部做某些(除了刪除對象之外的)操作:

如果容器是一個標準序列容器,則寫一個循環來遍歷容器中的元素,記住每次掉用erase時,要用它的返回值更新迭代器。

如果容器是一個標準關聯容器,則寫一個循環來遍歷容器中的元素,記住每次把迭代器傳給erase時,要對迭代器做後綴遞增。

10條:瞭解分配子(allocator)的約定和限制。

11條:理解自定義分配子的合理用法。

第12條:切勿對STL容器的線程安全性有不切實際的依賴。

對一個STL實現你最多隻能期望:

多個線程讀是安全的。

多個線程對不同的容器寫入操作是安全的。

你不能期望STL庫會把你從手工同步控制中解脫出來,而且你不能依賴於任何線程支持。


第13條:vector和string優先於動態分配的數組。

如果用new,意味着你要確保後面進行了delete。

如果你所使用的string是以引用計數來實現的,而你又運行在多線程環境中,並認爲string的引用計數實現會影響效率,那麼你至少有三種可行的選擇,而且,沒有一種選擇是捨棄STL。首先,檢查你的庫實現,看看是否可以禁用引用計數,通常是通過改變某個預處理變量的值。其次,尋找或開發一個不使用引用計數的string實現。第三,考慮使用vector<char>而不是string。vector的實現不允許使用引用計數,所以不會發生隱藏的多線程性能問題。

第14條:使用reserve來避免不必要的重新分配。

通常有兩種方式來使用reserve以避免不必要的重新分配。第一種方式是,若能確切知道或大致預計容器中最終會有多少個元素,則此時可使用reserve。第二種方式是,先預留足夠大的空間,然後,當把所有的數據都加入後,再去除多餘的容量。

第15條:注意string實現的多樣性。

如果你想有效的使用STL,那麼你需要知道string實現的多樣性,尤其是當你編寫的代碼必須要在不同的STL平臺上運行而你又面臨着嚴格的性能要求的時候。

第16條:瞭解如何把vector和string數據傳給舊的API。

如果你有個vector v,而你需要得到一個只想v中的數據的指針,從而可把數據作爲數組來對才,那麼只需要使用&v[0]就可以了,也可以用&*v.begin(),但是不好理解。對於string s,隨應的形式是s.c_str()。

如果想用來自C API的數據來初始化一個vector,那麼你可以利用vector和數組的內存佈局兼容性,先把數據寫入到vector中,然後把數據拷貝到期望最終寫入的STL容器中。

第17條:使用“swap技巧”出去多餘的容量。

vector<Contestant>(contestants).swap(contestants);

表達式vector<Contestant>(contestants)創建一個臨時的矢量,它是contestants的拷貝:這是由 vector的拷貝構造函數來完成的。然而,vector的拷貝構造函數只爲所拷貝的元素分配所需要的的內存,所以這個臨時矢量沒有多餘的容量。然後我們把臨時矢量中的數據和contestants中的數據作swap操作,在這之後,contestants具有了被去除之後的容量,即原先臨時變量的容量,而臨時變量的容量則變成了原先contestants臃腫的容量。到這時,臨時矢量被析構,從而釋放了先前爲contestants所佔據的內存。

同樣的技巧對string也實用:

string s;

...

string(s).swap(s);

第18條:避免使用vector<bool>。

作爲STL容器,vector<bool>只有兩點不對。首先,它不是一個STL容器;其次,它並不存儲bool。除此以外,一切正常。因此最好不要使用它,你可以用deque<bool>和bitset替代。vector<bool>來自一個雄心勃勃的試驗,代理對象在C++軟件開發中經常會很有用。C++標準委員會的人很清楚這一點,所以他們決定開發vector<bool>,以演示STL如果支持 “通過代理對象來存取其元素的的容器”。他們說,C++標準中有了這個例子,於是,人們在實現自己的基於代理的容器時就有了一個參考。然而他們卻發現,要創建一個基於代理的容器,同時又要求它滿足STL容器的所有要求是不可能的。由於種種原因,他們失敗了的嘗試被遺留在標準中。


第19條:理解相等(equality)和等價(equivalence)的區別。

標準關聯容器總是保持排列順序的,所以每個容器必須有一個比較函數(默認爲less)。等價的定義正是通過該比較函數而確定的。相等一定等價,等價不一定相等。

第20條:爲包含指針的關聯容器指定比較類型。

每當你創建包含指針的關聯容器時,容器將會按照指針的值(就是內存地址)進行排序,絕大多數情況下,這不是你所希望的。

第21條:總是讓比較函數在等值情況下返回false。

現在我給你演示一個很酷的現象。創建一個set,用less_equal作爲它的比較類型,然後把10插入到該集合中:

set<int, less_equal<int> > s; //s 用"<=" 來排序

s.insert(10);

s.insert(10);

對於第二個insert,集合會檢查下面的表達式是否爲真:

!(10a <= 10b) && !(10b <= 10a); //檢查10a和10b是否等價,結果是!(true) && !(true) 爲false

結果集合中有兩個10!

從技術上講,用於對關聯容器排序的比較函數必須爲他們所比較的對象定義個“嚴格的弱序化”(strict weak ordering)。

第22條:切勿直接修改set或multiset中的鍵。

如果你不關心可移植性,而你想改變set或multiset中元素的值,並且你的STL實現(有的STL實現中,比如set<T>:: iterator 的operator*總是返回const T&,就不能修改了)允許你這麼做,則請繼續做下去。只是注意不要改變元素中的鍵部分,即元素中能夠影響容器有序性的部分。

如果你重視可移植性,就要確保set和multiset中的元素不能被修改。至少不能未經過強制類型轉換(轉換到一個引用類型const_cast<T&>)就修改。

如果你想以一種總是可行而且安全的方式來許該set、multiset、map和multimap中的元素,則可以分5個簡單步驟來進行:

1. 找到你想修改的容器的元素。如果你不能肯定最好的做法,第45條介紹瞭如何執行一次恰當的搜索來找到特定的元素。

2. 爲將要被修改的元素做一份拷貝,。在map和multimap的情況下,請記住,不要把該拷貝的第一個部分聲明爲const。畢竟,你想要改變它。

3. 修改該拷貝,使它具有你期望的值。

4. 把該元素從容器中刪除,通常是通過erase來進行的(見第9條)。

5. 把拷貝插到容器中去。如果按照容器的排列順序,新元素的位置可能與被刪除元素的位置相同或緊鄰,則使用“提示”(hint)形式的insert,以便把插入的效率從對數時間提高到常數時間。把你從第1步得來的迭代器作爲提示信息。

第23條:考慮用排序的vector替代關聯容器。

標準關聯容器通常被實現爲平衡的二叉查找樹。也就是說,它所適合的那些應用程序首先做一些插入操作,然後做查找,然後可能又插入一些元素,或許接着刪掉一些,隨後又做查找,等等。這一系列時間的主要特徵是插入、刪除和超找混在一起。總的來說,沒辦法預測出針對這顆樹的下一個操作是什麼。

很多應用程序使用其數據結構的方式並不這麼混亂。他們使用其數據結構的過程可以明顯地分爲三個階段,總結如下:

1. 設置階段。創建一個新的數據結構,並插入大量元素。在這個階段,幾乎所有的操作都是插入和刪除操作。很少或幾乎沒有查找操作。

2. 查找操作。查詢該數據結構以找到特定的信息。在這個階段,幾乎所有的操作都是查找操作,很少或幾乎沒有插入和刪除操作。

3. 重組階段。改變該數據結構的內容,或許是刪除所有的當前數據,再插入新的數據。在行爲上,這個階段與第1階段類似。當這個階段結束以後,應用程序又回到第2階段。

第24條:當效率至關重要時,請在map::operator[]與map::insert之間謹慎作出選擇。

假定我們有一個Widget類,它支持默認構造函數,並根據一個double值來構造和賦值:

class Widget{

public:

Widget();

Widget(double weight);

Widget& operator=(double weight);

...

}

map 的operator[]函數與衆不同。它與vector、deque和string的operator[]函數無關,與用於數組的內置operator []也沒有關係。相反,map::operator[]的實際目的是爲了提供“添加和更新”(add or update)的功能。也就是說,對於下面的例子:

map<int, Widget> m;

m[1] = 1.50;

語句m[1] = 1.50相當於

typedef map<int, Widget> IntWidgetMap;

pair<INtWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1.Widget()));//用鍵值1和默認構造的值創建一個新的map條目

result.first->second = 1.50;//調用賦值構造函數

我們最好把對operator[]的調用換成對insert的直接調用:

m.insert(IntWidgetMap::value_type(1,1.50));

這裏的效果和前面的代碼相同,只是它通常會節省三個函數調用:一個用於創建默認構造的臨時Widget對象,一個用於析構該臨時對象,另一個是調用Widget的賦值操作符。

請看一下做更新操作時我們的選擇:

m[k] = v; //使用operator[]把k的值更新爲v

m.insert(IntWidgetMap::value_type(k,v)).first->second = v; //使用insert把k的值更新爲v

insert調用需要一個IntWidgetMap::value_type類型的對象,所以當我們調用insert時,我們必須構造和西溝一個該類型的對象。這要付出一個pair構造函數和一個pair析構函數的代價。而這又會導致對Widget的構造和析構動作,因爲pair<int, Widget>本身又包含了一個Widget對象。而operator[]不使用pair對象,所以它不會構造和析構pair或Widget。

如果要更新一個已有的映射表元素,選擇operator[];如果要添加一個新的元素,選擇insert。

第25條:熟悉非標準的哈希容器。

標準C++庫沒有任何哈希容器,每個人認爲這是一個遺憾,但是C++標準委員會認爲,把它們加入到標準中所需的工作會拖延標準完成的時間。已經有決定要在標準的下一個版本中包含哈希容器。

第4章 迭代器

第26條:iterator優先於const_iterator、reverse_iterator以及const_reverse_iterator。

減少混用不同類型的迭代器的機會,儘量用iterator代替const_iterator。從const正確性的角度來看,僅僅爲了避免一些可能存在的 STL實現缺陷而放棄使用const_iteraor顯得有欠公允。但考慮到在容器類的某些成員函數中指定使用iterator的現狀,得出 iterator較之const_iterator更爲實用的結論也就不足爲奇了。更何況,從實踐的角度來看,並不總是值得捲入 const_iterator的麻煩中。

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

下面的代碼試圖把一個const_iterator強制轉換爲iterator:

typedef deque<int> IntDeque; //類型定義,簡化代碼

typedef IntDeque::iterator Iter;

typedeef IntDeque:;const_iterator ConstIter;

ConstIter ci; //ci 是一個const_iterator

...

Iter i(ci); //編譯錯誤!從const_iterator 到 iterator沒有隱式轉換途徑

Iter i(const_cast<Iter>(ci)); //仍然是編譯錯誤!不能將const_iterator強制轉換爲iterator

包含顯式類型轉換的代碼不能通過編譯的原因在於,對於這些容器類型,iterator和const_iterator是完全不同的類,他們之間的關係甚至比string和complex<double>之間的關係還要遠。

下面是這種方案的本質。

typedef deque<int> IntDeque; //類型定義,簡化代碼

typedef IntDeque::iterator Iter;

typedeef IntDeque:;const_iterator ConstIter;

IntDeque d;

ConstIter ci; //ci 是一個const_iterator

... //使ci指向d

Iter i(d.begin());//使i指向d的起始位置

advance(i,distance<ConstIter>(i,ci));//移動i,使它指向ci所指的位置

這中方法看上去非常簡單和直接,也很令人吃驚。爲了得到一個與const_iterator指向同一位置的iterator,首先創建一個新的 iterator,將它指向容器的起始位置,然後取得const_iterator距離容器起始位置的偏移量,並將iterator向前移動相同的偏移量即可。這項技術的效率取決於你所使用的迭代起,對於隨機迭代器,它是常數時間的操作;對於雙向迭代器,以及某些哈希容器,它是線性時間的操作。

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

如果要在一個reverse_iterator ri指定的位置上插入元素,則只需在ri.base()位置處插入元素即可。對於插入操作而言,ri和ri.base()是等價的,ri.base()是真正與ri對應的iterator。

如果要在一個reverse_iterator ri指定的位置上刪除一個元素,則需要在ri.base()前一個位置上執行刪除操作。對於刪除操作而言,ri和ri.base()是不等價的。

我們還是有必要來看一看執行這樣一個刪除操作的實際代碼,其中暗藏着驚奇之處:

vector<int> v;

... //同上,插入1到5

vector<int>::reverse_iterator ri = find(v.rbegin(),v.rend(),3);//使ri指向3

v.erase(--ri.base()); //試圖刪除ri.base()前面的元素,對於vector,往往編譯通不過

對於vector和string,這段代碼也許能工作,但對於vector和string的許多實現,它無法通過編譯。這是因爲在這樣的實現中, iterator(和vconst_iterator)是以內置指針的方式實現的,所以ri.base()的結果是一個指針。C和C++都規定了從函數返回的指針不應該被修改,所以所以編譯不能通過。

既然不能對base()的結果做遞減操作,那麼只要先遞增reverse_iterator,然後再調用base()函數即可!

...

v.erase((++ri).base()); //刪除ri所指的元素,這下編譯沒問題了!

第29條:對於逐個字符的輸入請考慮使用istreambuf_iterator。

假如你想把一個文本文件的內容拷貝到一個string對象中,以下的代碼看上去是一種合理的解決方案:

ifstream inputFile("interestingData.txt");

inputFIle.unsetf(ios::skipws);//istream_iterator使用operator>>函數來完成實際的讀操作,而默認情況下operator>>函數會跳過空白字符

string fileData((istream_iterator<char> (inputFIle)),istream_iterator<char>());

然而,你可能會發現整個拷貝過程遠不及你希望的那般快。istream_iterator內部使用的operator>>實際上執行了格式化的輸入,但如果你只是想從輸入流中讀出下一個字符的話,它就顯得有點多餘了。

有一種更爲有效的途徑,那就是使用STL中最爲神祕的法寶之一:istreambuf_iterator。 istreambuf_iterator<char>對象使用方法與istream_iterator<char>大致相同,但是istreambuf_iterator<char>直接從流的緩衝區讀取下一個字符。(更爲特殊的是, istreambuf_iterator<char>對象從一個輸入流istream s中讀取下一個字符的操作是通過s.rdbuf()->sgetc()來完成的。)

ifstream inputFile("interestingData.txt");

string fileData((istreambuf_iterator<char>(inputFile)),istreambuf_iterator<char>());

這次我們用不着清楚輸入流的skipws標誌,因爲istreambuf_iterator不會跳過任何字符。

同樣的,對於非格式化的逐個字符輸出過程,你也應該考慮使用ostreambuf_iterator。


第30條:確保目標區間足夠大。

當程序員希望向容器中添加新的對象,這裏有一個例子:

int transmogrify(int x); //該函數根據x生成一個新的值

vector<int> values;

vector<int> results;

transform(values.begin(),values.end(),back_inserter(results),transmogrify);

back_inserter返回的迭代起將使得push_back被調用,所以back_inserter可適用於所有提供了push_back方法的容器。同理,front_inserter僅適用於那些提供了push_front成員函數的容器(如deque和list)。

當是使用reserver提高一個序列插入操作的效率的時候,切記reserve只是增加了容器的容量,而容器的大小並未改變。當一個算法需要向 vector或者string中加入新的元素,即使已經調用了reserve,你也必須使用插入型的迭代器。如下代碼給出了一種錯誤的方式:

vector<int> values;

vector<int> results;

...

results.reserve(results.size() + values.size());

transform(values.begin(), values.end(), results.end(), transmogrify);//變換的結果會寫入到尚未初始化的內存,結果將是不確定的

在以上代碼中transform欣然接受了在results尾部未初始化的內存中進行復制操作的任務。由於賦值操作重視在兩個對象之間而不是在一個對象與一個未初始化的內存塊之間進行,所以一般情況下,這段代碼在運行時會失敗。

假設希望transform覆蓋results容器中已有的元素,那麼就需要確保results中已有的元素至少和values中的元素一樣多。否則,就必須使用resize來保證這一點。

vector<int> values;

vector<int> results;

...

if(results.size() < values.size()){

results.resize(values.size());

}

transform(values.begin(),values.end(),results.begin(),transmogrify);

或者,也可以先清空results,然後按通常的方式使用一個插入型迭代起:

...

results.clear();

results.reserve(values.size());

transform(values.begin(),values.end(),back_inserter(results),transmogrify);

第31條:瞭解各種與排序有關的選擇。

sort(stable_sort)、partial_sort和nth_element算法都要求隨即訪問迭代器,所以這些算法只能被應用於 vector、string、deque和數組。partion(stable_partion)只要求雙向迭代器就能完成工作。對於標準關聯容器中的元素進行排序並沒有實際意義,因爲它們總是使用比較函數來維護內部元素的有效性。list是唯一需要排序卻無法使用這些排序算法的容器,爲此,list特別提供了sort成員函數(有趣的是,list::sort執行的是穩定排序)。如果希望希望一個list進行完全排序,可以用sort成員函數;但是,如果需要對list使用partial_sort或者nth_element算法的話,你就只能通過間接途徑來完成了。一種間接做法是,將list中的元素拷貝到一個提供隨即訪問迭代器的容器中,然後對該容器執行你所期望的算法;另一種簡介做法是,先創建一個list::iterator的容器,再對該容器執行相應的算法,然後通過其中的迭代器訪問list的元素;第三中方法是利用一個包含迭代器的有序容器的信息,通過反覆地調用splice成員函數,將 list中的元素調整到期望的目標位置。可以看到,你會有很多中選擇。

第32條:如果確實需要刪除元素,則需要在remove這一類算法之後調用erase。

1 2 3 99 5 99 7 8 9 99

調用remove(v.begin(),v.end(),99);後變成

1 2 3 5 7 8 9 8 9 99

remove無法從迭代器推知對應的容器類型,所以就無法調用容器的成員函數erase,因此就無法真正刪除元素。其他兩個算法remove_if和 unique也類似。不過list::remove和list::unique會真正刪除元素(比用erase-remove和erase-unique 更爲高效),這是STL中一個不一致的地方。

第33條:對包含指針的容器使用remove這一類算法時要特別小心。

無論你如何處理那些存放動態分配的指針的容器,你總是可以這樣來進行:或者調用remove類算法之前先手工刪除指針並將它們置爲空,或者通過引用計數的智能指針( 如boost::shared_ptr),或者你自己發明的其他某項技術。

下面的代碼利用第一種方式:

void delAndNullifyUncertified(Widget*& pWidget)

{

if(!pWidget->isCertified())

{

delete pWidget;

pWidget = 0;

}

}

for_each(v.begin(),v.end(),delAndNullifyUndertified);

v.erase(vemove(v.begin(),v.end(),static_cast<Widget*>(0)),v.end());

下面的的代碼使用第二中方式:

template<typename T> //RSCP = "Reference Counting Smart Pointer"

class RCSP{...};

tpedef RCSP<Widget> RCSPW;

vector<RCSPW> v;

...

v.push_back(RCSPW(new Widget));

...

v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());

第34條:瞭解哪些算法要求使用排序的區間作爲參數。

下面的代碼要求排序的區間:

binary_search lower_bound

upper_bound equal_range

set_union set_intersection

set_difference set_symmetric_difference

merge inplace_merge

includes

下面的算法並不一定需要排序的區間:

unique unique_copy

第35條:通過mismatch或lexicographical_compare實現簡單的忽略大小寫的字符串比較。

用mistatch實現:

//此函數判斷兩個字母是否相同,而忽略它們的大小寫

int ciCharCompare(char c1, char c2)

{

int lc1 = tolower(static_cast<unsigned_char>(c1));

int lc2 = tolower(static_cast<unsigned_char>(c2));

if(lc1 < lc2) return -1;

if(lc1 > lc2) return 1;

return 0;

}

/* 此函數保證傳遞給ciStringCompareImpl的s1比s2短,如果s1和s2相同,返回0;如果s1比s2短,返回-1;如果s1比s2長,返回1。*/

int ciStringCompare(const string& s1, const string& s2)

{

if(s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2);

else return – ciStringCompareImpl(s2, s1); 

}

//如果s1和s2相同,返回0;如果s1比s2短,返回-1;如果s1和s2都是在非結尾處發生不匹配,有開始不匹配的那個字符決定。

int ciStringCompareImpl(const string &s1, const string &c2)

{

typedef pair<string::const_iterator,string::const_iterator> PSCI;

PSCI p = mismatch(s1.begin(),s1.end(),s2.begin(),not2(ptr_fun(ciCharCompare)));

if(p.first == s1.end()){

if(p.second == s2.end()) return 0;

else return -1;

}

return ciCharCompair(*p.first, *p.second);

}

用lexicographical_compare實現:

bool ciCharLess(char c1, char c2)

{

return tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2));

}

bool ciStringCompare(const string &s1,const string &s2)

{

return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);

}

第36條:理解copy_if算法的正確實現。

STL中沒有copy_if的算法,下面是一個實現,但是不夠完美:

template<typename INputIterator,typename OUtputIterator,tpename Predicate>

OutputIterator copy_if(INputIterator begin,INputIterator end,OutputIterator destBegin,Predicate p)

{

return remove_copy_if(begin,end,destBegin, not1(0));

}

copy_if(widgets.begin(), widgets.end(), ostream_iterator<Widget>(cerr, "\n"),isDefective);//編譯錯誤

因爲not1不能被直接應用到一個函數指針上(見41條),函數指針必須先用ptr_fun進行轉換。爲了調用copy_if的這個實現,你傳入的不僅是一個函數對象,而且還應該是一個可配接(adaptable)的函數對象。雖然這很容易做到,但是要想成爲STL算法,它不能給客戶這樣的負擔。

下面是copy_if的正確實現:

template<typename INputIterator,typename OUtputIterator,typename Predicate>

OutputIterator copy_if(INputIterator begin,INputIterator end,OutputIterator destBegin,Predicate p)

{

while(begin != end){

if(p(*begin)) *destBegin++ = *begin;

++begin;

}

return destBegin;

}

第37條:使用accumulate或者for_each進行區間統計。

確保accumulate的返回類西和初始值類型相同。for_each返回的是一個函數對象。accumulate不允許副作用而for_each允許。(這是一個深層次的問題,也是一個涉及STL核心的問題,待解)


第38條:遵循按值傳遞的原則來設計函數子類。

在STL中,函數對象在函數之間來回傳遞的時候也是像函數指針那樣按值傳遞的。因此,你的函數對象必須儘可能的小,否則拷貝的開銷會很大;其次,函數對象必須是單態的,也就是說,它們不得使用虛函數。這是因爲,如果參數的類型是基類類型,而實參是派生類對象,那麼在傳遞過程中會產生剝離問題(slicing problem):在對象拷貝過程中,派生部分可能會被去掉,而僅保留了基類部分(見第3條)。

試圖禁止多態的函數子同樣也是不實際的。所以必須找到一種兩全其美的辦法,既允許函數對象可以很大並且/或保留多態性,又可以與STL所採用的按值傳遞函數子的習慣保持一致。這個辦法就是:將所需要的數據和虛函數從函數子中分離出來,放到一個新的類中,然後在函數子中設一個指針,指向這個新類。

第39條:確保判別式是“純函數”。

一個判別式(predicate)是一個返回值爲bool類型的函數。一個純函數(pure function)是指返回值僅僅依賴於其參數的函數。

因爲接受函數子的STL算法可能會先創建函數子對象的拷貝,然後使用這個拷貝,因此這一特性的直接反映就是判別式函數必須是純函數。

template<typename FwdIterator,typename Predicata>

FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p)

{

begin = find_if(begin, end, p);//可能是p的拷貝

if(begin == end return begin;

else{

FwdIterator next = begin;

return remove_copy_if(++next, end, begin, p);//可能是p的另一個拷貝

}

}

第40條:若一個類是函數子,則應使它可配接。

4個標準的函數配接器(not1、not2、bind1st和bind2nd)都要求一些特殊的類型定義。提供了這些必要的類型定義(argument_type、first_argument_type、second_argument_type以及result_type)的函數對象被稱爲可配接的(adaptable)函數對象,反之,如果函數對象缺少這些類型定義,則稱爲不可配接的。可配接的函數對象能夠與其他STL組件更爲默契地協同工作。不過不同種類的函數子類所需要提供的類型定義也不盡相同,除非你要編寫自定義的配接器,否則你並不需要知道有關這些類型定義的細節。這是因爲,提供這些類型定義最簡便的辦法是讓函數子從特定的基類繼承,或者更準確的說,如果函數子類的operator()只有一個形參,那麼它應該從 std::unary_function模板的一個實例繼承;如果函數子類的operator()有兩個形參,那麼它應該從std:: binary_function繼承。

對於unary_function,你必須指定函數子類operator()所帶的參數的類型,以及返回類型;對於binary_function,你必須指定三個類型:operator()的第一個和第二個參數的類型,以及operator()的返回類型。以下是兩個例子:

template<typename T>

class MeetsThreshold: public std::unary_function<Widget, bool> {

private:

const T threshold;

public:

MeetsThreshold(const T& threshold);

bool operator()(const Widget&) const;

...

};

struct WidgetNameCompare:

public std::binary_function<Widget, Widget, bool> {

bool operator() (const Widget& lhs, const Widget& rhs) const;

};

你可能已經注意到MeetsThreshold是一個類,而WidgetNameCompare是一個結構。這是因爲MeetsThreshold包含了狀態信息(數據成員threshold),而類是封裝狀態信息的一種邏輯方式;與此相反,WidgetNameCompare並不包含狀態信息,因而不需要任何私有成員。如果一個函數子的所有成員都是公有的,那麼通常會將其聲明爲結構而不是類。究竟是選擇結構還是類來定義函數子純屬個人編碼風格,但是如果你正在改進自己的編碼風格,並希望自己的風格更加專業一點的話,你就應該注意到,STL中所有無狀態的函數子類(如less<T>、 plus<T>等)一般都定義成結構。

我們在看一下WidgetNameCompare:

struct WidgetNameCompare:

public std::binary_function<Widget, Widget, bool> {

bool operator() (const Widget& lhs, const Widget& rhs) const;

};

雖然operator()的參數類型都是const Widget&,但我們傳遞給binary_function的類型卻是Widget。一般情況下,傳遞給unary_function或 binary_function的非指針類型需要去掉const和引用(&)部分(不要問其中的原因,如果你有興趣,可以訪問 boost.org,卡可能看他們在調用特性(traits)和函數對象配接器方面的工作)。

如果operator()帶有指針參數,規則又有不同了。下面是WidgetNameCOmpare函數子的另一個版本,所不同的是,這次以Widget*指針作爲參數:

struct PtrWidgetNameCompare:

public std::binary_function<const Widget*, const Widget*, bool> {

bool operator() (const Widget* lhs, const Widget* rhs) const;

};

第41條:理解ptr_fun、mem_fun和mem_fun_ref的來由。

如果有一個函數f和一個對象x,現在希望在x上調用f,而我們在x的成員函數之外,那麼爲了執行這個調用,C++提供了三種不同的語法:

f(x); //語法#1:f是一個非成員函數

x.f(); //語法#2:f是一個成員函數,並且x是一個對象或一個對象引用

p->f(); //語法#3:f是成員函數,並且p是一個指向對象x的指針

現在假設有個可用於測試Widget對象的函數:

void test(Widget& w);

另有一個存放Widget對象的容器:

vector<Widget> vw;

爲了測試vw中的每一個Widget對象,自然可以用如下的方式來調用for_each:

for_each(vw.begin(), vw.end(), test); //調用#1 (可以通過編譯)

但是,加入test是Widget的成員函數,即Widget支持自測:

class Widget{

public:

...

void test();

....

};

那麼在理想情況下,應該也可以用for_each在vw中的每個對象上調用Widget::test成員函數:

for_each(vw.begin(), vw.end(), &Widget::test);//調用#2(不能通過編譯)

實際上,如果真的很理想的話,那麼對於一個存放Widget* 指針的容器,應該也可以通過for_each來調用Widget::test:

list<Widget*> lpw;

for_each(lpw.begin(), lpw.end(), &Widget::test);//調用#3(也不能通過編譯)

這是因爲STL中一種和普遍的慣例:函數或函數對象在被調用的時候,總是使用非成員函數的語法形式(即#1)。

現在mem_fun和mem_fun_ref之所以必須存在已經很清楚了--它們被用來調整(一般是#2和#3)成員函數,使之能夠通過語法#1被調用。 mem_fun、mem_fun_ref的做法其實很簡單,只要看一看其中任意一個函數的聲明就清楚了。它們是真正的函數模板,針對它們所配接的成員函數的圓形的不同,有幾種變化形式。我們來看其中一個聲明,以便了解它是如何工作的:

template<typename R, typename C> //該mem_fun聲明針對不帶參數的非const成員函數,C是類,R是所指向的成員函數的返回類型

mem_fun_t<R,C>

mem_fun(R(C::*pmf) ());

mem_fun帶一個指向某個成員函數的指針參數pmf,並且返回一個mem_fun_t類型的對象。mem_fun_t是一個函數子類,它擁有該成員函數的指針,並提供了operator()函數,在operator()中調用了通過參數傳遞進來的對象上的該成員函數。例如,請看下面一段代碼:

list<Widget*> lpw;

...

for_each(lpw.begin(),lpw.end(),mem_fun(&Widget::test));//現在可以通過編譯了

for_each接受到一個類型爲mem_fun_t的對象,該對象中保存了一個指向Widget::test的指針。對於lpw中的每一個 Widget*指針,for_each將會使用語法#1來調用mem_fun_t對象,然後,該對象立即用語法#3調用Widget*指針的 Widget::test()。

(ptr_fun是多餘的嗎?)mem_fun是針對成員函數的配接器,mem_fun_ref是針對對象容器的配接器。

第42條:確保less<T>與operator<具有相同的含義。

operator<不僅僅是less的默認實現方式,它也是程序員期望less所做的事情。讓less不調用operator<而去坐別的事情,這會無端地違背程序員的意願,這與“少”帶給人驚奇的原則(the principle of least astonishment)完全背道而馳。這是很不好的,你應該儘量避免這樣做。

如果你希望以一種特殊的方式來排序對象,那麼最好創建一個特殊的函數子類,它的名字不能是less。


第43條:算法調用優於手寫的循環。

有三個理由:

效率:算法通常比程序員自己寫的循環效率更高。

STL實現者可以針對具體的容器對算法進行優化;幾乎所有的STL算法都使用了複雜的計算機科學算法,有些科學算法非常複雜,並非一般的C++程序員所能夠到達。

正確性:自己寫的循環比使用算法容易出錯。

比如迭代器可能會在插入元素後失效。

可維護性:使用算法的代碼通常比手寫循環的代碼更加簡介明瞭。

算法的名稱表明了它的功能,而for、while和do卻不能,每一位專業的C++程序員都應該知道每一個算法所做的事情,看到一個算法就可以知道這段代碼的功能,而對於循環只能繼續往下看具體的代碼才能懂代碼的意圖。

第44條:容器的成員函數優先於同名的算法。

第一:成員函數往往速度快;第二,成員函數通常與容器(特別是關聯容器)結合得更緊密(相等和等價的差別,比如對於關聯容器,count只能使用相等測試)。

第45條:正確區分count、find、binary_search、lower_bound、upper_bound和equal_range。

想知道什麼

使用算法

使用成員函數

對未排序的區間

對排序的區間

對set或map

對multiset或multimap

特定的值存在嗎

find

binary_search

count

find

特定的值存在嗎?如果有,第一個在哪裏

find

equal_range

find

find或lower_bound

第一個不超過特定值的對象在哪裏

find_if

lower_bound

lower_bound

lower_bound

第一個超過某個特定值的對象在哪裏

find_if

upper_bound

upper_bound

upper_bound

具有特定值的對象有多少個

count

equal_range (然後distance)

count

count

具有特定值的對象都在哪裏

find(反覆調用)

equal_range

equal_range

equal_range

第46條:考慮使用函數對象而不是函數指針作爲STL算法的參數。

  函數指針抑制了內聯機制,而函數對象可以被編譯器優化爲內聯。

  另一個理由是,這樣做有助於避免一些微妙的、語言本身的缺陷。在偶然的情況下,有些看似合理的代碼會被編譯器以一些合法但又含糊不清的理由而拒絕。例如,當一個函數模板的實例化名稱並不完全等同於一個函數的名稱時,就可能會出現這樣的問題。下面是一個例子:

template<typename FPType>

FPType average(FPType val1, FPType val2)//返回兩個浮點的平均值

{

return (val1 + val2) / 2;

}

template<typename InputIter1,typename InputIter2>

void writeAverages(InputIter1 begin1, INputIter1 end1, InputIter2 begin2,ostream& s) //將兩個序列的值按順序對應取平均,然後寫到一個流中

{

transform(begin1, end1, begin2,

ostream_iterator<typename iterator_trais<InputIterl>::value_type(s,"\n")>,

average<typename iterator_traits<InputIterl>::value_type> //錯誤?

);

}

許多編譯器接受這段代碼,但是C++標準卻不認同這樣的代碼。原因在於,理論上存在另一個名爲average的函數模板,它也只帶一個類型參數。如果這樣的話,表達式average<typename iterator_traits<InputIterl>::value_type>就會有二義性,因爲編譯器無法分辨到底應該實例化哪一個模板。換成函數對象就可以了。

第47條:避免產生“直寫型”(write-only)的代碼。

  代碼被閱讀的次數遠遠大於它被編寫的次數。

第48條:總是包含(#include)正確的頭文件。

幾乎所有的標準STL容器都被聲明在與之同名的頭文件中。

除了4個STL算法外,其他所有的算法都被聲明在<algorithm>中,這4個算法是accumulate、 inner_product、adjacent_difference和partial_sum,它們都被聲明在頭文件<numeric> 中。

特殊類型的迭代器,包括istream_iterator和istreambuf_iterator(見第29條),被聲明在<iterator>中。

標準的函數子(比如less<T>)和函數子配接器(比如not1、bind2nd)被聲明在頭文件<functional>中。

第49條:學會分析與STL相關的編譯器診斷信息。

  用文本替換(例如用string替換掉basic_string<char,struct std::char_traits<char>,class std::allocator<char> >)。

第50條:熟悉STL相關的Web站點。

SGI STL站點:http://www.sig.com/tech/stl/

STLport站點:http://stlport.org

BOost站點:http://boost.org


發佈了2 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章