1. 程式人生 > 程式設計 >詳解C++ 模板程式設計

詳解C++ 模板程式設計

型別模板

型別模板包括函式模板和類模板,基本上是C++開發人員接觸模板程式設計的起點。

下面程式碼演示了函式模板和類模板的使用方法:

// 函式模板
template<typename T>
T add(const T& a,const T& b) {
 return a + b;
}

// 類模板
template<typename T>
class Point {
private:
 T x[3];
 ...
};

型別模板以template開始宣告,尖括號內的typename關鍵字可用class替代。型別模板中typenameclass具有相同含義,均表示引數型別。實踐中typename

語義更廣泛,表示其後續的引數T是一個型別,不限定於類,建議使用。型別引數T可換成其他任意有意義的合法變數。

C++14新增變數模板:

// 變數模板
template<tyepename T>
constexpr T pi = T(3.1415926535897932385L);

尖括號之於模板猶如小括號之於函式:函式通過小括號()定義和呼叫,模板使用尖括號<>定義(需template關鍵字宣告)和例項化。上面演示了型別模板定義,下面程式碼介紹模板例項化:

int a = 1,b = 2;
// 例項化函式模板
std::cout << "add result:" << add<int>(a,b) << std::endl;

// 例項化類模板
auto p = Point<int>();

double radius = .5;
// 例項化變數模板
auto area = pi<double> * radius * radius;

同函式一樣,模板可以有預設值:

// 預設型別為int
template<typename T=int>
T add(const T& a,const T& b) {
 return a + b;
}

// 預設型別為double
template<typename T=double>
class Point {
private:
 T x[3];
 ...
};

與函式不同,對於函式模板,如果能從引數推斷出模板型別,則可略去尖括號模板例項化引數:

int a = 1,b = 2;
// 合法呼叫,編譯器能根據a b推斷出引數型別
std::cout << "add result:" << add(a,b) << std::endl;
// 等同於
std::cout << "add result:" << add<int>(a,b) << std::endl;

然而對於類模板,即使有預設引數,也不能省略尖括號(但是可以省去引數):

template<typename T=double>
struct Point {
 T x[3];
};

// 合法宣告
auto p = Point<double>();
// 合法宣告,型別使用預設的double
auto p2 = Point<>();
// 非法宣告,缺少模板呼叫標誌尖括號
auto p3 = Point();

型別引數模板在實際中使用最多,STL庫中vector、map等容器、algorithm中的許多演算法都用到了模板。

非型別引數模板

另一類常用模板是非引數模板,用來替代某個具體的值。例如:

// N維空間向量
template<int N>
struct Vector {
 double x[N];
};

// 例項化
auto v = Vector<100>();
...其他操作

需要注意的是,非型別引數模板能使用的型別十分有限,只有(signed/unsigned)整數、char和列舉這幾種型別可用(參考switch語法)。

同類型模板一樣,非型別引數模板也可以有預設值,但應用到類模板例項化也不能省略尖括號。

型別模板和非型別引數模板可以結合一起用:

template<typename T,int N>
struct Point {
 T x[N];
};

型別模板解決了型別問題,非型別引數模板解決了值的問題,實際中應用也十分廣泛。作為遞迴的經典場景,斐波那契數列可以用非型別模板解決:

template<int N>
struct Fib {
 static constexpr int value = Fib<N-1>::value + Fib<N-2>::value;
};
// 模板特化
template<>
struct Fib<1> {
 static constexpr int value = 1;
};
// 模板特化
template<>
struct Fib<0> {
 static constexpr int value = 0;
};

// 呼叫
std::cout << "Fib(10): " << Fib<10>::value << std::endl;

這個例子出現了”模板特化”,接下來介紹。

模板特化/偏特化

定義模板後,希望在特定條件下使用單獨的模板,這便是模板特化。上文中斐波那契數列定義的template<int N> struct Fib是母模板,接下來又定義了0和1兩個特化模板(子模板),指示編譯器遇到Fib<0>和Fib<1>的情況,使用這兩組單獨定義。需要注意的是特化模板的template引數為空,具體模板引數放到了模板名稱處,類似於模板例項化。

對多個模板引數的情形,如果只特化某個模板引數,便是偏特化。例如:

// 泛型模板定義
template<typename T1,typename T2> struct Add; 
// 特化模板
template<> struct Add<int,int> {...};
// 偏特化模板
template<typename T> struct Add<T,long> {....};

模板特化/偏特化類似於函式過載,能針對特殊情況進行特別處理。

模板匹配與SFINAE

模板特化使得同一個模板名稱有了多個定義,程式碼具體呼叫時會遇到模板匹配問題。理解模板匹配機制的關鍵便是SFINAE,這也是進階模板程式設計的必備知識點。

SFINAE是Substitution failure is not an error的縮寫,翻譯過來便是:匹配(替換)失敗不是錯誤。

怎麼理解這句話呢?

對於上面的斐波那契數列數列程式碼,編譯器遇到Fib<10>::value的程式碼,(可能)先會嘗試匹配Fib<0>,發現匹配不上,這是一個Substitution failure,但不是error,所以編譯器繼續嘗試其他可能性。接著匹配Fib<1>,同樣發現匹配不上,忽略這個Substitution failure繼續嘗試Fib<N>,OK,這一次沒問題,編譯成功。

如果是Fib<-1>::value,編譯器達到最大遞迴深度也找不到一個合適的匹配模板,這是一個error,因此編譯失敗。

備註:理解上面的話需要對編譯過程稍加了解,編譯過程會輸出許多資訊,編譯器一般只有遇到error才會終止編譯,比較常見的warning則不會。模板匹配中的Substitution可能連warning都算不上,不會影響編譯器繼續嘗試匹配

理解SFINAE是看懂稍微深奧點模板程式碼的基本功,重點便是:不怕你模板多,就怕找不到合適的模板。

兩階段編譯

有了模板(元)程式設計,C++原始碼編譯可以分為前期和後期,構成兩階段編譯。前期是模板的天下,編譯器掃描模板例項化語句,生成運算結果和具體程式碼;後期編譯器介入,再編譯生成機器碼。

模板程式碼執行在編譯期,因此有如下特點:

  • 沒有例項化的模板程式碼,即使有語法錯誤,編譯器也不會檢查和報錯。對按程式碼行數考核KPI的C++碼農,這絕對是福音,新增template程式碼十萬行,瞎編亂寫都可以,只要不例項化,永遠能編譯通過,編譯後的檔案大小(一般)不變,也不影響現有程式碼執行;
  • 對於常量,編譯前期直接計算,沒有執行時開銷。上文中的斐波那契數列值在編譯期便已經計算出來了;
  • 無法執行期動態呼叫程式碼。例如下面的要求做不到:
template<int N>
struct Point {double x[N];};
// 根據輸入動態生成類,無法實現和編譯成功
int n;
std::cin >> n;
auto p = new Point<n>();
  • 模板和多型/虛擬函式(理念)衝突。多型/虛擬函式的關鍵是執行期動態呼叫程式碼,而模板在編譯期確定,因此兩者理念上是衝突的。所以,如果你想一個成員函式既是模板函式,又是虛擬函式,怎麼做實現預期?

C/C++編譯有個預處理過程,只是做簡單字串替換,沒有具體運算,與模板生成程式碼不同

在編譯前期,除了模板程式碼被解釋執行,其他程式碼資訊都在,因此模板程式碼擁有類似反射/自省的能力,這也是C++超程式設計功能強大的原因之一。

C++11中的變化

C++11帶來了許多新特性和重大更新,可以認為C++11是一門新的語言。就模板來說,主要更新點如下:

1. 可以使用static constexpr int代替早期模板程式碼中的enum。網上許多斐波那契數列程式碼都是基於早期C++,一律使用enum方式定義欄位;

2. 可以使用using代替typedef。這是using語句能力的重大更新,早期我們定義型別或者別名都需要typedef,自C++11開始,簡單使用using就可以達到相同效果。

3. C++14引入了變數模板,上文已介紹。

模板優缺點

上文根據自己理解和實踐簡要介紹了C++模板程式設計的相關概念,本節總結一下C++模板的優缺點:

C++模板程式設計優點:

  • 減少程式碼輸入,提高程式碼重用和程式設計效率;
  • 支援鴨子型別(Duck typing)的特性,使用便利,功能強大;
  • 某些情況下能減少執行期開銷;
  • 能實現超程式設計,C++高手必備之路;

C++模板程式設計缺點:

  • 語法看起來是hack黑科技,程式碼可讀性差,編寫繁瑣;
  • 模板程式碼除錯困難,生成的錯誤資訊也晦澀難懂。你可以還記得剛開始使用STL模板的map等資料型別報錯的恐怖提示?
  • 編譯時間增加。

感謝閱讀,歡迎指正!

以上就是詳解C++ 模板程式設計的詳細內容,更多關於C++ 模板程式設計的資料請關注我們其它相關文章!