天天看點

[ C++ ] template 模闆進階 (特化,分離編譯)

本篇内容包括C++ 非典型模闆參數,類模闆的特化,模闆的分離編譯。C++模闆簡單概念及其使用大家可點選此連結:​​[ C++ ] C++之模闆template​​

1.  數組模闆示例和非類型模闆參數

        模闆常用作容器類,這是因為類型參數的概念非常适合于将相同的存儲方案用于不同的類型。

        首先介紹一個允許指定數組大小的簡單數組模闆。一種方法是在類中使用動态數組和構造函數參數來提供元素數目;另一種方法是使用模闆參數來提供正常數組的大小,C++ 11 新增的模闆array就是這樣做的。

代碼示例:

// 定義一個模闆類型的靜态數組
template<class T, size_t N = 10>
class array
{
public:
  T& operator[](size_t index) { return _array[index]; }
  const T& operator[](size_t index)const { return _array[index]; }

  size_t size()const{ return _size; }
  bool empty()const{ return 0 == _size; }

private:
  T _array[N];
  size_t      
[ C++ ] template 模闆進階 (特化,分離編譯)

        上述代碼中,關鍵字class(或在這種上下文中等價的關鍵字typename)指出T為類型參數,size_t 指出N的類型為無符号整數。這種參數(指定特殊的類型而不是用作泛型名)稱為非類型(non-type)或表達式(expression)參數。

        表達式參數有一些限制,表達式參數可以是整型,枚舉,引用或指針。是以,double是不合法的,但是double* 卻是合法的。另外,模闆代碼不能修改參數的值,也不能使用參數的位址。是以,在array模闆中不能使用類似++N或者&N等表達式。另外,執行個體化模闆時,用作表達式參數的值必須是常量表達式。

表達式參數的優點:        

        與平常使用的構造函數方法相比,這種改變數組大小的方法有一個優點。構造函數 方法使用的是通過new和delete管理堆記憶體,而表達式參數方法使用的是為自動變量維護的記憶體棧。這樣執行速度将更快,尤其是在使用了很多小型數組時。

表達式參數的缺點:

        表達式參數的主要缺點是每種數組大小都将生成自己的模闆。也就是說,下面的聲明将生成兩個獨立的類聲明:

ArrayTP<double, 12> eggweights; 
ArrayTP<double, 13> donuts;      
[ C++ ] template 模闆進階 (特化,分離編譯)

但下面的聲明隻生成一個類聲明,并将數組大小資訊傳遞給類的構造函數:

Stack<int> eggs(12);
Stack<int> dunkers(13);      
[ C++ ] template 模闆進階 (特化,分離編譯)

        另一個差別是,構造函數方法更通用,這是因為數組大小是作為類成員(而不是寫死)存儲在定義中的。這樣可以将一種尺寸的數組賦給另一種尺寸的數組,也可以建立允許數組大小可變的類。 

2.模闆的特化

2.1什麼是模闆的特化

通常情況下,使用模闆可以實作一些與類型無關的代碼,但對于一些特殊類型的可能會得到一些錯誤的結果,需要特殊處理,比如:實作了一個專門用來進行小于比較的函數模闆

代碼執行個體:

template<class T>
bool Less(T left, T right){
  return left < right;
}

int main(){
  cout << Less(1, 2) << endl;//可以比較

  return 0;
}      
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)

當然我們之前實作的日期類,也可以通過模闆來進行日期比較。

[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)

但是如果這下面這種情況還能拿到我們想要的結果嗎?

Date* p1 = &d1;
  Date* p2 = &d2;
  cout << Less(p1, p2) << endl;      
[ C++ ] template 模闆進階 (特化,分離編譯)

        此時的結果是不能,是以我們可以看到,Less絕大多數情況下都是可以正常比較的,但是在特殊場景下就得到錯誤的結果。正如此例,p1指向的d1顯然小于p2指向的d2對象,但是Less内部并沒有比較p1和p2指向的對象内容,而是比較的是p1和p2指針的位址,就無法達到預期而出現錯誤。

        此時,就需要對模闆進行特化。即:在原模闆的基礎上,針對特殊類型所進行特殊化的實作方式。模闆特化中分為函數模闆特化與類模闆特化。

2.2 函數模闆特化

函數模闆特化步驟:

        1.必須先有一個基礎的函數模闆。

        2.關鍵字template後面接一個對空的尖括号<>

        3.函數名後跟一對進括号,尖括号中指定需要特化的類型

        4.函數形參表:必須要和模闆函數的基礎參數類型完全相同,如果不同編譯可能會報錯。

現就将剛才日期類進行函數模闆特化:

template<class T>
bool Less(T left, T right){
  return left < right;
}

//日期類函數模闆特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
  return *left < *right;
}
int main(){
  cout << Less(1, 2) << endl;//可以比較

  Date d1(2022, 7, 7);
  Date d2(2022, 7, 8);
  cout << Less(d1, d2) << endl;

  Date* p1 = &d1;
  Date* p2 = &d2;
  cout << Less(p1, p2) << endl;
  return 0;
}      
[ C++ ] template 模闆進階 (特化,分離編譯)

此時,我們就達到了想要的結果。注意,一般情況下如果函數模闆遇到不能處理或者處理有誤的類型時,為了實作簡單通常都是将該函數直接給出。也就是直接給出一個指針類型的函數模闆。

////指針類型  (* , *) 
template<class T>
bool Less(T* left, T* right){
  return      
[ C++ ] template 模闆進階 (特化,分離編譯)

這種實作簡單明了,代碼可讀性高,容易書寫,因為對于一些參數類型複雜的函數模闆,特化時特别給出,是以函數模闆不建議特化。

2.3 類模闆特化 

2.3.1 全特化

所謂全特化就是将模闆參數清單中所有的都确定化。我們使用自定Data類來舉例說明:

template<class T1,class T2>
class Data
{
public:
  Data()
  {
    cout << "Data<T1,T2>" << endl;
  }

private:
  T1 _d1;
  T2 _d2;
};      
[ C++ ] template 模闆進階 (特化,分離編譯)

此時,我們如果傳入兩個參數int,double根據模闆他會比對到T1,T2 但是如果我們想要比對到int和double時,我們就需要全特化類模闆。

////全特化
template<>
class Data<int, double>
{
public:
  Data()
  {
    cout << "Date<int,double>"<<endl;
  }
};      
[ C++ ] template 模闆進階 (特化,分離編譯)

此時如果我們測試輸入檢視結果:

[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)

2.3.2 偏特化

偏特化:任何針對模闆參數進一步進行條件限制設計的特化版本。

偏特化有一下兩種表現形式:

        1. 部分特化 -- 将模闆參數類表中的一部分參數特化

        2. 參數更進一步限制 -- 偏特化并不僅僅是指特化部分參數,而是針對模闆參數更進一步的條件限制所設計出來的一個特化版本。

1.部分特化 

假設我要将第二個參數特化為int

////半特化  第二個參數總是int類型
////1、将模闆參數類表中的一部分參數特化。
template<class T1>
class Data<T1,int>
{
public:
  Data()
  {
    cout << "Date<T1,int>"<<endl;
  }
};      
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)

2.參數更近一步限制

兩個參數偏特化為指針類型

//偏特化 參數給定了 兩個指針類型 具體那種不确定
//2、偏特化并不僅僅是指特化部分參數,
//   而是針對模闆參數更進一步的條件限制所設計出來的一個特化版本。
template<class T1,class T2>
class Data<T1*, T2*>
{
public:
  Data()
  {
    cout << "Data<T1*,T2*>"      
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)

兩個參數偏特化為引用類型

template<class T1, class T2>
class Data<T1&, T2&>
{
public:
  Data()
  {
    cout << "Data<T1&,T2&>"      
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)

注:優先級 :全特化 > 半特化 > 預設 

3. 模闆分離編譯

3.1 什麼是分離編譯

一個程式(項目)由若幹個源檔案共同實作,而每個源檔案單獨編譯生成目标檔案,最後将所有目标檔案連結起來形成單一的可執行檔案的過程稱為分離編譯模式。

3.2 模闆的分離編譯

假如有以下場景,模闆的聲明與定義分離開,在頭檔案中進行聲明,源檔案中完成定義:

// a.h
template<class T> 
T Add(const T& left, const;
// a.cpp
template<class T> T Add(const T& left, const{
  return left + right;
}
// main.cpp

#include"a.h"

int main(){
  Add(1, 2);
  Add(1.0, 2.0);

  return 0;
}      
[ C++ ] template 模闆進階 (特化,分離編譯)

分析:C/C++程式要運作,一般要經曆以下步驟:預處理-->編譯-->彙編-->連結

編譯:對程式按照語言特性進行詞法,文法,語義分析,錯誤檢查無誤後生成彙編代碼,注意頭檔案不參與編譯 編譯器對工程中的多個源檔案是分離開單獨編譯的。

連結:将多個obj檔案合并成一個,并處理沒有解決的位址問題。

[ C++ ] template 模闆進階 (特化,分離編譯)
[ C++ ] template 模闆進階 (特化,分離編譯)

3.3 解決方法

1. 将聲明和定義放到一個檔案 "xxx.hpp" 裡面或者xxx.h其實也是可以的。推薦使用這種。

2. 模闆定義的位置顯式執行個體化。這種方法不實用,不推薦使用。

4.模闆總結

優點:

        1.模闆複用了代碼,節省資源,更快的疊代開發,C++的标準模闆庫(STL)是以而産生

        2.增強了代碼的靈活性,簡化程式設計工作,提高程式的可靠性。

缺點:

        1.模闆會導緻代碼膨脹問題,也會導緻編譯時間變長       

繼續閱讀