天天看點

深入了解c++中的函數模闆

非類型模闆參數

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

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

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

namespace bite
{
	template<class T, size_t N>
	class array
	{
	public:
		void push_back(constT& data)
		{
			//N=10;
			_array[_size++] = data;
		}

		T& operator[](size_t)
		{
			assert(index < _size)
				return _array[index];
		}

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

		size_t size()const
		{
			return _size;
		}
	private:
		T _array[N];
		size_t _size;

	};
}
           

注意事項

  1. 浮點數、類對象以及字元串是不允許作為非類型模闆參數的。
  2. 非類型的模闆參數必須在編譯期就能确認結果。

模闆的特化

模闆特化的概念

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

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
	}

	bool operator>(const Date&d)const
	{
		return _day > d._day;
	}

	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "/" << d._month << "/" << d._day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};
template<class T>
T& Max(T& left, T& right)
{
	return left > right ? left : right;
}
	char *p1 = "world";
	char *p2 = "hello";
	cout << Max(p1, p2) << endl;
	cout << Max(p2, p1) << endl;
           

這個代碼他就無法比較字元串類型的變量的大小

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

函數模闆特化

如果不需要通過形參改變外部實參加上const

例如

template<class T>
const T& Max(const T& left, const T& right)
{
	return left > right ? left : right;
}
//函數模闆的特化
template<>
char *& Max<char*>(char*& left, char*& right)
{
	//>0大于 =0等于 <0小于
	if (strcmp(left, right) > 0)
	{
		return left;
	}
	return right;
}
           

注意事項

  1. 必須要先有一個基礎的函數模闆
  2. 關鍵字template後面接一對空的尖括号<>
  3. 函數名後跟一對尖括号,尖括号中指定需要特化的類型
  4. 函數形參表: 必須要和模闆函數的基礎參數類型完全相同,如果不同編譯器可能會報一些奇怪的錯誤。

一般函數模闆沒必要特化,直接把相應類型的函數給出就行

char* Max(char *left, char* right)
{
	if (strcmp(left, right) > 0)
	{
		return left;
	}
	return right;
}
           

類模闆的特化

全特化

//全特化-----對所有類型參數進行特化
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

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


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

偏特化

部分特化

//偏特化,将模闆參數清單中的參數部分參數類型化
template<class T1>
class Data<T1,int>
{
public:
	Data() { cout << "Data<T1, int>" << endl; }
private:
	T1 _d1;
	int _d2;
};

int main()
{
	Data<int, double>d1;
	Data<int, int>d2;
	Data<double, int>d3;
	system("pause");
	return 0;
}
           

參數更進一步的限制

//偏特化:讓模闆參數清單中的類型限制更加的嚴格
template<class T1,class T2>
class Data<T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
private:
	T1* _d1;
	T2* _d2;
};

int main()
{
	Data<int*, int>d1;
	Data<int, int*>d2;
	Data<int*, int*>d3;//特化
	Data<int*, double*>d4;//特化
	system("pause");
	return 0;
}
           

模闆特化的作用之類型萃取

編寫一個通用類型的拷貝函數

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

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

class String
{
public:
	String(const char* str = "")
	{
		if (str == nullptr)
			str = "";
		this->_str = new char[strlen(str) + 1];
		strcpy(this->_str, str);
	}

	String(const String& s)
		:_str ( new char[strlen(s._str)+1])
	{
		strcpy(this->_str, s._str);
	}

	String& operator=(const String &s)
	{
		if (this != &s)
		{
			char *str = new char[strlen(s._str) + 1];
			strcpy(str, s._str);
			delete[]_str;		
			_str = s._str;
		}
	}

	~String()
	{
		delete[]_str;
	}

private:
	char* _str;
};

//寫一個通用的拷貝函數,要求:效率盡量高
template<class T>
void Copy(T* dst, T* src, size_t size)
{
	memcpy(dst, src, sizeof(T)*size);
}

void TestCopy()
{
	int array1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int array2[10];

	Copy(array2, array1, 10);

	String s1[3] = { "111", "222", "333" };
	String s2[3];
	Copy(s2, s1, 3);
}
           

如果是自定義類型用memcpy,那麼會存在

  1. 淺拷貝----導緻代碼崩潰
  2. 記憶體洩露—S2數組中每個String類型對象原來的空間丢失了

增加一個拷貝函數處理淺拷貝

template<class T>
void Copy2(T* dst, T*src, size_t size)
{
	for (size_t i = 0; i < size; ++i)
	{
		dst[i] = src[i];
	}
}
           
  1. 優點:一定不會出錯
  2. 缺陷:效率比較低,讓使用者做選擇,還需要判斷調用哪個函數

讓函數自動去識别所拷貝類型是内置類型或者自定義類型

bool IsPODType(const char* strType)
{
	//此處将所有的内置類型枚舉出來
	const char * strTypes[] = { "char", "short", "int", "long", "long long", "float", "double" };
	for (auto e : strTypes)
	{
		if (strcmp(strType, e) == 0)
			return true;
	}
	return false;
}

template<class T>
void Copy(T* dst, T*src, size_t size)
{
	//通過typeid可以将T的實際類型按照字元串的方式傳回
	if (IsPODType(typeid(T).name())
	{
		//T的類型:内置類型
		memcpy(dst, src, sizeof(T)*size);
	}
	else
	{
		//T的類型:自定義類型----原因:自定義類型種可能會存在淺拷貝
		for (size_t i = 0; i < size; ++i)
		{
			dst[i] = src[i];
		}
	}
}
           

在編譯期間就确定類型—類型萃取

如果把一個成員函數的聲明和定義放在類裡面,編譯器可能會把這個方法當成内聯函數來處理

class String
{
public:
	String(const char* str = "")
	{
		if (str == nullptr)
			str = "";
		this->_str = new char[strlen(str) + 1];
		strcpy(this->_str, str);
	}

	String(const String& s)
		:_str(new char[strlen(s._str) + 1])
	{
		strcpy(this->_str, s._str);
	}

	String& operator=(const String& s)
	{
		if (this != &s)
		{
			char* str = new char[strlen(s._str) + 1];
			strcpy(str, s._str);
			delete[] _str;
			_str = str;
		}

		return *this;
	}

	~String()
	{
		delete[]_str;
	}

private:
	char* _str;
};


//确認T到底是否是内置類型
//是
//不是
//對應内置類型
struct TrueType
{
	static bool Get()
	{
		return true;
	}
};

//對應自定義類型
struct FalseType
{
	static bool Get()
	{
		return true;
	}
};

template<class T>
struct TypeTraits
{
	typedef FalseType PODTYPE;

};

template<>
struct TypeTraits<char>
{
	typedef TrueType PODTYPE;
};

template<>
struct TypeTraits<short>
{
	typedef TrueType PODTYPE;
};
template<>
struct TypeTraits<int>
{
	typedef TrueType PODTYPE;
};

//........還有很多内置類型


template<class T>
void Copy(T* dst, T* src, size_t size)
{
	// 通過typeid可以将T的實際類型按照字元串的方式傳回
	if (TypeTraits<T>::PODTYPE::Get())
	{
		// T的類型:内置類型
		memcpy(dst, src, sizeof(T)*size);
	}
	else
	{
		// T的類型:自定義類型---原因:自定義類型中可能會存在淺拷貝
		for (size_t i = 0; i < size; ++i)
			dst[i] = src[i];
	}
}
           

STL中的類型萃取

// 代表内置類型
struct __true_type {};
// 代表自定義類型
struct __false_type {};

template <class type>
struct __type_traits
{
	typedef __false_type is_POD_type;
};

// 對所有内置類型進行特化
template<>
struct __type_traits<char>
{
	typedef __true_type is_POD_type;
};
template<>
struct __type_traits<signed char>
{
	typedef __true_type is_POD_type;
};
template<>
struct __type_traits<unsigned char>
{
	typedef __true_type is_POD_type;
};
template<>
struct __type_traits<int>
{
	typedef __true_type is_POD_type;
};
template<>
struct __type_traits<float>
{
	typedef __true_type is_POD_type;
};
template<>
struct __type_traits<double>
{
	typedef __true_type is_POD_type;
};
// 注意:在重載内置類型時,所有的内置類型都必須重載出來,包括有符号和無符号,比如:對于int類型,必
須特化三個,int -- signed int -- unsigned int
// 在需要區分内置類型與自定義類型的位置,标準庫通常都是通過__true_type與__false_type給出一對重載
的
// 函數,然後用一個通用函數對其進行封裝
// 注意:第三個參數可以不提供名字,該參數最主要的作用就是讓兩個_copy函數形成重載
template<class T>
void _copy(T* dst, T* src, size_t n, __true_type)
{
	memcpy(dst, src, n*sizeof(T));
}
template<class T>
void _copy(T* dst, T* src, size_t n, __false_type)
{
	for (size_t i = 0; i < n; ++i)
	dst[i] = src[i];
}
template<class T>
void Copy(T* dst, T* src, size_t n)
{
	_copy(dst, src, n, __type_traits<T>::is_POD_type());
}
           

分離編譯

預處理----->編譯---->彙編----->連結

深入了解c++中的函數模闆
// 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;
}
           

沒有模闆的函數,沒有問題

有模闆的函數,編譯可以過,但是連結會出錯

函數模闆編譯:

  1. 執行個體化之前:編譯器隻做一些簡答的文法檢測,不會生成處理具體類型的代碼。并不會确認函數的入口位址
  2. 執行個體化期間:編譯器會推演形參類型來確定模闆參數清單中T的實際類型,在生成具體類型的代碼
  3. 不支援分離編譯

解決分離編譯

  1. 将聲明和定義放到一個檔案 “xxx.hpp” 裡面或者xxx.h其實是可以的。
  2. 模闆定義的位置顯式執行個體化。

模闆總結

優點

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

缺點

  1. 模闆會導緻代碼膨脹問題,也會導緻編譯時間變長
  2. 出現模闆編譯錯誤時,錯誤資訊非常淩亂,不易定位錯誤

更加深入學習參考這個連結