天天看點

C++——模闆特化&&模闆為什麼不能分離編譯1. 非類型模闆參數2. 模闆的特化3 模闆分離編譯4. 模闆總結

目錄

  • 1. 非類型模闆參數
  • 2. 模闆的特化
    • 2.1 概念
    • 2.2 函數模闆特化
    • 2.3 類模闆特化
      • 2.3.1 全特化
      • 2.3.2 偏特化
      • 2.3.3 類模闆特化應用示例
  • 3 模闆分離編譯
    • 3.1 什麼是分離編譯
    • 3.2 模闆為什麼不能分離編譯?
    • 3.3 解決方法
  • 4. 模闆總結

泛型程式設計||函數模闆||類模闆 部落格連結

1. 非類型模闆參數

模闆參數分類:類型形參數與非類型形參數。

類型形參即:出現在模闆參數清單中,跟在class或者typename之類的參數類型名稱。

非類型形參,就是用一個常量作為類(函數)模闆的一個參數,在類(函數)模闆中可将該參數當成常量來使用。

namespace HB
{
	// 定義一個模闆類型的靜态數組
	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 _size;
	};
}
           

注意:

  1. 浮點數、類對象以及字元串是不允許作為非類型模闆參數的。
  2. 非類型的模闆參數必須在編譯期就能确認結果,不能傳一個int類型的變量,即使這個變量已經被指派。

類似的應用有array:

C++——模闆特化&amp;&amp;模闆為什麼不能分離編譯1. 非類型模闆參數2. 模闆的特化3 模闆分離編譯4. 模闆總結

因為數組對于越界的檢查是越界讀——不檢查,越界寫——抽查,是以可以使用C++裡的容器array,array隻要越界就會報斷言錯誤,使用方法如下:

#include<array>
int main()
{
	int a1[10];
	array<int, 10> a2;
	array<int, 100> a3;

	
	cout << a1[10] << endl;
	cout << a1[11] << endl;

	//a1[10] = 0;
	a1[15] = 0;

	// C++11  array
	cout << a2[10] << endl;
	cout << a2[11] << endl;

	//a2[10] = 0;//報斷言錯誤
	//a2[15] = 0;

	return 0;
}
           

2. 模闆的特化

2.1 概念

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

// 函數模闆 -- 參數比對
template<class T>
bool Less(T left, T 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;
}
           

可以看到,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;
}
// 對Less函數模闆進行特化
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;
}
           

注意:一般情況下如果函數模闆遇到不能處理或者處理有誤的類型,為了實作簡單通常都是将該函數直接給出。

bool Less(Date* left, Date* right)
{
	return *left < *right;
}
           

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

2.3 類模闆特化

2.3.1 全特化

全特化即是将模闆參數清單中所有的參數都确定化。

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;
};
void TestVector()
{
	Data<int, int> d1;
	Data<int, char> d2;
}
           

2.3.2 偏特化

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

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

偏特化有以下兩種表現方式:

  • 部分特化

    将模闆參數類表中的一部分參數特化。

// 将第二個參數特化為int
template <class T1>
class Data<T1, int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
private:
	T1 _d1;
	int _d2;
};
           
  • 參數更進一步的限制

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

//兩個參數偏特化為指針類型
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); // 調用特化的指針版本
	//有現成的就用,沒有現成的再轉換
}
           

2.3.3 類模闆特化應用示例

有如下專門用來按照小于比較的類模闆Less:

#include<vector>
#include <algorithm>
template<class T>
struct Less
{
	bool operator()(const T& x, const T& y) const
	{
		return x < y;
	}
};
int main()
{
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 6);
	Date d3(2022, 7, 8);
	vector<Date> v1;
	v1.push_back(d1);
	v1.push_back(d2);
	v1.push_back(d3);
	// 可以直接排序,結果是日期升序
	sort(v1.begin(), v1.end(), Less<Date>());
	vector<Date*> v2;
	v2.push_back(&d1);
	v2.push_back(&d2);
	v2.push_back(&d3);
	// 可以直接排序,結果錯誤日期還不是升序,而v2中放的位址是升序
	// 此處需要在排序過程中,讓sort比較v2中存放位址指向的日期對象
	// 但是走Less模闆,sort在排序時實際比較的是v2中指針的位址,是以無法達到預期
	sort(v2.begin(), v2.end(), Less<Date*>());
	return 0;
}
           

通過觀察上述程式的結果發現,對于日期對象可以直接排序,并且結果是正确的。但是如果待排序元素是指針,結果就不一定正确。因為:sort最終按照Less模闆中方式比較,是以隻會比較指針,而不是比較指針指向空間中内容,此時可以使用類版本特化來處理上述問題:

// 對Less類模闆按照指針方式特化
template<>
struct Less<Date*>
{
	bool operator()(Date* x, Date* y) const
	{
		return *x < *y;
	}
};
           

特化之後,在運作上述代碼,就可以得到正确的結果

3 模闆分離編譯

3.1 什麼是分離編譯

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

3.2 模闆為什麼不能分離編譯?

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

// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
// main.cpp
#include"a.h"
int main()
{
	Add(1, 2);
	Add(1.0, 2.0);
	return 0;
}
           
C++——模闆特化&amp;&amp;模闆為什麼不能分離編譯1. 非類型模闆參數2. 模闆的特化3 模闆分離編譯4. 模闆總結

因為在main.obj中調用的Add< int >與Add< double >,編譯器在連結時才會找其位址,但是這兩個函數沒有執行個體化沒有生成具體代碼,也就沒有具體的位址,是以連結時報錯。

3.3 解決方法

  1. 将聲明和定義放到一個檔案 “xxx.hpp” 裡面或者xxx.h其實也是可以的。推薦使用這種。
  2. 模闆定義的位置顯式執行個體化。這種方法不實用,不推薦使用。

4. 模闆總結

【優點】

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

2.增強了代碼的靈活性

【缺陷】

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

2.出現模闆編譯錯誤時,錯誤資訊非常淩亂,不易定位錯誤

繼續閱讀