天天看點

C++:19---重載與模闆、模闆特例化

一、重載與模闆

  • 函數模闆可以被另一個模闆或一個普通非模闆函數重載
  • 如果涉及函數模闆,則函數比對規則會有以下的限制:
  • 如果同樣好的函數中隻有一個是非模闆函數,則選擇此函數
  • 如果同樣好的函數中沒有非模闆函數,而有多個函數模闆,則其中一個模闆比其他模闆更特例化,則選擇此模闆
  • 否則,調用有歧義
  • ①對于一個調用,其候選函數包括所有模闆實參推斷成功的函數模闆執行個體
  • ②候選的函數模闆總是可行的,因為模闆實參推斷會排除任何不可行的模闆
  • ③可行函數(模闆與非模闆)按類型轉換(如果對此調用需要的話)來排序。當然,可以用于函數模闆調用的類型是非常有限的
  • ④如果恰有一個函數比任何其他函數都更好的比對,則選擇此函數。但是,如果有多個函數提供同樣好的比對,則:

編寫重載模闆

  • 我們構造一組函數,它們在調試中可能很有用,用來列印相關的資訊,兩個重載函數定義如下:
  1. //第一版本
  2. //列印任何類型
  3. template<typename T>
  4. string debug_rep(const T &t)
  5. {
  6. ostringstream ret;
  7. ret << t;
  8. return ret.str();
  9. }
  10. //第二版本
  11. //參數為指針類型的
  12. //注意:此函數不能用于char*(字元指針),因為IO庫為char*值定義了一個<<版本,
  13. //此<<版本假定指針表示一個空字元結尾的字元數組,并列印數組的内容而非位址值(我們将在下面介紹如何處理字元指針)
  14. template<typename T>
  15. string debug_rep(T *p)
  16. {
  17. ostringstream ret;
  18. ret << "pointer: " << p; //列印指針自身的值
  19. //如果有内容,列印指針所指的内容
  20. if (p)
  21. ret << " " << debug_rep(*p);//調用上一版本的debug_rep函數
  22. else
  23. ret << " null pointer";
  24. return ret.str();
  25. }
  • 如果我們編寫下面的代碼:
  • 将調用第一版本的debug_rep。因為第二個版本debug_rep版本要求一個指針參數,是以不符合
  1. std::string s("hi");
  2. std::cout << debug_rep(s) << std::endl;
  •  如果我們編寫下面的代碼,那麼兩個函數都可生成執行個體:
  • debug_rep(const string*&):由第一個版本的debu_rep執行個體化而來
  • debug_rep(string*):由第二個版本的debu_rep執行個體化而來
  • 但是第二個版本是最精确的比對,因為第一個版本需要進行普通指針到const指針的轉換。是以編譯器調用的是第二個版本
  1. std::string s("hi");
  2. std::cout << debug_rep(&s) << std::endl;

多個可行模闆的最終精确選擇

  • 根據上面定義的debug_rep(),我們定義下面的調用代碼:
  1. std::string s("hi");
  2. const std::string *sp = &s;
  3. std::cout << debug_rep(sp) << std::endl;
  • 此例中的兩個模闆都是可行的,而且都是精确比對:
  • debug_rep(const string*&):由第一個版本debu_rep執行個體化而來
  • debug_rep(const string*):由第二個版本的debu_rep執行個體化而來
  • 但是根據重載函數模闆的特殊規則,此調用的解析被解析為debug_rep(T*),是以調用的是第二個版本的debu_rep
  • 原因在于:debug_rep(const T&)本質上可以用于任何類型(包括指針類型),debug_rep(T*)隻适用于指針類型,是以第二版本更适合
  • 當有多個重載模闆對一個調用提供同樣好的比對時,應選擇最特例化的版本。

非模闆和模闆的重載

  • 現在我們編寫一個非模闆版本的debug_rep()函數
  1. //第三版本
  2. //列印雙引号包圍的string
  3. string debug_rep(const string &s)
  4. {
  5. return '"' + s + '"';
  6. }
  • 現在我們有下面的調用,那麼也将有兩個版本的函數可調用:
  • debug_rep<string>(const string&):第一版本的模闆
  • debug_Rep(const string&):第三版本的普通非模闆函數
  • 但是編譯器最終選擇第三版本來調用。是以當存在多個同樣好的函數模闆時,編譯器選擇最特例化的版本,一個非模闆函數比一個函數模闆更好
  1. std::string s("hi");
  2. std::cout << debug_rep(s) << std::endl;

重載模闆和類型轉換(處理C風格字元串和字元串字面常量)

  • 現在我們來讨論一下:C風格字元串指針和字元串字面常量
  • 現在我們有下面的調用,那麼上面三個版本都可以調用:
  • debug_rep(const T&):T被綁定到char[10](第一版本)
  • debug_rep(T*):T被綁定到const char(第二版本)
  • debug_rep(const string&):要求從const char*到string的類型轉換(第三版本)
  • 但是編譯器最終選擇第二版本來調用。因為第三版本需要進行一次使用者定義的類型轉換,第一版本不是針對于指針的,第二版本是針對于指針的,是以最終選擇第二版本的哈數來調用
std::cout << debug_rep("hi world!") << std::endl; //最終調用debug_rep(T*)
  • 如果希望字元指針按string來處理,可以定義下面兩個非模闆重載版本:
  1. //将字元指針轉換為string,并調用string版本的debug_rep
  2. string debug_rep(char *p)
  3. {
  4. return debug_rep(string(p)); //調用第三版本的
  5. }
  6. string debug_rep(const char *p)
  7. {
  8. return debug_rep(string(p)); //調用第三版本的
  9. }

缺少聲明可能導緻程式行為異常

  • 我們以上面的使char*版本的debug_rep()的函數為例:
  • 為了使char*版本的debug_rep()可以正常工作,在定義此版本之前,debug_rep(const string&)的聲明必須在作用域中
  • 否則,char*版本的debug_rep()就會去調用函數模闆的debug_rep(),與我們最初的目的相反了
  1. template<typename T> string debug_rep(const T &t);
  2. template<typename T> string debug_rep(T *p);
  3. //為了使debug_rep(char*)的定義正常工作,此debug_rep()的聲明必須在作用域中
  4. //否則debug_rep(char*)将調用模闆函數版本的
  5. string debug_rep(const string &s);
  6. string debug_rep(char *p)
  7. {
  8. //如果接收一個const string&的版本的聲明不在作用域中
  9. //傳回語句将調用debug_rep(const T&)的T執行個體化為string的版本
  10. return debug_rep(string(p));
  11. }
  • 通常,如果使用了一個忘記聲明的函數,代碼将編譯失敗,但對于重載函數模闆的函數而言,則不是這樣。如果編譯器可以從模闆執行個體化出與調用比對的版本,則缺少的聲明就不重要了。在本例中聲明接受的string參數的debug_rep版本,編譯器會預設地執行個體化接受const T&的模闆版本

二、模闆執行個體化

  • 編寫單一模闆,使之對任何可能的模闆實參都是最适合的,都能執行個體化,這并不總是能辦到。在某些情況下,通用模闆的定義對特定類型是不适合的:通用定義可能編譯失敗或做得不正确。是以我們需對針對類或函數定義一個特例化版本
  • 下面是兩個模闆:
  1. //第一版本:可以比較任意兩個類型
  2. template<typename T>
  3. int compare(const T&, const T&);
  4. //第二版本:處理字元串字面常量
  5. template<size_t N,size_t M>
  6. int compare(const char(&)[N], const char(&)[M]);
  • 隻有我們傳遞給compare資格字元串字面常量或者一個數組時,編譯器才會調用第二個版本;如果我們傳遞給它字元指針,就會調用第一個版本:
  1. const char *p1 = "hi", *p2 = "mom";
  2. compare(p1, p2); //調用第一版本
  3. compare("hi", "mom"); //調用第二版本
  • 我們無法将一個指針轉換為一個數組的引用,是以對于p1和p2的使用,調用的是第一版本的模闆函數

定義函數模闆特例化

  • 為了處理字元指針(而不是數組),可以為第一個版本的compare定義一個模闆特例化版本。一個特例化版本就是模闆的一個獨立的定義,在其中一個或多個模闆參數被指定為特定的類型 
  • 特例化一個函數模闆時,必須為原模闆中的每個模闆參數都提供實參。為了指出我們正在執行個體化一個模闆,應使用關鍵字template後跟一個空尖括号對<>
  1. template<typename T>
  2. int compare(const T&, const T&);
  3. template<size_t N,size_t M>
  4. int compare(const char(&)[N], const char(&)[M]);
  5. //compare的特例化版本,處理字元數組的指針
  6. template<>
  7. int compare(const char* const &p1,const char* const &p2)
  8. {
  9. return strcmp(p1, p2);
  10. }
  • 當我們特例化一個模闆時,函數參數類型必須與一個先前聲明的模闆中對應的類型比對。本例中我們特例化的模闆是:
  1. template<typename T>
  2. int compare(const T&, const T&);
  • 因為我們想要字元指針,是以T為char*,是以最基本的參數應該為const char*&,另外,我們希望定義一個常量指針,是以在char*後面也加一個const

函數重載與模闆特例化

  • 當定義函數模闆的特例化版本時,我們本質上接管了編譯器的工作。即,我們為原模闆的一個特殊執行個體提供了定義。重要的是要弄清楚:一個特例化版本本質上是一個執行個體,而非函數名的一個重載版本
  • 特例化的本質是執行個體化一個模闆,而非重載它。是以,特例化不影響函數比對。
  • 但是如果我們将一個特殊的函數定義為一個特例化版本還是一個獨立的非模闆函數,會影響到函數比對(例如我們在上面在上面定義的3個compare函數,其中兩個是模闆,一個是非模闆,那麼非模闆的将與模闆函數構成重載)

類模闆特例化

  • 除了特例化函數模闆,我們還可以特例化類模闆
  • 作為了一個例子:
  • 一個重載的調用運算符,它接受一個容器關鍵字類型的對象,傳回一個size_t
  • 兩個類型成員,result_type和argument_type,分别調用運算符的傳回類型和參數類型
  • 預設構造函數和拷貝指派運算符
  • 我們将标準庫的hash模闆定義一個特例化版本,使其來儲存我們自定義的Sales_data類
  • 預設情況下,無序容器使用hash<key_type>來組織元素。為了讓我們自己的資料類型也能使用這種預設組織方式,我們自定義了一個hash模闆的特例化
  • 一個特例化的hash類必須定義:
  • 另外,由于hash模闆定義在std命名空間内,是以如果我們想要特例化hash,必須先打開std命名空間,然後在其中進行特例化
  • 下面的代碼是針對于hash模闆的特例化,其特例化的對象是我們自定義的Sales_data對象,其中有一些注意點:
  • 使用“template<>”表明這是一個特例化版本的類型
  • operator()函數:是用來傳回給定類型的值的一個哈希函數。對于一個給定值,任何時候調用此函數都應該傳回相同的結果,一個好的哈希函數對不相等的對象(幾乎總是)應該産生不同的結果
  • 标準庫被内置類型和很多标準庫類型都定義了hash類的特例化版本。是以我們在operator()函數中直接調用這些特例化的hash類,然後求取哈希值,最後将哈希值進行按位與(^),最終将哈希結果傳回
  1. namespace std {
  2. template<>
  3. struct hash<Sales_data>
  4. {
  5. typedef size_t result_type;
  6. typedef Sales_data argument_type;
  7. size_t operator()(const Sales_data& s)const;
  8. };
  9. size_t hash<Sales_data>::operator()(const Sales_data& s)const
  10. {
  11. //下面的hash類都是标準庫針對特定的資料類型進行的特例化,我們直接調用就可以了
  12. return hash<std::string>()(s.bookNo) ^
  13. hash<unsigned>()(s.units_sold) ^
  14. hash<double >()(s.revenue);
  15. }
  16. }
  • 下面是Sales_data類型的定義,由于hash的特例版需要通路Sales_data的私有成員,是以在Sales_data的定義中,我們将hash的特例化版本作為其友元類:
  1. template<class T> class std::hash; //友元聲明
  2. class Sales_data {
  3. private:
  4. std::string bookNo;
  5. unsigned units_sold;
  6. double revenue;
  7. friend class std::hash<Sales_data>; //特例化版本的hash為其友元
  8. };
  • 需要注意的是:我們特例化hash類中operator()函數中使用ash()函數計算所有三個資料成員的哈希值,進而與我們為Sales_data定義的operator==是相容的。預設情況下,為了處理特定關鍵字類型,無序容器會組合使用key_type對應的特例化hash版本和key_type上的相等運算符
  • 假定我們的特例化版本在作用域中,當将Sales_data作為容器的關鍵字類型時,編譯器就會自動使用上面我們定義的特例化版本,例如:
  1. //使用hash<Sales_data>和Sales_data的operator==
  2. unordered_multiset<Sales_data> SDset;

為了讓Sales data的使用者能使用hash的特例化版本,我們應該在Sales_ data 的頭

檔案中定義該特例化版本。

類模闆部分特例化

  • 與函數模闆不同,類模闆的特例化不必為所有模闆實參提供實參,我們可以隻指定一部分而非所有模闆參數,或是參數的一部分而非全部特性
  • 一個“類模闆的部分特例化”本身是一個模闆,使用它時使用者還必須為那些在特例化版本中未指定的模闆實參提供實參

我們隻能部分特例化類模闆,而不能部分特例化函數模闆。

  • 例如标準庫remove_reference類型,該模闆是通過一系列的特例化版本來完成其功能的。定義如下:
  • 第一個模闆是最通用的模闆,可用于任意類型執行個體化
  • 第二個模闆和第三個模闆是特例化版本:根據規則,首先定義模闆參數;在類名之後,為要特例化的模闆參數指定實參,這些實參列于模闆名之後的尖括号中。這些實參與原始模闆中的參數按位置對應
  1. //原始的、最通用的版本
  2. template<class T>
  3. struct remove_reference {
  4. typedef T type;
  5. };
  6. //部分特例化版本
  7. template<class T>
  8. struct remove_reference<T&> { //針對于左值引用的
  9. typedef T type;
  10. };
  11. template<class T>
  12. struct remove_reference<T&&> { //針對于右值引用的
  13. typedef T type;
  14. };
  • 當我們有下面的程式時,程式會根據類型自動調用不同的模闆
  1. int i;
  2. //調用原始模闆
  3. remove_reference<decltype(42)>::type a;
  4. //i為左值引用,調用第一個(T&)部分特例化版本
  5. remove_reference<decltype(i)>::type b;
  6. //std::move(i)傳回右值,調用第二個(T&&)部分特例化版本
  7. remove_reference<decltype(std::move(i))>::type c;
  8. //a、b、c均為int

特例化成員而不是類

  • 我們可以之特例化特定成員函數而不是特例化整個模闆
  • 例如,如果Foo是一個模闆,包含一個成員Bar,我們可以隻特例化該成員:
  1. //下面是一個模闆類
  2. template<typename T>
  3. struct Foo {
  4. Foo(const T &t = T()) :mem(t) {}
  5. void Bar() {
  6. //通用的Bar()函數
  7. }
  8. T mem;
  9. };
  10. //特例化Foo<int>版本的的成員Bar
  11. template<>
  12. void Foo<int>::Bar()
  13. {
  14. //...
  15. }
  • 我們有下面的調用代碼:
  1. Foo<string> fs; //執行個體化Foo<string>::Foo()
  2. fs.Bar(); //執行個體化Foo<string>::Bar()
  3. Foo<int> fi; //執行個體化Foo<int>::Foo()
  4. fi.Bar(); //執行個體化Foo<int>::Bar()
  • 除了int之外的任何類型都使用在Foo内部定義的Bar()函數,而int類型的Foo對象使用在外部定義的特例化Bar()成員函數 

繼續閱讀