文章目錄
- 一、非類型模板參數
- 應用場景
- 二、模板的特化
- 函數模板特化
- 類模板特化
- 全特化
- 偏特化
- 三、模板分離編譯
- 解決方法
- 四、模板總結
一、非類型模板參數
先前介紹的函數模板和類模板都是針對類型的類模板參數,非類型模板參數有哪些使用場景呢?我們先來看下面這個例子:
const int N = 10;template <class T>
class stack
{
public:T _a[N];int top;int capacity;
};int main()
{stack<int> s1; //10stack<int> s2; //1000return 0;
}
這是我們定義的一個靜態的棧,棧的大小由N來決定,我們想創建一個大小為10的棧s1和一個大小為1000的棧s2就會出現一個問題,由于靜態棧類型大小的每次編譯時是固定的,如果我們創建這兩個棧那么空間只能開1000,那么s1就會確定浪費990大小的空間。所以這個場景下我們就可以用非類型模板參數,它就是用一個整型常量來作為類(函數)模板的一個參數,在類(函數)模板中可以把這個參數當作常量來使用。
template <class T, size_t N> //非類型模板參數
class stack
{
public:T _a[N];int top;int capacity;
};int main()
{stack<int, 10> s1; //10stack<int, 1000> s2; //10000return 0;
}
非類型模板參數也能給缺省值,模板參數和函數參數一樣,缺省值只能從右往左給,傳實參時只能從左往右依次給,避免參數匹配的歧義性。
若模板參數是全缺省,我們創建對象時模板參數可以都不傳,但是還是要帶<>:
stack<> s3; //全缺省
注意:
1、浮點數、類對象以及字符串是不允許作為非類型模板參數的。
2.、非類型的模板參數必須在編譯期就能確認結果。
應用場景
在標準庫里array容器就用到了非類型模板參數,它是靜態數組,類似C語言的arr[ ],。功能也和arr[ ]差不多,它創建出來后也不會對成員初始化,初始化值需要用它的一個接口fill。
array和arr[ ]的本質區別是array下標訪問是用的重載函數operator[ ],所以有嚴格的越界讀和越界寫的檢查,而arr[ ]本質是指針±和指針的解引用,只有在臨界區域對越界寫有抽查,完全沒有越界讀檢查。
array<int, 10> a1;
a1.fill(1); //初始化
在C++中我們用數組容器一般優先考慮vector,array相比vector的優勢在開大量固定大小的數組時array效率更高,因為array是在棧上開空間,在函數棧幀創建出來時空間就開好了,vector需要在堆上動態開辟。
二、模板的特化
當我們實現出的模板函數或者模板類不符合我們預期時就可以用到模板特化,模板特化與原模板深入綁定,只有當原模版存在時模板特化才會生效。簡單來說普通模板實例化是按模板生成代碼,特化本身已是具體代碼,它是為特殊類型改寫代碼,且匹配優先級比普通模板更高。
函數模板特化
函數模板特化的返回值、參數列表需與主模板替換類型后完全匹配,
我們先來看一個例子:
Less對傳過來的整型變量可以準確的比較大小,而對于傳過來的整型指針變量我們本意是想讓它比較指針指向的整型變量的大小,而這里底層邏輯是比較指針變量本身的大小,所以不符合我們預期,這里就可以針對整型指針變量單開一個模板的特化版本,改寫比較邏輯。
這個時候可能有的讀者有和我當時一樣的想法,為什么不直接寫一個函數用來特殊處理int*,比如:bool Less(double* left, double* right),而是用模板特化呢?
我們要知道模板是一種泛型編程的思想,模板本意是設計出一套通用邏輯,而如果我們再創建一個普通函數,就會破壞模板本身的泛型語義一致性,使得不同類型調用Less函數時行為變得零散。模板特化就是模板為特定類型做到特殊適配,模板與特化模板是強耦合的,后續要調整通用模板的邏輯時也要考慮特化模板是否要配套修改。
我們初步設計的函數特化如下:
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}
注意事項:
1.必須要先有一個基礎的函數模板
2. 關鍵字template后面接一對空的尖括號<>
3. 函數名后跟一對尖括號,尖括號中指定需要特化的類型
4. 函數模板特化參數要和原模版一致,如果不同編譯器可能會報一些奇怪的錯誤。
前三點很好理解,最后一點我們需要借助一個例子來理解,其實前面我們寫的普通模板并不是最最優的,我們T類型我們無法確定,可能是內置類型也可能是string或者自定義類型,如果對象一但過大這時我們原先普通模板是傳值傳參因為要拷貝效率就很低了,這里的最優解是傳引用,并且比較邏輯不改變參數最好還要加const修飾:
template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}
而普通模板寫成這樣我們再寫特化版本難度就變高了,在此之前我們要先明確一點,當const修飾指針變量是當const在*左邊時是修飾的指針指向的內容不能修改,當const在*右邊時是修飾指針變量本身不能被修改。
const T* p1; //修飾*p1
T const * p2; //修飾*p2
T* const p3; //修飾p3
這里我們要寫模板特化就要梳理普通模板的邏輯,因為特化版本的參數類型要和原模版參數類型嚴格一致,原模版是引用的T變量,并且const是修飾的被引用的T變量本身,我們要為int*寫一個模板特化,那么int*在邏輯上就是原模版的T,所以我們不能寫成const int*& left,因為const是修飾的int*變量指向的內容,就相當于const是修飾原模版中的*T,所以為了和原模版參數嚴格匹配這里模板特化的參數應該是int* const & left,這樣才和原模版一樣,const修飾的是引用變量本身。
template<>
bool Less<int*>(int* const & left, int* const & right)
{return *left < *right;
}
類模板特化
類模板特化就是將全部或者一部份參數確定化,模板參數全部確定化是全特化,模板參數部分確定化是偏特化,當然偏特化也可以是對參數的進一步限制。
類模板特化也需要以沒有特化過的原模版為基礎進行特化。模板名稱和模板參數結構上需要與原模板保持一致,但在成員定義、實現邏輯上可以完全不同。
全特化的匹配優先級比偏特化優先級更高,因為全特化更現成,具體用全特化還是偏特化由使用場景決定。
全特化
全特化即是將模板參數列表中所有的參數都確定化。
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};//全特化
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
};
偏特化
- 特化部分參數
//特化部分參數
template<class T1>
class Data<T1, char>
{
public:Data() { cout << "Data<T1, char>" << endl; }
};
- 對參數進一步限制
//對參數的進一步限制
template<class T1, class T2>
class Data<T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>" << endl; }
};template<class T1, class T2>
class Data<T1&, T2*>
{
public:Data() { cout << "Data<T1&, T2*>" << endl; }
};
也可以兩種混著特化:
template<class T2>
class Data<int, T2*>
{
public:Data() { cout << "Data<int, T2*>" << endl; }
};
三、模板分離編譯
一個程序(項目)由若干個源文件共同實現,而每個源文件單獨編譯生成目標文件,最后將所有目標文件鏈接起來形成單一的可執行文件的過程稱為分離編譯模式。也就是我們常說的把聲明放在頭文件,把實現放在.cpp。
模板不建議分離編譯,無論的類模板還是函數模板。這是模板的定義對調用方不可見,定義方不知道調用方的使用需求導致的。
比如一個模板定義在tep.cpp,聲明在tep.h,test.cpp調用這個模板。
調用方有模板實例化成什么具體類型的信息,但是它只有模板的聲明,所以它在編譯階段無法實例化出具體代碼,因為模板要實例化必須要模板的完整定義。
定義方tep.cpp有模板的完整定義,但是它沒有模板實例化成什么具體類型的信息,所以它也同樣無法在編譯階段實例化出具體代碼。
那么定義方模板在編譯期間因為沒有生成具體代碼就不會進符號表,然后鏈接階段調用方想找到調用模板的符號但是符號表里沒有,就會因為找不到符號而報錯。
歸根結底就是因為.cpp文件在鏈接之前不能交互,那么發生在鏈接之前的編譯階段就會因為信息無法交互使得模板無法生成具體代碼。
這也和內聯函數不能分離編譯的原因有有相似之處,都是因為分離編譯導致
“定義不可見”,最終鏈接階段符號缺失。但是內聯函數是因為調用處沒有內聯函數的完整定義導致函數無法展開,內聯函數又因為機制不會在定義處進符號表。模板是因為調用處沒有模板的定義無法實例化出具體代碼,定義處因為沒有具體類型而無法生成具體代碼。
類模板也一樣,類模板是將它的成員函數分離到了兩個文件。
解決方法
這個問題就是因為沒有實例化造成的,所以解決這個問題就是要讓模板在編譯階段能實例化,不管是定義方還是調用方。
1、模板定義的位置顯式實例化。這種方法不實用,不推薦使用,因為你每調用一種類型都要顯示實例化一份。所以這也驗證了模板是可以分離編譯的,只是不推薦。
template<class T>
void FuncT(const T& x)
{cout << "void FuncT(const T& x)" << endl;
}
//顯示實例化
template
void FuncT(const int& x);
2、模板的最佳實踐就是不要分離編譯,把模板直接定義在頭文件,調用模板的地方在編譯期間就有了模板的定義生成了具體代碼,填入了對應的符號,不用在鏈接期間再去找。
四、模板總結
優點
1、模板復用了代碼,節省資源,更快的迭代開發,C++的標準模板庫(STL)因此而產生
2、增強了代碼的靈活性
缺陷
1、模板會導致代碼膨脹問題,也會導致編譯時間變長(不可避免,自己寫也得寫)
2、出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤
以上就是小編分享的全部內容了,如果覺得不錯還請留下免費的贊和收藏
如果有建議歡迎通過評論區或私信留言,感謝您的大力支持。
一鍵三連好運連連哦~~