前言
泛型編程是C++繼面向對象編程之後的又一個重點,是爲了編寫與具體類型無關的代碼。而模板是泛型編程的基礎。模板簡單來理解,可以看作是用宏來實現的,事實上確實有人用宏來實現了模板類似的功能。模板,也可以理解爲模具行業的模型。根據分類,有函數模板和類模板。根據傳入的不同模板參數,函數模板會生成不同模板函數。類模板則生成不同的模板類。
模板參數
1. 概念
模板定義以關鍵字template開始,<>中是模板參數列表(template parameter list),模板參數列表即表示可以是一個或多個模板參數(template parameter)。
l 模板實參則是在實例化函數模板或是類模板時的類型或值。
l 模板形參是通過模板實參推導出來的或是直接顯式指定。
l 模板參數和普通函數參數一樣,可以有默認值參數。
2. 模板形參分類
l 類型參數(type parameter)
表明這個模板參數是一個類型。
template<class/typename T, ……>
l 非類型參數(nontype parameter)
表明這個模板參數不是一個類型,而是常量數值,只能是整形、指針和引用。
template<int N, ……>
template<class/typename T, int N, ……>
// 例
template<const int* pM>
void Test1(){}
template<const char M>
void Test2()
{
int nArr[M] = {0};
}
class A{};
template<A& AA>
void Test3(){}
const int n = 4;
extern const int arr[] = {1, 2, 3};
A a;
int _tmain(int argc, _TCHAR* argv[])
{
Test1<arr>();
Test2<n>();
Test3<a>();
return 0;
}
3. 模板實參
l 模板實參推斷(template argument deduction)
編譯器使用函數調用中的實參類型來推斷出模板實參,然後用這些實參生成對應的函數。
l 顯式模板實參(explicit template argument)
類模板生成具體的模板類,都是顯式模板實參來實現的。
MyClass<int> myClass;
Set<int>(4);
有些模板函數沒有實參,這時就必須通過顯式模板實參來生成對應的模板函數。
模板函數有實參,但是類型可能無法正確推導,這時也可以加上顯式模板實參的。
模板函數有實參,並且能夠正確推導類型,這個時加與不加顯式模板實參均可。
函數模板
1. 概念
函數模板,就是根據模板形參的不同,生成不同的模板函數。
template<class/typename T, ……>
retType FunctionName(parameter)
{
// Function Body
}
class和typename在此處意義一樣,只是用class容易被人誤解爲後面的形參T是類。
函數模板在沒有實例化時,只是一個生成函數的模板。
而在具體的實例化之後,即指明具體的形參,就生成了一個具體的模板函數。
2. 聲明和定義
函數模板與普通函數一樣,聲明與定義可以分開,也可以一起。
l 聲明
template<typename T>
bool Compare(T t1, T t2);
l 定義
template<typename T>
bool Compare(T t1, T t2)
{
return (t1 > t2);
}
類模板
1. 概念
類模板根據模板形參的不同,生成不同的模板類。
template<class/typename T, ……>
class ClassName
{
// Class definition
};
類模板,在沒有實例化之前,只是一個生成類的模板。
而在具體的實例化之後,其實就生成了一個具體的模板類。
2. 聲明與定義
l 聲明
template<class/typename T>
class ClassName
{
// Class Declaration
void Test();
};
l 定義
直接在類模板內部定義成員函數,也可以在模板類的外部定義成員函數。
template<class/typename T>
void ClassName<T>::Test()
{
}
l 前置聲明
和普通的函數聲明一樣,參數指明與不指明均可。
template<class/typename T> class ClassName;
template<class/typename> class ClassName;
類的類型成員
1. 概念
T::size_type *p;
編譯器無法識別上面是定義一個指針變量p,還是一個T中的一個靜態成員變量size_type與變量p相乘。
所以引入了typename來標識後面T::後面的類型,而不是變量。注意此處只能用typename,不能用class。
struct NEW_TYPE
{
public:
typedef int n_size;
};
template<typename T>
typename T::n_size Set(typename T::n_size _n)
{
typename T::n_size m = 2;
T::n_size n = 3;
T::n_size* p;
return n;
}
VS編譯器不能識別T::n_size是一個靜態常量還是一個類型,所以這個時候需要用typename來標識它是一個類型。VS編譯器能夠識別T::n_size* p;經測試返回類型以及形參類型時必須用typename指定其爲類型。
模板編譯與鏈接
1. 編譯
C++是採用聲明和實現分在兩個文件中,因爲這樣可以使用分離編譯。編譯每個cpp文件,如果遇上外部函數只需要記錄其名字即可。模板代碼,一般放在hpp文件中,即聲明和實現代碼在一起。因爲C++是編譯型語言而不是解釋型語言,所以模型的具體實現代碼編譯時必須確定下來。模板在某種程度上,類似於宏,甚至有人用宏實現類似模板的功能。很多時候一個類模板會包括很多功能,爲了防止代碼膨脹,編譯模板時編譯器會自動識別用到的函數,用到的類,也就是隻編譯用到的。也就是我們模板中,有一些函數如果沒有用一以,哪怕其中有語法錯誤,也不會被編譯到,自然也就不會報錯了。
那麼在多個CPP文件中用到了相同形參的函數模板,因爲CPP是分離編譯的,所以每個CPP文件中都會編譯出一個相同的模板函數
2. 鏈接
每個Cpp編譯之後,生成一個obj的目標文件,然後鏈接器鏈接這些目標文件生成DLL或exe。鏈接的過程中,鏈接器會檢查是否有重複定義,抑或庫衝突之類的。鏈接的時候,鏈接器會使用前面已經生成的模板函數,自動放棄後面生成的模板函數,這樣能夠防止重複定義以及可能避免代碼膨脹導致的exe增大。這種方式導致的一個壞處是,大幅增加編譯時間,因爲用到的模板都會實例化並進行編譯。
模板能不能使用分離編譯呢?網上基本上都說不能,理由是模板實現代碼放在CPP中時,並不知道具體的模板參數類型,所以無法編譯。既然無法知道模板的具體形數,那麼就解決這個問題,告訴編譯器具體的形參。
顯式實例化
1. 概念
顯式實例化(explicit instantiation),就是顯式地告訴編譯器模板形參的類型或值。
extern template declaration; // 外部實例化聲明
template declaration; // 實例化定義
extern template void Test<int>(const int& _t);
template void Test<int>(const int& _t);
extern template class Ctest<char*>;
template class Ctest<char*>;
extern在修飾模板聲明時的作用與聲明全局變量的作用一樣,就是告訴編譯器,當前修飾的聲明已經在其他CPP文件中定義。
實例化定義,一種是直接顯式實例化,另一種是隱式實例化即編譯器識別到CPP中有模板的實例化代碼也同樣會實例化。
外部實例化聲明是爲了解決重複實例化,提升編譯效率。但是習慣了聲明和定義分開的編程習慣。我們同樣可以用顯式實例化來分離編譯。函數模板和類模板的顯式實例化方法一樣的。
2. 實例
h文件只用來存放模板聲明
// fun.h file
template<typename T>
void Test(const T& _t);
cpp文件存放定義及具體的實例化定義,即顯式實例化。這樣實例化肯定只有一次,並且結構清晰。這種方式適用於模板實例化的具體類型不多的情況。因爲如果模板是庫文件,不停修改是不方便的。
// fun.cpp file
template void Test<int>(const int& _t);
template void Test<float>(const float& _t);
template<typename T>
void Test(const T& _t)
{
T t = _t;
}
尾置返回類型
1. 概念
尾置返回類型(trailing return type)是在形參列表後面以->符號開始標明函數的返回類型,並在函數返回類型處用auto代替。尾置返回類型即可以直接指明類型,也可以用decltype推出出類型
2. 實例
auto Function(int i)->int
auto Fun3(int i)->int(*)[5] // 返回指定數組的指針
int n = 10;
auto Function(int i)->decltype(n)
template<class T, class W>
auto Function(T t, W w)->decltype(t+w)
{
return t +w;
}
// 如果是自定義類型,則應該重載+實現t+w
3. 備註
注:C++14中,已經將尾置返回類型去掉了,可以直接用auto推導出類型。
參考:msdn.microsoft.com/en-us/library/dd537655(v=vs.100).aspx
函數模板指針
1. 普通函數指針
因爲C++要兼容C,所以函數名加&與不加都表示函數指針。
l 實例化的模板函數的指針
template<typename T>
void Test(const T& _t)
{
T t = _t;
}
typedef void (*pTest)(const int& _t);
pTest p = Test<int>;
pTest pT = &Test<float>;
p(3);
l 用模板參數來指代函數指針
template <typename T, typename NAME_TYPE>
void TestFun(T fun, NAME_TYPE n)
{
fun(n);
}
TestFun(Test<int>, 4);
TestFun(&Test<double>, 4.0); // 用&與否均可
2. 類成員函數指針
l 類靜態成員函數指針
和普通函數指針一樣,都是__cdecl的調用方式,只能直接調用。
CMyClass<int>::pFunCalc pCalc = CMyClass<int>::Calc;
pCalc();
l 類成員函數指針
成員函數指針聲明時必須是&ClassName::,這是自VS2005之後就必須要求,以前類名加與不加&均可。其實不加&不規範,因爲函數名並不是指針。函數名是對象,取地址是纔是指針。自VS2005之後,類函數名指針必須加&。並且類函數指針調用時,必須指明調用對象標明是__thiscall的調用方式(這是類函數特有的調用方式,因爲它會將類指針作爲參數傳遞進去)。
CMyClass<int> myClass;
CMyClass<int>::pFunSetValue pSet =& CMyClass<int>::SetValue;
myClass.SetValue(4);
(myClass.*(&CMyClass<int>::SetValue))(4);
(myClass.*pSet)(4);
myClass.TestFun(pSet, 4);
myClass.TestFun(&CMyClass<int>::SetValue, 4);
// 模板代碼
template<typename T>
class CMyClass
{
public:
// 普通函數的參數傳遞方式,默認可以不加__cdecl
typedef int (__cdecl *pFunCalc)();
// 標識這是一種__thiscall的參數傳遞方式,類成員函數特有的
typedef void (CMyClass::*pFunSetValue)(const T& _t);
public:
void TestFun(pFunSetValue _pFun, const T& _t)
{
// 成員函數指針必須指明調用對象標明是__thiscall的調用方式
(this->*_pFun)(_t);
(this->*(&CMyClass<T>::SetValue))(4);
// 靜態函數和普通一樣,不用指明調用對象,表明__cdecl的調用方式
(*(&CMyClass<T>::Calc))(); }
void SetValue(const T& _t)
{
m_t = _t;
}
static int Calc()
{
T t = 2;
return t*2;
}
private:
T m_t;
};
// 在另外的類中使用成員函數指針
template<typename T>
class CMyTest
{
public:
typedef void (CMyClass <T>::*pFunSet)(const T& t);
void TestFun(MyClass<T>* p, pFunSet fun, T t)
{
(p->*fun)(t);
}
void Test(MyClass<T>* p, typename CMyClass <T>:: pFunSetValue fun, T t)
{
(p->*fun)(t);
}
};
模板特化與偏特化
1. 概念
l 模板的特化
即模板的特殊化,即模板的通脹算法不能滿足特殊實例。那麼即需要單獨的代碼來處理特殊的實例。而實例是根據不同的形參類型決定的。特化就是處理模板的某一特殊模板形參。形式:
template<typename T1, typename T2> class Test{};
template<typename T1, typename T2> void Set(T1 t1, T2 t2){}
// specialization
template<> class Test<int, int>{};
template<> void Set(int t1, int t2){}
// call the special function
Test<int, int> test;
Set(4, 3);
Set<int, int>(4, 3);
l 模板的偏特化
模板的偏特化只能用於類模板,不能用於函數模板,函數模板只有重載。
template<typename T1, typename T2> class Test{};
template<typename T1, int N> class TestNon{};
// partial specialization
template<typename T1> class Test<T1, int>{};
template<typename T1> class TestNon<T1, 5>{};
Test<char*, int> test;
TestNon<int, 5> testNon;
2. 應用
因爲特化和偏特化均是在編譯時實現的。所以我們能夠將一些邏輯判斷移到編譯期來做。這樣能夠提前測試代碼,因爲編譯期發現錯誤比運行過程中容易。模板元編程就是這樣實現的。另外我們可以用特化來處理一些異常。將異常情況移到特化代碼中處理,這樣主代碼的邏輯就會更簡單清晰。
l 類型模板形參
// Boost中一個例子。
template< typename T >
struct is_pointer
{
static const bool value = false;
};
template< typename T >
struct is_pointer< T* >
{
static const bool value = true;
};
這樣我就可以通過is_pointer<T>::value來判斷當前類型是否爲指針類型。
l 非類型模板形參
Template<bool b>
Struct algo_sort
{
Template<typename T>
Static void sort(T& obj)
{
Quick_sort(obj);
}
}
Template<>
Struct algo_sort<true>
{
Template<typename T>
Static void sort(T& obj)
{
Select_sort(obj);
}
}
這樣就能夠通過模板形參的不同調用不同的排序方法。
仿函數
1. 概念:
仿函數(functor),就是使一個類的使用看上去像一個函數。其實現就是類中實現一個operator(),這個類就有了類似函數的行爲,就是一個仿函數類了。
// alg_for_each.cpp
// compile with: /EHsc
#include <vector>
#include <algorithm>
#include <iostream>
// The function object multiplies an element by a Factor
template <class Type>
class MultValue
{
private:
Type Factor; // The value to multiply by
public:
// Constructor initializes the value to multiply by
MultValue ( const Type& _Val ) : Factor ( _Val )
{
}
// The function call for the element to be multiplied
void operator ( ) ( Type& elem ) const
{
elem *= Factor;
}
};
int main( )
{
std::vector< int> v1;
std::vector< int>::iterator Iter1;
// Constructing vector v1
for ( int i = -4 ; i <= 2 ; i++ )
{
v1.push_back( i );
}
// Using for_each to multiply each element by a Factor
std::for_each ( v1.begin ( ) , v1.end ( ) , MultValue<int> ( -2 ) );
}
2. 解析
上面標註爲紅色的代碼,因爲類MultValue重載了括號運算符,光看代碼,很容易將這裏理解成了括號運算。但這是錯誤的理解。首先我們回到for_each這個函數本身的理解上來,MSDN對第三個參數給出的解釋:User-defined function object that is applied to each element in the range. 可以知道,第三個參數是一個函數對象。
這裏就需要我們有這樣一個概念,如果一個類重載了括號運算符,那麼這個類建立的對象就具有類似函數的功能。這樣的話,那麼第三個參數就可以是一個重載了括號的對象了。
MultiValue<int>& mValue = MultValue<int> ( -2 );
for_each ( v1.begin ( ) , v1.end ( ) , mValue); // 1
for_each ( v1.begin ( ) , v1.end ( ) , MultValue<int> ( -2 ) ); // 2
直接用上面這種方式來寫會好理解多了。再來理解MultValue<int> (-2)這樣一句代碼,它直接生成了一個無名的臨時對象,然後初始化一個引用。MultValue<int> (-2);這樣的一句話,將直接調用構造函數生成一個無名的臨時對象。這樣看來,上面的語句1和語句2其實是一個意思。
for_each函數的第三個函數本來可以直接用一個用戶定義的函數來完成,但是爲什麼MSDN中多用重載類的括號運算符來完成這樣的功能呢?通過上面的例子,我們可以發現,主要表現類的構造函數上,可以初始化不同的參數,這一點是自定義的函數所不具備的。另外,利用結構體還能把可能用到的函數封裝到一個結構體中,便於管理。
這樣回頭看以前常用的sort函數。
vector<int> vInt;
sort(vInt.begin(), vInt.end(), greater<int> ());
sort(vInt.begin(), vInt.end(), less<int> ());
// STL中的less<int>的代碼.
// TEMPLATE STRUCT less
template<class _Ty>
struct less
: public binary_function<_Ty, _Ty, bool>
{ // functor for operator<
bool operator()(const _Ty& _Left, const _Ty& _Right) const
{ // apply operator< to operands
return (_Left< _Right);
}
};
// 在C++中struct基本上和class一個意思
greater<int>()和less<int>()也是直接構建一個無名對象,然後調用重載的括號運算符。
其他
l 函數模板可以重載,和普通函數重載類似,函數模板重載某些時候能也達到特化的效果。
template<typename M, typename N> void TestSpec(M m, N n){}
template<typename M> void TestSpec(M m, int n){}
void TestSpec(int m ,int n){}
l 類模板中,還可以添加成員模板函數。
l 友元模板類需要前到前置聲明。
l 派生類只能從模板類派生(類模板的一個實例化)。