C++之 模板(下)模板的特化、類型萃取、分離編譯

在上篇博客中我們簡單講了一下兩大部分:函數模板類模板,本篇博客我們討論一下它的邊緣知識~


非類型模板參數

模板參數分爲:類型形參非類型形參

  • 類型形參:出現在模板參數列表中,跟在class或者typename之後的參數類型名稱。
  • 非類型形參,就是用一個常量作爲類 / 函數模板的一個參數,在類 / 函數模板中可將該參數當成常量來使用。

仍然使用動態數組(Vector)來演示:

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 _size == 0;
	}

private:
	T _array[N];		//非類型形參的使用
	size_t _size;
}

【注】:

  1. 浮點數類對象以及字符串是不允許作爲非類型模板參數的。
  2. 非類型的模板參數必須在編譯期就能確認結果。

類模板的特化

通常情況下,使用模板可以實現一些與類型無關的代碼,但對於一些特殊類型的可能會得到一些錯誤的結果,比如:

template<class T>
bool IsEqual(T& left, T& right){
	return left == right;
}

這個函數模板比較的對象是整型家族元素,如果出現浮點型或者字符串類型就無能爲力了,例如下面場景,函數模板的比較就是有問題的。

int main(){
	char p1[] = "giturtle";
	char p2[] = "giturtle";
	if(IsEqual(p1, p2))
		cout << "Yes" << endl;
	else
		cout << "No" << endl;

	return 0;
}

因爲對於字符串,我們希望通過字典序進行對比。此時,就需要對模板進行特化。即:在原模板類的基礎上,針對特殊類型所進行特殊化的實現方式

模板特化中分爲:

  • 函數模板特化
  • 類模板特化

函數模板特化

函數模板的特化步驟:

  1. 必須要先有一個基礎的函數模板
  2. 關鍵字template後面接一對空的尖括號<>
  3. 尖括號中指定需要特化的類型
  4. 函數形參表:必須要和模板函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。
template<>
bool IsEqual<char*>(char*& left, char*& right){
	if(strcmp(left, right) > 0)		//字典序比較
		return true;
	return false;
}

【注】:一般情況下如果函數模板遇到不能處理或者處理有誤的類型,爲了實現簡單通常都是將該函數直接給出。

bool IsEqual(char* left, char* right){
	if(strcmp(left, right) > 0)
		return true;
	return false;
}

類模板特化

全特化

全特化:是將模板參數類表中所有的參數都確定化

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;}
	private:
	int _d1;
	char _d2;
};

int main(){
	Data<int, int> d1;
	Data<int, char> d2;
	
	return 0;
} 

輸出結果

Data<int, int> 
Data<int, char> 

可見全特化之後的版本成功匹配了某些場景,成爲了特定情況下最適合的類模板的實例化版本。

偏特化

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

偏特化分爲:

  • 部分特化
  • 參數更進一步的限制

對於類模板:

template<class T1, class T2>
class Data{
public:
	Data() {cout<<"Data<T1, T2>" <<endl;}
private:
	T1 _d1;
	T2 _d2;
};
  1. 部分特化版本:
//特化第二個參數爲 int 類型
template <class T1>
class Data<T1, int>{
public:
	Data() {cout<<"Data<T1, int>" <<endl;}
private:
	T1 _d1;
	int _d2;		//偏特化
}; 
  1. 參數更進一步的限制版本:
//兩個參數偏特化爲指針類型
template <typename T1, typename T2>
class Data <T1*, T2*>{
public:
	Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
	T1 _d1;
	T2 _d2;
};

//兩個參數偏特化爲引用類型
template <typename T1, typename T2>
class Data <T1&, T2&>{
public:
	Data(const T1& d1, const T2& d2)
	 : _d1(d1)
	 , _d2(d2)
	{
		cout<<"Data<T1&, T2&>" <<endl;
	}
private:
	const T1 & _d1;
	const T2 & _d2;
};

void test2 (){
	Data<double , int> d1; 		//特化的 int 版本
	Data<int , double> d2;	    //基礎 的模板
	Data<int *, int*> d3; 		//特化的 指針 版本
	Data<int&, int&> d4(1, 2);  //特化的 引用 版本
}

輸出結果

Date(T1,int);		// 調用特化的 int 版本
Date(T1,T2);		// 調用 基礎 的模板
Data(T1*,T2*);		// 調用特化的 指針 版本
Data(T1&,T2&);		// 調用特化的 引用 版本

類型萃取

類型萃取是類模板特化的應用。

例如:實現一個通用的拷貝函數

  1. memcpy
template<class T>
void Copy(T* dst, const T* src, size_t size){
	memcpy(dst, src, sizeof(T)*size);
}

上述代碼雖然對於任意類型的空間都可以進行拷貝,但是如果拷貝自定義類型對象就可能會出錯,因爲自定義類型對象有可能會涉及到深拷貝(比如string),而memcpy屬於淺拷貝。如果對象中涉及到資源管理,就只能用賦值。

  1. 賦值方式拷貝
template<class T>
void Copy(T* dst, const T* src, size_t size){
	for(size_t i = 0; i < size; ++i){
		dst[i] = src[i];
	}
}

用循環賦值的方式雖然可以,但是代碼的效率比較低,而C/C++程序最大的優勢就是效率高。那能否將另種方式的優勢結合起來?遇到內置類型就用memcpy拷貝,遇到自定義類型就用循環賦值方式來做?可以,設定一個flag即可。

  1. 增加bool類型區分自定義與內置類型
template<class T>
void Copy(T* dst, const T* src, size_t size, bool flag){
	if(flag)
		memcpy(dst, src, sizeof(T)*size);
	else{
		for(size_t i = 0; i < size; ++i)
		dst[i] = src[i];
	}
}

通過多增加一個參數,就可將兩種拷貝的優勢體現結合起來。
但缺陷是:用戶需要根據所拷貝元素的類型去傳遞第三個參數,那出錯的可能性就增加。那能否讓函數自動去識別所拷貝類型是內置類型或者自定義類型呢?

  1. 使用函數區分內置於自定義類型
    因爲內置類型的個數是確定的,可以將所有內置類型集合在一起,如果能夠將所拷貝對象的類型確定下來,在內置類型集合中查找其是否存在即可確定所拷貝類型是否爲內置類型。
bool IsPODType(const char* strType){
	const char* arrType[] = {"char", "short", "int", "long", "long long", "float","double", "long double"};
	for(size_t i = 0; i < sizeof(array)/sizeof(array[0]); ++i){
		if(0 == strcmp(strType, arrType[i]))
			return true;
	}
	return false;
}

template<class T>
void Copy(T* dst, const T* src, size_t size){
	if(IsPODType(typeid(T).name()))
		memcpy(dst, src, sizeof(T)*size);
	else{
		for(size_t i = 0; i < size; ++i)
		dst[i] = src[i];
	}
}

通過typeid來確認所拷貝對象的實際類型,然後再在內置類型集合中枚舉其是否出現過,既可確認所拷貝元素的類型爲內置類型或者自定義類型。
但缺陷是:枚舉需要將所有類型遍歷一遍,每次比較都是字符串的比較,效率比較低。

  1. 類型萃取
    爲了將內置類型與自定義類型區分開,給出以下2個類分別代表內置類型自定義類型
// 代表內置類型
struct TrueType{
	static bool Get(){
		return true ;
	}
};

// 代表自定義類型
struct FalseType{
	static bool Get(){
		return false ;
	}
};

給出以下類模板,將來用戶可以按照任意類型實例化該類模板。

template<class T>
struct TypeTraits{
	typedef FalseType IsPODType;
};

對上述的類模板進行以下方式的實例化:

// 所有內置類型都需要特化一下:
template<>
struct TypeTraits<char>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<short>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<int>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<long>{
	typedef TrueType IsPODType;
};

通過對TypeTraits類模板重寫4Copy函數模板,來確認所拷貝對象的實際類型。

/*
	T爲int:TypeTraits<int>已經特化過,
		程序運行時就會使用已經特化過 TypeTraits<int>,
		該類中的 IsPODType 剛好爲類 TrueType ,
		而 TrueType 中 Get 函數返回 true ,
		內置類型使用 memcpy 方式拷貝

	T爲string:TypeTraits<string>沒有特化過,
		程序運行時使用 TypeTraits 類模板, 
		該類模板中的 IsPODType 剛好爲類 FalseType ,
		而 FalseType 中 Get 函數返回 false,
		自定義類型使用賦值方式拷貝
*/

template<class T>
void Copy(T* dst, const T* src, size_t size){
	if(TypeTraits<T>::IsPODType::Get())
		memcpy(dst, src, sizeof(T)*size);
	else{
		for(size_t i = 0; i < size; ++i)
			dst[i] = src[i];
	}
}

測試代碼

int main() {
	int a1[] = { 1,2,3,4,5,6,7,8,9,0 };
	int a2[10];
	Copy(a2, a1, 10);
	for (const auto& e : a2) {
		cout << e << " ";
	}
	cout << endl;

	string s1[] = { "1111", "2222", "3333", "4444" };
	string s2[4];
	Copy(s2, s1, 4);
	for (const auto& e : s2) {
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

輸出結果

1 2 3 4 5 6 7 8 9 0
1111 2222 3333 4444

可見完成了我們的預期~

類型萃取有什麼用呢?其實STL庫中許多底層實現都是使用類型萃取的,有興趣的看官可以去查看一下底層源碼。


模板的分離編譯

什麼是分離編譯?

一個程序(項目)由若干個源文件共同實現,而每個源文件單獨編譯生成目標文件,最後將所有目標文件鏈接起來形成單一的可執行文件的過程稱爲分離編譯模式

那模板的分離編譯有什麼不同?

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

// a.h -> 頭文件
template<class T>
T Add(const T& left, const T& right);		//聲明
// a.cpp  -> cpp文件
template<class T>
T Add(const T& left, const T& right){		//實現
	return left + right;
}
// main.cpp	-> cpp文件
#include"a.h"
int main(){
	Add(1, 2);			//調用
	Add(1.0, 2.0);
	
	return 0;
}

因爲在a.cpp中編譯器沒有找到對Add模板函數的的實例化,因此不會生成具體的加法函數。

main.obj中調用的Add<int>Add<double>,編譯器在鏈接時纔會尋址,單這兩個函數沒有實例化生成具體代碼,因此鏈接時會報錯。

解決方法

  1. 將聲明和定義放到一個文件 “xxx.hpp” 裏,或者xxx.h中也是可以的。
  2. 模板定義的位置顯式實例化。但這種方法不實用,不推薦使用。

小結

【優點】:

  1. 模板複用了代碼,節省資源,更快的迭代開發,C++的標準模板庫(STL)因此而產生。
  2. 增強了代碼的靈活性

【缺點】:

  1. 模板會導致代碼膨脹問題,也會導致編譯時間變長
  2. 出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤。

以上是C++模板相關知識的大概梳理,順便貼上模板(上篇)的鏈接,有興趣的博友可以傳送過去看看,主要講解:泛型編程函數模板類模板~

C++之 模板(上):【 https://blog.csdn.net/qq_42351880/article/details/100058341

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