C++模板常用功能講解

前言

泛型編程是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.    聲明和定義

函數模板與普通函數一樣,聲明與定義可以分開,也可以一起。

聲明

template<typename T>

bool Compare(T t1, T t2);

定義

template<typename T>

bool Compare(T t1, T t2)

{

return (t1 > t2);

}

 

類模板

1.    概念

類模板根據模板形參的不同,生成不同的模板類。

template<class/typename T, ……>

class ClassName

{

// Class definition

};

類模板,在沒有實例化之前,只是一個生成類的模板。

而在具體的實例化之後,其實就生成了一個具體的模板類。

2.    聲明與定義

聲明

template<class/typename T>

class ClassName

{

// Class Declaration

void Test();

};

定義

直接在類模板內部定義成員函數,也可以在模板類的外部定義成員函數。

template<class/typename T>

void ClassName<T>::Test()

{

}

前置聲明

和普通的函數聲明一樣,參數指明與不指明均可。

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,所以函數名加&與不加都表示函數指針。

實例化的模板函數的指針

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);

用模板參數來指代函數指針

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.    類成員函數指針

類靜態成員函數指針

和普通函數指針一樣,都是__cdecl的調用方式,只能直接調用。

CMyClass<int>::pFunCalc pCalc = CMyClass<int>::Calc;

pCalc();

類成員函數指針

成員函數指針聲明時必須是&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.    概念

模板的特化

即模板的特殊化,即模板的通脹算法不能滿足特殊實例。那麼即需要單獨的代碼來處理特殊的實例。而實例是根據不同的形參類型決定的。特化就是處理模板的某一特殊模板形參。形式:

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);

模板的偏特化

模板的偏特化只能用於類模板,不能用於函數模板,函數模板只有重載。

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.    應用

因爲特化和偏特化均是在編譯時實現的。所以我們能夠將一些邏輯判斷移到編譯期來做。這樣能夠提前測試代碼,因爲編譯期發現錯誤比運行過程中容易。模板元編程就是這樣實現的。另外我們可以用特化來處理一些異常。將異常情況移到特化代碼中處理,這樣主代碼的邏輯就會更簡單清晰。

類型模板形參

 // 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來判斷當前類型是否爲指針類型。

非類型模板形參

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  派生類只能從模板類派生(類模板的一個實例化)。

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