1. 程式人生 > >《C++ Templates》(基礎部分)筆記整理

《C++ Templates》(基礎部分)筆記整理

函式模板
一.初探函式模板
   函式模板的宣告形式:
   template< comma-separated-list-of-parameters >//template< 用逗號隔開的引數列表>
   可以用class來替代typename,聰語義上講,二者等價。因此,即使在這裡使用class,你也可以用任何型別(前提是該型別提供模板使用的操作)來例項化模板引數。另外還應該注意,這種用法和型別宣告不同,也就是說,在宣告(引入)型別引數的時候,不能用關鍵字struct代替typename。
   只要使用函式模板,(編譯器)會自動地引發這樣一個例項化過程,因此程式設計師並不需要額外地請求模板的例項化。





注:模板被編譯了兩次,分別發生在
    1.例項化之前,先檢查模板程式碼本身,檢視語法是否正確;在這裡會發現錯誤的語法;
    2.在例項化期間,檢查模板程式碼,檢視是否所有的呼叫都有效。在這裡會發現無效的呼叫,如該例項化型別不支援某些函式呼叫等。


二.實參的演繹
   模板引數可以由我們所傳遞的實參來決定。
   有三種方法來處理型別不匹配的錯誤:
   1.對實參進行強制型別轉換,使它們可以互相匹配:
     max( static_cast<double>(4),4.2)
   2.顯示指定(或者限定)T的型別:
     max<double>(4,4.2)
   3.指定兩個引數可以具有不同的型別。


三.模板引數
   函式模板有兩種型別的引數:
   1.模板引數:位於函式模板名稱的前面,在一對尖括號內部進行宣告:
   template<typename T> //T是模板引數
   2.呼叫引數:位於函式模板名稱之後,在一對圓括號內部進行宣告:
   …max(T const& a,T const& b)//a和b都是呼叫引數
   因為呼叫引數的型別構造自模板引數,所以模板引數和呼叫引數通常是相關的。我們把這個概念稱為:函式模板的實參演譯。它讓你可以像呼叫普通函式那樣呼叫函式模板。

注:1.當模板引數和呼叫引數沒有發生關聯,或者不能由呼叫引數來決定模板引數的時候,你在呼叫時就必須顯式指定模板實參。必須顯示的指定模板實參列表。



四.過載函式模板
   當一個非模板函式和一個同名的函式模板同時存在,且該函式模板還可以被例項化為這個非模板函式。對於非模板函式和同名函式模板,如果其他條件都是相同的話,在呼叫時,過載解析過程通常會呼叫非模板函式,而不會從該模板產生出一個例項。模板是不允許自動型別轉化的;但普通函式型別可以進行自動型別轉換。




注:在所有過載的實現裡面,我們都是通過引用來傳遞每個實參的。




五.小結
   1.模板函式為不同的模板實參定義了一個函式家族;
   2.當你傳遞模板實參的時候,可以根據實參的型別來對函式模板進行例項化;
   3.可以顯式指定模板引數;
   4.可以過載函式模板;
   5.當過載函式模板的時候,其改變限制在:顯式的指定模板引數;
   6.一定要讓函式模板的所有過載版本的宣告都位於他們被呼叫的位置之前。


類模板
    與函式相似,類也可以被一種或多種型別引數化。
一.類模板Stack的實現
    類模板Stack<>是通過C++標準庫的類模板vector<>來實現的;因此不需要自己實現記憶體管理、拷貝建構函式和賦值運算子;可以將精力放在該類模板的介面實現上。
    類模板的宣告和函式模板的生命很相似;在宣告之前,先(用一條語句)宣告作為型別引數的識別符號。
    為定義類模板的成員函式,需指定該成員函式是一個函式模板,且還需要使用這個類模板的完整型別限定符。(Stack<T>::)。
    對於類模板的任何成員函式,你都可以把它實現為行內函數,將它定義於類聲明裡面。




注:需要在兩個靠在一起的模板尖括號(即>)之間留一個空格;否則,編譯器將會認為你是在使用operator>>,而這將會導致一個語法錯誤:
    Stack<Stack<int>> intStackStack; // ERROR:這裡不允許使用>>


二.類模板的特化
   可以用模板實參來特化類模板。通過特化類模板,可以優化及於某種特定型別的實現,或者克服某種特定型別在例項化類模板時所出現的不足。如果要特化一個類模板,你還要特化該類模板的所有成員函式。雖然也可以只特化某個成員函式,但這個做法並沒有特化整個類,也就沒有特化整個類模板。
   為了特化一個類模板,你必須在起始處宣告一個template<>,接下來宣告用來特化類模板的型別。這個型別被用作模板實參,且必須在類名的後面直接指定:
   template<>
   class Stack<std::string>{
   ……
   }
   進行類模板的特化時,每個成員函式都必須重新定義為普通函式,原來模板函式中的每個T也相應地被進行特化的型別取代:
   void Stack<std::string>::push(std::string const& elem)
   {
        elems.push_back(elem);//附加傳入實參elem的拷貝
   }




注:deque會釋放記憶體;當需重新分配記憶體時,deque的元素並不需要被移動。


三.區域性特化
   如果有多個區域性特化同等程度的匹配某個宣告,那麼就稱該宣告具有二義性。




四.預設模板實參
   對於類模板,你還可以為模板引數定義預設值;這些值就被稱為預設模板實參;且它們可以引用之前的模板引數。
   在類Stack<>中,可以把用於管理元素的容器定義為第二個模板引數,並且使用std::vector<>作為它的預設值;當在程式中宣告Stack物件的時候,你還可以指定容器的型別。




五.小結
  1.類模板是具有如下性質的類:在類的實現中,可以有一個或者多個型別還沒有被指定;
  2.為了使用類模板,你可以傳入某個具體型別作為模板實參;然後編譯器將會基於該型別來例項化類模板;
  3.對於類模板而言,只有那些被呼叫的成員函式才會被例項化;
  4.可以用某種特定型別特化類模板;
  5.可以用某種特定型別區域性特化類模板;
  6.可以為類模板的引數定義預設值,這些值還可以引用之前的模板引數。




非型別模板引數
    非型別模板引數是有限制,通常而言,它們可以是常函式(包括列舉值)或者指向外部連結物件的指標。


注:1.由於字串文字是內部連結物件(因為兩個具有相同名稱但處於不同模組的字串,是兩個完全不同的物件),所以你不能使用它們來作為模板實參;
    2.不能使用全域性指標作為函式模板;




小結:1.模板可以具有值模板引數,而不僅僅是型別模板引數;
           2.對於非型別模板引數,你不能使用浮點數、class型別的物件和內部連結物件(例如string)作為實參。

模板的技巧性基礎知識
一.關鍵字typename
    在C++標準化的過程中,引入關鍵字typename是為了說明:末班內部的識別符號可以是一個型別。
    譬如下面的例子:
    template<typename T>
    class MyClass{
          typename T::SubType *ptr;
          ……
    };
    上例中,第2個typename被用作說明:SubType是一個定義於類T內部的一個型別。因此,ptr是一個指向T::SubType型別的指標。
    若不使用typename,SubType就會被認為是一個靜態成員,那麼他應該是一個具體的變數或物件,於是,如下表達式:
    T::SubType * ptr
會被看做是類T的靜態成員SubType和ptr的乘積。


注:1.通常而言,當某個依賴於模板引數的名稱是一個型別是,就應該使用typename。
    2. .template構造
    考慮下面這個使用標準bitset型別的例子
    template<int N>
    void printBitset(std::bitset<N> const &bs)
    {
         std::cout<<bs.template       to_string<char,char_traits<char>,allocator<char> >();
    }
    上例中.template說明:bs.template後面的小於號(<)是模板實參列表的起始符號;只有在編輯其判斷小於號之前呢,存在依賴於模板引數的構造,才會出現這種問題。在本例中,傳入引數bs就是依賴於模板引數N的構造。
    {只有該前面存在依賴於模板引數的物件時,我們才需要在模板內部使用.template標記(和類似的諸如->template的標記),而且這些標記也只能在模板中才能作用。}




二.使用this->
   對於具有基類的類模板,自身使用名稱X並不等同於this->x。即使該x是從基類繼承獲得的,也是如此。例如:
   template <typename T>
   class Base{
       public:
           void exit();
   };


   template <typename T>
   class Derived : Base<T>{
       public:
           void foo();
               exit();
       }
   };


   在上例中,在foo()內部決定要呼叫哪個exit()時,並不會考慮基類Base中定義的exit()。因此,你如果不是獲得錯誤,就是呼叫了另一個exit()。


注:對於那些在基類中的宣告,並且依賴於模板引數的符號(函式或者變數的符號),應在它們前面使用this->或者Base<T>::。如果希望避免不確定性,你可以(使用諸如this->和Base<T>::等)限定(模板中)所有的成員訪問。




三.成員模板
   類成員也可以是模板。巢狀類和成員函式都可以作為模板。
   (預設賦值運算子要求兩邊具有相同的型別。)
   定義成員模板的語法:在定義有模板引數T的模板內部,還定義了一個含有模板引數T2的內部模板:
   template <typename T>
     template <typename T2>




注:1.對於類模板而言,只有那些被呼叫的成員函式才會例項化。因此,如果元素型別不同的類模板之間沒有進行相互賦值,就可以使用vector來作為內部容器。
    2.因為自定義的模板賦值運算子並不是必不可少的,所以不存在push_front()的情況下,某些程式並不會出現錯誤資訊,而且能正確執行。




四.模板的模板引數(VC6不支援,VC7支援)
   用stack為例,如果要使用一個預設值不同的內部容器,程式設計師必須兩次指定元素型別。也就是說,為了指定內部容器的型別,需要同時傳遞容器的型別和它所含元素的型別。如下:
   Stack<int,std::vector<int> >vStack;
   然而藉助模板的模板引數,你可以只指定容器的型別而不需要制定所含元素的型別,就可以宣告這個Stack類模板。
   Stack<int,std::vector>vStack;
   為獲得此特性,必須把第2個模板引數指定位模板的模板引數。在使用時,第2個引數必須是一個類模板,並且由第一個模板引數傳遞來的型別進行例項化。




注:1.之前說過作為模板引數的宣告,通常可以使用typename來替換關鍵字class。然而,若是為了定義一個類,則只能使用關鍵字class。
    2.函式模板並不支援模板的模板引數。




五.零初始化
   對於int、double或者指標等基本型別,並不存在“用一個有用的預設值來對它們進行初始化”的預設建構函式;相反,任何未被初始化的區域性變數都具有一個不確定值。
   故我們應該顯示的呼叫內建型別的預設建構函式,並將預設值設為0。如下:
   template<typename T>
   void foo()
   {
       T x = T();//如果T是內建型別,x是零或者false
   }
   對於類模板,在用某種型別例項化該模板,為了確認它所有的成員都已經初始化完畢,需要定義一個預設建構函式,通過一個初始化列表來初始化類模板的成員:
   template<typename T>
   class MyClass{
       private:
           T x;
       public:
           MyClass():x(){//確認x已被初始化,內建型別也是如此
           }
           ……
   };






六.使用字串作為函式模板的實參
   對於非引用型別的引數,在實參演繹的過程中,會出現陣列到指標的型別轉換(這種轉換通常也被稱為decay)。
   對於字元陣列和字串指標之間不匹配的問題並沒有什麼通用的解決方法。根據不同的情況,可以:
   1.使用非引用引數,取代引用引數(然而,這可能會導致無用的拷貝)。
   2.進行過載,編寫接受引用引數和非引用引數的兩個過載函式(然而,這可能會導致二義性)。
   3.對具體型別進行過載(譬如對std::string進行過載)。
   4.過載陣列型別,譬如:
       template <typename T,int N,int M>
       T const* max(T const (&a)[N],T const (&b)[M])
       {
           return a<b ? b:a;
       }
   5.強制要求應用程式程式設計師使用顯示型別轉換。






七.小結
   1.如果要訪問依賴於模板引數的型別名稱,應該在型別名稱前新增關鍵字typename。
   2.巢狀類和成員函式也可以是模板。
   3.賦值運算子的模板版本並沒有取代預設賦值的運算子。
   4.類模板也可以作為模板引數。我們稱之為模板的模板引數。
   5.模板的模板引數必須精確的匹配。匹配時並不考慮“模板的模板實參”的預設模板實參(如std::deque的allocator)。
   6.通過先是呼叫預設建構函式,可以確保模板的變數和成員都已經用一個預設值完成初始化,這種方法對內建型別的變數和成員也適用。
   7.對於字串,在實參演繹過程中,當且僅當引數不是引用時,才會出現數組到指標的型別轉換。



模板實戰
一.包含模型
   對於非模板程式碼大多C和C++程式設計師會這樣組織:
   1.類(class)和其他型別都放在標頭檔案中。
   2.對於全域性變數和(非內聯)函式,只有宣告放在標頭檔案中,定義則位於dot-C檔案中。
   但是對於模板程式碼卻會發生連結錯誤,針對這個問題,通常採用對待巨集或行內函數的解決方法:把模板的定義也包含在宣告模板的標頭檔案中,即讓定義和宣告都位於同意標頭檔案中。稱模板的這種組織方式為包含模型,其帶來的問題是:大大增加了編譯複雜程式所耗費的時間。若不考慮建立期的時間問題,建議儘量使用包含模型來組織模組程式碼。




注:非行內函數模板與“行內函數和巨集”有一個很重要的區別,那就是非行內函數模板在呼叫的位置並不會被擴充套件,而是當它們基於某種型別進行例項化之後,才產生一份新的(基於該型別的)函式拷貝。


二.顯示例項化
   C++標準還提供了一種手工例項化模板的機制:顯示例項化指示符。顯式例項化指示符由關鍵字template和緊接其後的我們所需要例項化的實體(可以是類、函式、成員函式等)的宣告組成,而且,該宣告是一個已經用引數完全替換之後的宣告。可以顯式例項化類模板,這樣就可以同時例項化他的所有類成員。但是有一點需要注意:對於這些在前面已經例項化過的成員,就不能再次對它們進行例項化。
   對於每個不同實體,在一個程式中最多隻能有一個顯式例項化體,換句話說,你可以同時顯式例項化print_typeof<int>和print_typeof<double>,但是同一程式中的每個識別符號都只能夠出現一次。如果不遵循這條規則,通常都會導致連結錯誤,聯結器會報告:發現了例項化實體的重複定義。
    為了能夠根據實際情況,自由的選擇包含模型或顯示例項化,可以把模板的定義和模板的宣告放在兩個不同的檔案中。通常的做法是使用標頭檔案來表達這兩個檔案(標頭檔案大多是那些希望被#include、具有特定副檔名的檔案);通常而言,遵守這種檔案分開約定是明智的。


三.分離模型(匯出模板)
   1.關鍵字export:在一個檔案中定義模板,並在模板的定義和(非定義的)宣告的前面加上關鍵字export。
    即使在模板定義不可見的條件下,被匯出的模板也可以正常使用,換句話說,使用模板的位置和模板定義的位置可以在兩個不同的翻譯單元中。
    實際上關鍵字export可以應用於函式模板、類模板的成員函式、成員函式模板和類模板的靜態資料成員。另外,還可用於類模板的宣告,這將意味著每個可匯出的類成員都被看作可匯出實體,但類模板本身實際上卻沒有被匯出(因此,類模板的定義仍然需要出現在標頭檔案中)。你仍然可以隱式或顯式的定義內聯成員函式。然而,行內函數卻是不可匯出的。
    export關鍵不能和inline關鍵字一起使用;如果用於模板的話,export要位於關鍵字template的前面。
    但其也存在的缺點:在應用分離模型的最後,例項化過程需要處理兩個位置:模板被例項化的位置和模板定義出現的位置。雖然這兩個位置在原始碼中看起來是完全分離的,但系統卻為了這兩個位置建立了一些看不見的耦合。這就意味著編譯器需要進行一些額外的處理,來跟蹤所有的這些耦合。這也將導致程式的建立時間可能會比包含模型所需要的建立時間還要多。並且,被匯出的模板可能會導致出人意料的語義。






注:當exported模板遇到編譯錯誤,且提示要引用被隱藏程式碼的定義時,應如何處理?
    1.對於我們預先編寫的程式碼,存在一個可以在包含模型和分離模型之間相互切換的開關;在此,使用預處理指示符
來獲得這種特性。另外,需重申的是除了明顯的邏輯區別之外,這兩種模型之間還具有細微的語義區別。




四.模板和內聯
   把短小函式宣告為行內函數是提高執行效率所普遍採用的方法。inline修飾符表明的是一種實現:在函式的呼叫處使用函式體(即內容)直接進行內聯替換,它的效率更優於普通函式的呼叫機制(針對短小函式而言)。
    函式模板和行內函數都可以被定義於多個翻譯單元中。
    函式模板預設情況下是內聯的。
    對於許多不屬於類定義一部分的短小模板函式,應使用關鍵字inline來宣告它們。




五.預編譯標頭檔案
   當翻譯一個檔案時,編譯器是從檔案的開頭一直進行到檔案末端的。
   預編譯標頭檔案機制主要依賴於下面的事實:我們可以使用某種方式來組織程式碼,讓多個檔案中前面的程式碼都是相同的。
   充分利用預處理標頭檔案的關鍵之處在於:(儘可能地)確認許多檔案開始處的相同程式碼的最大行數。
   有些程式設計師會認為:在使用預編譯標頭檔案的時候,允許#include一部分額外無用的標頭檔案,要比只選擇有用的標頭檔案具有更好的編譯速度:這還可以讓包含策略的管理變得更加容易。
   通常而言,預編譯這個檔案需要一段時間;但對於具有足夠記憶體的系統,預編譯標頭檔案機制會使得處理速度比編譯大多數單個(未經過預編譯的)標準標頭檔案快很多。
   管理預編譯標頭檔案的一種可取的方法是:對預編譯檔案進行分層,即根據標頭檔案的使用頻率和穩定性來進行分層。





六.除錯模板
   1.理解長段的錯誤資訊
   2.淺式例項化
   3.長符號串
   4.跟蹤程式
    在確認程式可以正確執行之前,我們先要確認程式的建立過程也是成功的。
    跟蹤程式可以是一個使用者定義的類,可以用作一個測試模板的實參。
    5.oracles
    在某些領域,tracer的一個擴充套件版本被稱為oracles(或稱為run-time analysis oracles)。它們是連線到推理引擎的tracers——所謂推理引擎(inference engine)是一個程式,他可以記住用來推匯出結論的斷言和推理。
    6.archetype
    利用archetype,我們可以驗證一個模板實現是否會請求期待以外的語法約束。典型而言,一個模板的實現可能會為模板庫中標記的每個concept,都開發一個archetype。



七.小結
   1.模板給原始的“編輯器+連結器”模型帶了挑戰,因此,需要使用其他的方法來組織模板程式碼,這些方法是包含模型、顯式例項化和分離模型。
   2.在大多數情況下,你應該使用包含模型(就是說,把所有模板程式碼都放在標頭檔案中)。
   3.通過把模板宣告程式碼和模板定義程式碼放在不同的標頭檔案中,你可以很容易的在包含模型和顯式例項化之間做出選擇。
   4.C++標準為模板定義了一個分離的編譯模型(使用關鍵字export)。然而,該關鍵字的使用還沒有普及,很多編譯器也不提供支援。
   5.除錯模板程式碼是具有挑戰性的。
   6.模板例項化可能會具有很長的名稱。
   7.為了充分利用預編譯程式碼,要確認#include指示符的順序是相通的。




模板術語
一.“類模板”還是“模板類”
    在C++中,類和聯合都被稱為類型別。如果不加額外的限定,我們通常所說的“類”是指:用關鍵字class或者struct引入的類型別。需要特別主義的一點就是:類型別包括聯合,而“類”不包括聯合。
    對於稱呼具備模板特性的類,現今還存在一些混淆:
    1.術語類模板說明的是:該類是一個模板;它代表的是:整個類家族的引數化的描述。
    2.另一方面,模板類通常被用於下面幾個方面:
      (1).作為類模板的同義詞。
      (2).從模板產生的類。
      (3).具有一個template-id名稱的類。




二.例項化和特化
    模板例項化是一個通過使用具體值替換模板引數,從模板產生出普通類、函式或者成員函式的過程。這個過程最後獲得的實體(譬如類、函式或者成員函式)就是我們通常所說的特化。




三.一處定義原則
    現在,我們只需要記住下面的ODR基本原則就足夠了:
    1.和全域性變數與靜態資料成員一樣,在整個程式中,非行內函數和成員函只能被定義一次。
    2.類型別和行內函數在每個翻譯單元中最多隻能被定義一次,如果存在多個翻譯單元,則其所有的定義都可以是等同的。
    3.一個翻譯單元是指:預處理一個原始檔所獲得的結果;就是說,它包括#include指示符(即所包含的標頭檔案)所包含的內容。




四.模板實參和模板引數
   模板引數是指:位於模板宣告或定義內部,關鍵字template後面所列舉的名稱。
   模板實參是指:用替換模板引數的各個物件。和模板引數不同的是,模板引數可以有不侷限於“識別符號名稱”(就是有多種型別或值)。
   一個基本原則是:模板引數必須是一個可以在編譯期確定的模板實體或者值。