C++模板:什麼是特化?學習筆記

參考:IBM編譯器中國開發團隊博客
其中幾個比較好的例子,下面的class 都可以換成 typename,向後兼容性比較好。
而且typename是較class更加新的標準,具體class 可能導致的問題可見這篇文章,講的特別詳細
知無涯值C++ typename

struct t1{}; struct t2{}; struct t3{};

void func(t1 arg){ printf("called t1\n"); }
void func(t2 arg){ printf("called t2\n"); }
void func(t3 arg){ printf("called t3\n"); }

int main(void)
{
t1 x1; t2 x2; t3 x3;
func(x1);
func(x2);
func(x3);
return 0;
}

輸出:
called t1
called t2
called t3

這個很簡單,編譯器根據傳遞給函數的實參類型來決定調用哪個函數,這就是重載解析。在調用前,編譯器有一個候選函數調用列表:
void func(t1);
void func(t2);
void func(t3);
每個調用函數都有各自的參數,編譯器根據參數最匹配原則選擇相應的函數

模板函數:

#include <iostream>
#include <typeinfo>

struct t1{}; struct t2{}; struct t3{};

using namespace std;

template <class A, class B, class C> void func(A a1, B a2, C a3)
{
   cout << "A: " << typeid(a1).name() << endl;
   cout << "B: " << typeid(a2).name() << endl;
   cout << "C: " << typeid(a3).name() << endl;
}

int main(void)
{
  t1 x1; t2 x2; t3 x3;
  func(x1,x2,x3);
  return 0;
}

輸出:
A: t1
B: t2
C: t3

在這個使用了一個函數模板的例子中,編譯器有一個帶有3個未知類型<A,B,C>的候選調用函數,它將實參 (x1,x2,x3)傳遞給函數func中的3個形參(A,B,C),可以很容易看到編譯器是如何推導出模板參數的:
A t1
B t2
C t3
編譯器實例化了模板函數;將實參傳遞給模板函數中的形參以創建一個真正的函數:
void func(t1 a1, t2 a2, t3 a3)

如果有其他的候選重載函數,他們都將會和非模板函數的例子一樣被綁定在一起,然後在重載解析中根據實參類型調用相應的函數。

重載解析允許用戶創建同一個函數的不同版本,這些函數將根據傳進來的參數的類型,做一些不同的操作。編譯器會根據類型信息來選擇相應的函數。通過使用模板函數,用戶可以定義帶參數化類型的函數,從而減少需要定義的重載函數的個數。編譯器會選擇正確的模板併爲用戶創建候選的重載函數。

類模板:

#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

int main(void)
{
   container<t1,10> test;
   test.callMe();
   return 0;
}

輸出:
primary A: t1 I: 10

在這個例子中,編譯器並不會玩什麼把戲,這個例子中只有一個類container, 它接收了實參<t1,10>並傳遞給模板參數<A, I>,推導出A即爲t1,I爲10。

含有一個全特化的類模板:

#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

template <> struct container<t3,99>{
   void callMe(){
      cout << "complete specialization t3, 99" << endl;
   }
};

int main(void)
{
   container<t1,10> test1;
   test1.callMe();
   container<t3,99> test2;
   test2.callMe();
   return 0;
}

輸出:
primary A: t1 I: 10
complete specialization t3, 99

在這個例子中有兩個模板,其中一個是全特化模板,即模板中模板參數全部指定爲確定的類型。特化(specialized)不過是一個花哨的術語,意思是形參不再爲形參,它們已經有了確定的值。我更傾向於使用“全特化”這個術語,感覺這更容易讓人理解。但是在大多數的C++書籍,包括標準C++,都將其稱爲“顯示特化”。
現在編譯器有了兩個類名都爲container的類模板,類模板被重載:
template <class A, int I> struct container;
template <> struct container<t3,99>;

當編譯器執行到container<t1,10>test1, 對於參數<t1, 10>:

  • 候選模板1可推出 <A=t1, I=10> ,所以候選模板1有效;
  • -候選模板2無法推出<t3,99> 能與 <t1,10>匹配,所以候選模板2被剔除。
    這樣編譯器只有一個候選模板1,也即最終被匹配的模板。
    當編譯器執行到container<t3, 99>test2,對於參數<t3, 99>:
  • 候選模板1可推出<A=t3, I=99>,所以候選模板1有效
  • 候選模板2,很明顯 <t3,99> 與模板中的 <t3,99>相匹配,所以候選模板2有效。
    在一個程序中發現有兩個或者兩個以上候選模板有效時,編譯器根據最匹配原則選擇最爲匹配的那個模板,即候選模板2。

偏特化+全特化的一個例子:

#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

template <class A1>  struct container<A1,25>{
   void callMe(){
      cout << "partial specialization" << typeid(A1).name() << " and 25 " << endl;
   }
};

template <> struct container<t3,99>{
   void callMe(){
      cout << "complete specialization t3, 99" << endl;
   }
};


int main(void)
{
   container<t1,10> test1;
   test1.callMe();
   container<t3,99> test2;
   test2.callMe();
   container<t2,25> test3;
   test3.callMe();
   container<t3,25> test4;
   test4.callMe();
   return 0;
}

輸出:
primary A: t1 I: 10
complete specialization t3, 99
partial specializationt2 and 25
partial specializationt3 and 25

此例有3個候選模板:
template <class A, int I> struct container;
template struct container<A1,25>;
template <> struct container<t3,99>;

模板1是帶有兩個模板參數的主模板,模板2是帶有一個模板參數的偏特化模板,模板3是無模板參數的全特化模板。
如前面所說,偏特化也僅是一個花哨的術語,偏特化模板中的模板參數沒有被全部確定,需要編譯器在編譯時進行確定。
當編譯器編譯執行到container<t3,25> test4,參數爲<t3,25>:

  • 候選模板1,編譯器可推導出 <A=t3, I=25>,故候選模板1有效;
  • 候選模板2,編譯器爲偏特化模板可推導出<A1=t3, 25>,故候選模板2有效;
  • 候選模板3, 編譯器不可能從<t3,25>得到<t3,99>,故候選模板3被剔除。
    候選模板2是最匹配的模板,故匹配模板2。

最後一個例子:

#include <iostream>
#include <typeinfo>
using namespace std;

struct t1{}; struct t2{}; struct t3{};

template <class A, int I> struct container{
   void callMe(){
      cout << "primary A: " << typeid(A).name() << " I: " << I << endl;
   }
};

template <int I1>  struct container<t3,I1>{
   void callMe(){
      cout << "partial specialization t3 and " << I1  << endl;
   }
};

template <> struct container<t3,99>{
   void callMe(){
      cout << "complete specialization t3, 99 " << endl;
   }
};


int main(void)
{
   container<t1,10> test1;
   test1.callMe();
   container<t3,99> test2;
   test2.callMe();
   container<t3,75> test3;
   test3.callMe();
   container<t3,25> test4;
   test4.callMe();
   return 0;
}

輸出:
primary A: t1 I: 10
complete specialization t3, 99
partial specialization t3 and 75
partial specialization t3 and 25

本質上,偏特化模板的匹配和選擇過程與重載解析非常類似。實際上,在非常複雜的偏特化情況下,編譯器可能就是將偏特化直接譯成函數,然後直接調用重載解析來處理。
重載解析和偏特化匹配都用到了模板參數推導。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章