模板

在我們編寫代碼時,我們會遇見這種情況:
比如交換函數,當我們要交換的類型是int(傳的參數爲int型)時,我們要編寫的swap函數的形參就應該是int,但當我們要交換的是double型時,我們還要再寫一個swap函數來滿足要求。每換一種類型就要再重載一個swap函數來滿足條件。
雖然通過這方法重載實現所有類型的交換函數,但是這種方法有幾個不好的地方,一是重載函數僅僅類型不同,導致代碼的複用率很低,只要有新類型出現,就要增加對應的函數;再者代碼的可維護性比較低,一個出錯可能所有的重載都出錯,要一個一個改。

通過上面的例子,我們想能不能告訴編譯器一個模子,編譯器可以通過不同的類型利用這樣的模子自動生成適合各種類型的函數。答案是當然可以
即泛型編程:編寫與類型無關的通用代碼,而模板是泛型編程的基礎。

下面我們來鄭重的引入模板

模板分爲函數模板和類模板

函數模板:

  • 什麼是函數模板?
    函數模板是一個與類型無關,並且對所有類型都適用的函數,在使用時函數可被參數化,根據實參類型結合模板產生函數的特定類型版本實現函數功能。
  • 如何使用?
    template <typename T1,typename T2...>
    返回值 函數名(參數列表){ }
    typename是用來定義模板參數關鍵字的,也可以用class
    例如:

    template<typename T>   
    void Swap(T &x, T &y)      
    //之前不同的類型,對應不同的形參列表,所以要寫多個重載函數,但現在只寫一個模板函數,編譯器就可以根據這個模板結合傳入的參數就可完成所有類型的生成對應類型的函數以供調用
    
    T tmp = x;
    x = y;
    y = tmp;
    }
    //在這個函數中,T只能被替換爲一樣的類型,若傳入參數不同,則編譯器其則生成不了匹配的函數,而編譯器又不會進行類型轉換,因而會報錯
    //若想要不同類型,可定義兩個T
    template<class T1, class T2>       //typename可用class代替
    void Swap(T1 &x, T2 &y)     
    {
    T1 tmp = x;
    x = y;
    y = tmp;
    }
    int main()
    {
    int x1=0, y1=1;
    double x2 = 1.0, y2 = 2.0;
    Swap(x1, y1);
    Swap(x2, y2);
    Swap(x1, y2);
    }
  • 函數模板原理
    其實模板本身並不是函數,而是編譯器用使用方式產生特定具體類型函數的模具。所以其實模板就是將本來應該我們做的重複的事情交給了編譯器。在編譯器編譯階段,編譯器會根據傳入的實參類型來推演生成對應類型的函數以供調用。比如:當用double類型使用函數模板時,編譯器通過對實參類型的推演,將T確定爲double類型,然後產生一份專門處理double類型的代碼,對於字符類型也是如此。
  • 函數模板實例化
    編譯器根據不同的參數用模板推演不同的函數稱爲函數模板的實例化,模板參數實例化分爲:隱式實例化和顯示實例化。
    通過下面例子介紹隱式實例化和顯示實例化

    template<typename T>
    T Add(const T &x, const T &y)
    {
    return x + y;
    }
    int main()
    {
    int x1=0, y1=1;
    double x2 = 1.0, y2 = 2.0;
    //隱式實例化,讓編譯器根據實參推演模板參數的實際類型
    Add(x1, y1);     //隱式實例化
    
    //當無法確定T是什麼,由該推演成什麼的時候,會報錯,因而要不然進行強轉使傳入參數類型相同,要不然進行顯式實例化
    Add(x1, (int)y2); //強轉
    
    //顯式實例化,直接告訴編譯器,由模板該推演成什麼,在函數名後的<>中指定模板參數的實際類型
    Add<int>(x1, (int)y2);   
    }
  • 模板參數匹配原則

1.一個非模板函數可以和一個同名的函數模板同時存在,而且該函數模板還可以被實例化爲這個非模板函數。
2.模板函數不允許自動類型轉換,但普通函數可以進行自動類型轉換。
3.對於非模板函數和同名函數模板,如果其他條件都相同,在調動時會優先調用非模板函數而不會從該模板產生出一個實例。如果模板可以產生一個具有更好匹配的函數,那麼將選擇模板。

int Add(int x, int y)
{
    return x + y;
}
template<typename T>
T Add(const T &x, const T &y)
{
    return x + y;
}
template<typename T1, typename T2>
T1 Add(const T1 &x, const T2 &y)
{
    return x + y;
}

int main()
{
//1
    Add(1, 2);    //調用非模板函數,無需模板實例化
    Add<int>(1, 2);//調用編譯器特化的模板函數版本   --如果指定類型就必須用模板來生成相應類型
    //2.
    Add(1, 2.0);      //此處會調用非模板函數,發生隱式類型轉換   (說明:此處還未加Add(const T1 &x, const T2 &y)函數模板)
    //3.
    Add(1, 2.0);        //選擇函數模板若選擇非模板函數,則會發生隱式類型轉化,
                        //不如調用Add(const T1 &x, const T2 &y)這個實例化的函數形參列表更匹配

}

類模板

  • 使用格式:
    template <class T1,class T2....>
    class A( /A爲類模板名,A不是具體的類,是編譯器根據被實例化的類型生成具體類的模具)
    {
    ...
    };

  • 類模板實例化

類模板實例化需要在類模板名字後跟<>,然後將實例化的類型放在<>中即可,類模板名字不是真正的類,而實例化的結果纔是真正的類。
例如容器vector的實現:

template <class T>
class vector{
    typedef T* iterator;
    public:
        //構造析構
        vector()
            : _start(nullptr)
            , _finish(nullptr)
            , _endOfStorage(nullptr)
        {
        }
        vector(int n, const T &value = 0)
            :_start(nullptr)
            , _finish(nullptr)
            , _endOfStorage(nullptr)
        {
            _start = new T[n+1];
            int i = n;
            while (i--)
            {
                _start[i] = value;
            }
            _start[n] = '\0';
            _finish = _start + n;
            _endOfStorage = _finish;
        }
        template <class InputIterator>
        vector(InputIterator first, InputIterator last)    
            :_start(nullptr)
            , _finish(nullptr)
            , _endOfStorage(nullptr)
        {
            int n=0;
            auto tmp = first;
            while (tmp != last)                
            {
                n++;
                tmp++;
            }
            _start = new T[n + 1];
            _finish = _start + n;
            _endOfStorage = _finish;

            last--;
            while (n--)
            {
                _start[n] = *last;
                last--;
            }
        }
        vector(const vector<T>& v)
            :_start(nullptr)
            , _finish (nullptr)
            ,_endOfStorage(nullptr)
        {
            reserve(v.capacity());
            //memcpy(_start, v._start,v.size());
            for (int i = 0; i < v.size(); i++)
            {
                _start[i] = v._start[i];
            }
            _finish = _start + v.size();
            _endOfStorage = _start + v.capacity();
        }
        ~vector()
        {
            delete[] _start;
            _start = nullptr;
            _finish = nullptr;
            _endOfStorage = nullptr;
        }
        iterator begin()
        {
            return _start;
        }
        iterator end()
        {
            return _finish;
        }

        int size()const
        {   
            return _finish - _start;
        }
        int capacity()const
        {
            return  _endOfStorage - _start;
        }

friend iterator find(iterator begin, iterator end, const T &value);
    private:
        T *_start;
        T *_finish;
        T *_endOfStorage;

    };
// 注意:類模板中函數放在類外進行定義時,需要加模板參數列表
    template <class T>
    T* find(T* begin, T* end, const T &value)
    {
        auto it = begin;
        while (it != end)
        {
            if (*it == value)
            {
                return it;
            }
            it++;
        }
        return nullptr;
    }
int main(){
vector<int> v1;   //實例化
}

模板的其他知識說明:

模板參數

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

例如:
namespace Eg
{
    template <class T,size_t N=10>     //N在下面類中,作爲常數使用
    class array{
    public:
        size_t size()const
        {
            //N = 20;      //會報錯:錯誤 1   error C2106: “=”: 左操作數必須爲左值,可證明N是常數
            return _size;
        }
    private:
        T _array[N];
        size_t _size;
    };
}
void Test1(){
    Eg::array<int> x;     
    x.size();
}
  • 注:
    1.浮點數、類對象以及字符串是不允許作爲非類型模板參數
    Eg::array<int, 2.0> x3; //這樣創建對象會報錯
    2.非類型模板參數要在編譯期就能確認結果,比如1+2可以在編譯時確認,而變量a+變量b不可以確認因而會報錯
    Eg::array<int, 2+1> x2; //√
    //int a = 1, b = 2;
    //Eg::array<int, a+b> x3; //×

模板特化

對於一些特殊的類型(比如指針類型),使用已寫的模板可能達不到我們想要的結果,得出錯誤的結果。
例如:

template <class T>
T& MAX_T(T& left, T& right)
{
    return left>right?left:right;
}
void Test(){
    int x = 2,y=3;
    cout << MAX_T(x, y) << endl;  
    char *p1 = "wello";
    char *p2 = "hello";
    cout << MAX_T(p1, p2) << endl;   //應該輸出wello  但因爲比較的是地址卻輸出hello不符合邏輯,因此我們要爲char*來特化一個模板提供給這種類型
}

通過上述列子我們可以通過對模板進行特化,在原來模板函數(或類)的基礎上,針對特殊類型進行特殊化的實現。比如上述例子中爲char*特化一個函數,按照我們的想法針對這種類型進行特殊化處理,得到正確的結果。

模板特化又分爲函數模板特化與類模板特化

函數模板特化

函數模板的特化方式:
1.要先有一個基礎函數模板
2.關鍵字template後面接一堆空的尖括號<>
3.函數名後跟一對尖括號裏面放需要特化的類型
4.函數形參表必須要和模板函數的基礎參數類型完全相同,不同的話編譯器可能會報一些錯誤

//比如針對上述char*類型特化:
template <class T>
T& MAX_T(T& left, T& right)
{
    return left>right?left:right;
}
template <>
char*& MAX_T<char*>(char*& left, char*& right)
{
    if (strcmp(left, right) == 1)
    {
        return left;
    }
    else{
        return right;
    }
}
void Test(){
    char *p1 = "wello";      
    char *p2 = "hello";
    cout << MAX_T(p1, p2) << endl;   
    //會調用char*& MAX_T<char*>(char*& left, char*& right)特化模板函數
}

但是!!
對於特例化的函數模板,一般都是將該函數直接給出,一是實現簡單,二是因爲函數模板可能會遇到不能處理或者處理有誤的類型

類模板特化

類模板特化又分爲全特化和偏特化

  1. 全特化:即將模板參數列表中所有的參數都確定了
template<class T1, class T2>
class Data1
{
public:
    Data1() { cout << "Data<T1, T2>" << endl; }
private:
    T1 _d1;
    T2 _d2;
};

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

void Test(){
    Data1<int, int> d1;      //用特化模板參數類
    Data1<int, double> d2;
}

2.偏特化:有兩種表現:部分特化和參數更進一步的限制

template<class T1, class T2>
class Data2
{
public:
    Data2() { cout << "Data2<T1, T2>" << endl; }
private:
    T1 _d1;
    T2 _d2;
};
  • 部分特化:將模板參數列表中部分參數類型化
template<class T1>
class Data2<T1, int>    //特化一半
{
public:
    Data2()
    {
        cout << "Data2<T1, int>" << endl;
    }

private:
    T1 _d1;
    int _d2;
};
void Test(){                     
    Data2<int, int> d1;      //用部分特化模板參數類
    Data2<double, int> d2;   //用部分特化模板參數類     因爲後面的都是int,都符合這個部分特化模板
    Data2<int, double> d3;
    Data2<double, double> d4;
    }
  • 參數更進一步的限制:讓模板參數列表中的類型限制更加嚴格
template<class T1, class T2>
class Data2<T1*, T2*>
{
public:
    Data2()
    {
        cout << "Data2<T1*, T2*>" << endl;
    }
private:
    T1* _d1;
    T2* _d2;
};
void Test(){                     
    Data2<int*, int> d5;    //使用class Data2<T1, int>
    Data2<int, int*> d6;    //使用template<class T1, class T2> class Data2{}
    Data2<int*, int*> d7;  //使用class Data2<T1*, T2*>
    Data2<int*, double*> d8; //使用class Data2<T1*, T2*>
}

類型萃取

通過以下題目來感受類型萃取

// 寫一個通用的拷貝函數,要求:效率儘可能高

/*Way1:
//String:用該函數會報錯拷貝的是地址,析構會對同一塊空間釋放兩次,會報錯
template<class T>
void Copy(T* dst, T* src, size_t size)
{
memcpy(dst, src, sizeof(T)*size);
}
*/

//因而要區分自定義和內置類型,來調用使用不同的方法進行拷貝
/*
//Way2:增加函數判定 區分自定義和內置類型
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(arrType) / sizeof(arrType[0]); ++i)      //每次都要遍歷,效率太低!
    {
        if (0 == strcmp(strType, arrType[i]))
            return true;
    }
    return false;
}

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

//Way3:萃取類型
//代表內置類型
struct TrueType{
    static bool Get(){     //只有靜態才能用 ::  訪問
        return true;
    }
};
//代表自定義類型
struct FlaseType{
    static bool Get(){
        return false;
    }
};

template<class T>
struct TypeTraits{
    typedef FlaseType IsPodeType;
};

//對上述模板進行實例化,將內置類型都特化
template<>
struct TypeTraits<int>{
    typedef TrueType IsPodeType;
};
template<>
struct TypeTraits<double>{
    typedef TrueType IsPodeType;
};
/*
T爲int:TypeTraits<int>已經特化過,程序運行時就會使用已經特化過的TypeTraits<int>, 該類中的IsPODType剛好爲類TrueType,而TrueType中Get函數返回true,內置類型使用memcpy方式拷貝
T爲string:TypeTraits<string>沒有特化過,程序運行時使用TypeTraits類模板, 該類模板中的IsPODType剛好爲類FalseType,而FalseType中Get函數返回true,自定義類型使用賦值方式拷貝
*/

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

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

    String s1[3] = { "1111", "2222", "3333" };
    String s2[3];            
    Copy(s2, s1, 3);
}

模板分離編譯

要說模板分離編譯我們就要先來談談什麼是分離編譯

分離編譯

一個工程中有很多文件,但他們分爲兩大類:頭文件和源文件
程序的運行有以下五個步驟:
預處理-->編譯-->彙編-->鏈接
頭文件會在預處理階段展開,在預處理期間程序會將頭文件中的內容複製一份到源文件。參與編譯的只有源文件,而且每個源文件都單獨進行編譯生成目標文件,最後將所有目標文件鏈接形成單一的可執行文件。如下圖:
模板

目標文件鏈接的時候,是通過找函數地址(入口)進行調用的。

注意:強調:每個源文件都單獨進行編譯

模板的分離編譯

對於模板:
實例化之前編譯器只會做一些簡單的語法測驗,不會生成處理具體類型的代碼
實例化期間編譯器用過推演形參類型來確保模板參數,通過列表中T的實際類型在生成處理具體類型的代碼

//頭文件 CompilingTest.h
template <class T>
T Add(T left,T right);

//源文件 CompilingTest.c
#include "CompilingTest.h"
template <class T>
T Add(T left, T right)
{
    return left + right;
}

//源文件 main.c
#include "CompilingTest.h"
int main()
{
    Add(1.0, 1.0);   //會發生報錯,因爲沒有實例化Add(int,int),找不到匹配的函數入口地址,因而在鏈接時會報錯
    }
//頭文件 CompilingTest.h
template <class T>
T Add(T left,T right);

//源文件 CompilingTest.c
#include "CompilingTest.h"
template <class T>
T Add(T left, T right)
{
    return left + right;
}
void tmp(){
    Add(1, 2);         //在此編譯器推演出了T->int的函數,在鏈接時找到了適合Add(1, 1);該函數的入口地址,因而纔不會報錯
}

//源文件 main.c
#include "CompilingTest.h"
int main()
{
    Add(1, 1);   //這樣就不會發生錯誤了
    //Add(1.0, 1.0);  //無匹配的Add(double,,double)
}

通過上述例子可得模板不支持分離編譯
模板

解決方法:

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

【分離編譯擴展閱讀】 http://blog.csdn.net/pongba/article/details/19130

模板優缺點

優點:

  1. 模板複用了代碼,節省資源,更快的迭代開發,C++的標準模板庫(STL)因此而產生
  2. 增強了代碼的靈活性
    缺點
  3. 模板會導致代碼膨脹問題,也會導致編譯時間變長
  4. 出現模板編譯錯誤時,錯誤信息非常凌亂,不易定位錯誤
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章