【Cpp】第十章-模板進階

模板進階

  之前的博客已經介紹過模板的概念,這是Cpp在實現泛型編程中不可缺少的一環,在模板進階的討論中會着重於模板的更爲高級的使用。

非類型模板參數

使用

  在模板中我們通常都是定義一個類型模板參數,在進行實例化的時候通過傳入類型來實例化模板,但是模板中也可以定義非類型的模板參數。用一個封裝的定長順序表進行舉例。

#include <iostream>
#include <assert.h>
using namespace std;
template<class T, size_t L>
class Array
{
public:
    Array()
    :_arr({0})
    ,_size(0)
    {}
    size_t Size() const
    {
        return _size;
    }
    T& operator[](size_t pos)
    {
        assert(pos < _size);
        return _arr[pos];
    }
    void Push_Back(T data)
    {
        if(_size < L)
        {
            _arr[_size] = data;
            _size++;
        }
        else
        {
            cout << "Array is full" << endl;
        }
    }
    void Pop_Back()
    {
        if(!Empty())
        {
            _size--;
        }
        else
        {
            cout << "Array is empty" << endl;
        }
        
    }
    bool Empty() const
    {
        return _size == 0;
    }
private:
    T _arr[L];
    size_t _size;
};
int main()
{
    Array<int, 20> arr;
    arr.Push_Back(1);
    arr.Push_Back(2);
    arr.Push_Back(2);
    arr.Push_Back(5);
    arr.Push_Back(7);
    arr.Push_Back(5);
    arr.Pop_Back();
    arr.Pop_Back();
    arr.Pop_Back();
    for(int i = 0; i < arr.Size(); i++)
    {
        cout << arr[i] << endl;
    }
}

注意

  非類型模板參數在使用中有很嚴格的要求,它必須遵循以下規則,其實這塊的要求在網上大部分博客中都說的十分模糊並且在我的實驗下發現過於片面和侷限,因此我給出我的總結和理解。
  1、非類型模板參數可以是整形,指針和引用。
  2、再給模板參數傳參時要求其必須是一個常量表達式。
  3、如果模板參數是一個整形,那麼在傳參的時候可以傳入字面值常量,也可以是全局/局部常量,可以是外部/內部鏈接(關於鏈接屬性參考其他博客)。
  4、如果模板參數是一個指針或者引用,那麼傳參時要求,如果傳入變量,變量必須是全局的,如果是常量常量必須是外部鏈接屬性的,並且不能把動態對象的指針或引用傳入。也就是說局部變量和內部鏈接屬性的常量是不可以當作模板參數構造模板的,並且指針和引用還不能是動態對象的。
  以上是我個人在環境下多次實驗得出的理解和總結,環境是gcc 6.3.0,如果有誤區還請指出。

模板的特化

  模板可以封裝不同的類型做相同的操作,然而有這麼一種情況,我想要根據不同的類型做出不同於原來模板的操作,這就需要用到模板的特化這個語法。特化就是在原模版的基礎上,根據特殊類型進行特殊化的實現方式。

函數模板的特化

  函數模板特化要求當然要有基本的函數模板,在特化時格式要求template後跟一對尖括號,並且函數名後尖括號內寫特化的模板參數。參數及函數體中的模板參數必須全部替換爲特化的模板實參,不然會報錯。

#include <iostream>
using namespace std;
//爲了驗證函數模板與類模板
//先寫一個函數模板
template<class T>
T Add(T a, T b)
{
    return a + b;
}
//特化處理
template<>
int Add<int>(int a, int b)
{
    cout << "specialization" << endl;
    return a + b;
}
int main()
{
    cout << Add(1, 2) << endl;
}


specialization
3

  這樣的特化寫法是最爲規範的寫法,就像是函數模板的顯示實例化一樣,當然我們也可以將<int>不要,只要能讓模板知道你再特化哪一種情況即可,但是要與模板完全符合,不然是編不過的。
  還有一種更爲簡單的方式,及利用我在模板初階中提到的模板匹配規則。模板匹配是在所有同名函數都不符合調用的情況下才會進行實例化,因此我們可以用類似重載的情況寫同名函數來進行處理,在調用時會優先調用非模板函數。這種情況並不能算是模板的特化處理,但是可以用來處理一些模板無法匹配的情況。

#include <iostream>
using namespace std;
template<class T>
T Add(T& a, T& b)
{
    return a + b;
}
template<>
int Add(int& a, int& b)
{
    cout << "specialization" << endl;
    return a + b;
}
int Add(int a, int b)
{
    cout << "overload" << endl;
    return a + b;
}
int main()
{
    int a = 1;
    int b = 2;
    cout << Add(a, b) << endl;//調用重載的Add
    cout << Add<int>(a, b) << endl;//調用特化的Add
}


overload
3
specialization
3

  在這個例子中可以看出根據模板匹配規則確實優先調用了我們重載的函數,我們只有顯示實例化纔會調用模板。

類模板的特化

  類模板特化與函數模板類似,但是由於類模板是無法通過其他方式識別模板參數的類型的,因此我們必須通過<>來顯示實例化才能進行特化。

#include <iostream>
using namespace std;
//類模板
template<class T1, class T2>
class Data
{
public:
    Data()
    {
        cout << "Data<T1, T2>" << endl;
    }
private:
    T1 _data1;
    T2 _data2;
};
//特化處理
template<>
class Data<int, char>
{
public:
    Data()
    {
        cout << "Data<int, char>" << endl;
    }
private:
    int _data1;
    char _data2;
};
int main()
{    
    Data<int, int> data1;
    Data<int, char> data2;
}


Data<T1, T2>
Data<int, char>

特化的種類

全特化

  全特化就是將模板參數全部進行實例特化的情況。這裏用類模板舉例。

#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
    Data()
    {
        cout << "Data<T1, T2>" << endl;
    }
private:
    T1 _data1;
    T2 _data2;
};
//這樣的把所有的模板參數都進行實例特化的就叫全特化
template<>
class Data<int, char>
{
public:
    Data()
    {
        cout << "Data<int, char>" << endl;
    }
private:
    int _data1;
    char _data2;
};
int main()
{    
    Data<int, int> data1;
    Data<int, char> data2;
}


Data<T1, T2>
Data<int, char>

偏特化

  偏特化就是全特化以外的情況,並沒有將所有的模板參數全部都實例化爲某一特殊情況。偏特化又有兩種情況。
  1、部分特化:部分特化是將模板參數中一部分模板參數進行實例化特化的情況。

#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
    Data()
    {
        cout << "Data<T1, T2>" << endl;
    }
private:
    T1 _data1;
    T2 _data2;
};
//這樣的把所有的模板參數都進行實例特化的就叫全特化
template<>
class Data<int, char>
{
public:
    Data()
    {
        cout << "Data<int, char>" << endl;
    }
private:
    int _data1;
    char _data2;
};
//這裏就是一個部分特化,只將第一個參數進行實例化的情況
template<class T>
class Data<int, T>
{
public:
    Data()
    {
        cout << "Data<int, T>" << endl;
    }
};
int main()
{
    Data<int, int> data1;//這裏會調用部分特化
    Data<int, char> data2;//這裏調用全特化
}



Data<int, T>
Data<int, char>

  通過這個例子還可以證實如果實例化同時符合全特化和部分特化的特化情況,則會優先調用全特化,之後纔會考慮部分特化的情況。
  2、將參數進一步限制的特化
  這個類型的特化是將參數進行了進一步的限制,我們可以將參數限制爲指針類型或者引用,在這種情況下才會調用這種類型的特化

#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
    Data()
    {
        cout << "Data<T1, T2>" << endl;
    }
private:
    T1 _data1;
    T2 _data2;
};
//這樣的把所有的模板參數都進行實例特化的就叫全特化
template<>
class Data<int, char>
{
public:
    Data()
    {
        cout << "Data<int, char>" << endl;
    }
private:
    int _data1;
    char _data2;
};
//這裏就是一個部分特化,只將第一個參數進行實例化的情況
template<class T>
class Data<int, T>
{
public:
    Data()
    {
        cout << "Data<int, T>" << endl;
    }
};
//將兩個參數類型限制爲指針類型,如果兩個參數都是指針類型則調用這個特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
    Data()
    {
        cout << "Data<T1*, T2*>" << endl;
    }
};
int main()
{
    Data<int, int> data1;//這裏會調用部分特化
    Data<int, char> data2;//這裏調用全特化
    Data<int*, char*> data3;//這裏調用類型限制的特化
}

類型萃取

  類型萃取是通過特化的方式使模板可以識別不同的類型從而使用不同的處理方法。比如說我們要寫一個拷貝函數,對於內置類型我們直接調用memcpy()函數拷貝內存,而對於自定義類型我們可能需要使用自定義的其他拷貝方法進行深拷貝,這時候就可以使用類型萃取的方式來區別內置類型和自定義類型達到不同處理方式的結果。
  這並不是一種新的語法,更像是一種設計模式,在STL中就有所體現。

模板的分離編譯

  什麼是分離編譯模式?分離編譯模式就是我們通常在寫項目是方便項目管理時所使用的將函數和類的聲明放進.h文件中而在.cpp文件中寫類定義和類成員函數定義的模式。這種方法可行就是應爲我們將聲明寫入.h而在需要使用的地方引入頭文件,在鏈接過程中編譯器會根據聲明找到具體實現定義的地址完成鏈接。

模板不支持分離編譯

  但是這裏要說的是,無論是函數模板還是類模板在沒有實例化之前都是沒有具體代碼的,那麼也就沒有具體定義的地址,我們根據聲明也就無法鏈接到定義的地方。

// 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;
}

  以上這個例子中的代碼時編不過的,原因就是模板不支持分離編譯,無法連接。

如何解決?

  解決方法也很簡單,最簡單的就是將定義和聲明都寫到頭文件中可以了,這樣就省去了鏈接這個步驟,也就不存在錯誤了。

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