c++容器使用經驗總結
第1章 容器
第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庫會把你從手工同步控制中解脫出來,而且你不能依賴於任何執行緒支援。
第2章 vector和string
第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容器的所有要求是不可能的。由於種種原因,他們失敗了的嘗試被遺留在標準中。
第3章 關聯容器
第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。
第5章 演算法
第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核心的問題,待解)
第6章 函式子、函式子類、函式及其他
第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。
第7章 在程式中使用STL
第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相關的編譯器診斷資訊。