天天看點

第16章 模闆和泛型程式設計【C++】

第16章 模闆和泛型程式設計

已經學習了标準容器,我們就會産生好奇,為什麼它可以存儲任意類型呢?向自定義的函數的形參與實參都是有着嚴格的類型比對,面向對象程式設計和泛型程式設計都能處理在編寫程式時不知道類型的情況,不同之處在于,OOP能處理類型在程式運作之前都未知的情況,而泛型程式設計中,在編譯時就能獲知類型了,在OOP總我們知道利用虛函數與動态綁定機制可以做到

為什麼使用泛型程式設計

有時某種算法的代碼實作是相同的,隻有變量類型不同,如下面的情況

int compare(const string& s1,const string& s2){
    if(s1<s2)return -1;
    if(s2<s1)return 1;
    return 0;
}
int compare(const double& d1,const double& d2){
    if(s1<s2)return -1;
    if(s2<s1)return 1;
    return 0;
}      

泛型程式設計就是為解決這種問題而生的

函數模闆

函數模闆就是一個公式,可以來生成針對特定類型的函數版本

編譯器生成的版本通常被稱為模闆的執行個體

//example1.cpp
//模闆定義以template關鍵詞開始,後面跟模闆參數清單,是一個逗号隔開一個或多個模闆參數的清單
template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2)
        return -1;
    if (v2 < v1)
        return 1;
    return 0;
}

int main(int argc, char **argv)
{
    //編譯器背後生成 int compare(const int& v1,const int& v2)
    cout << compare(10, 13) << endl; //-1
    //生成 int compare(const string& v1,const string& v2)
    cout << compare(string{"hello"}, string{"asd"}) << endl; // 1
    return 0;
}      

typename與class

泛型參數的類型确定是編譯器時檢測被調用時的實參的類型确定的

​​

​template<class ...>​

​​與​

​template<typename ...>​

​兩種方式都是可以的,但是現代C++更推薦typename即後者

template<class T,typename U>
T func(T*ptr,U*p){
    T& tmp=*p;
    //...
    return tmp;
}      

但func被調用時,編譯器根據T的類型,将模闆中的T類型替換為實參類型

非類型模闆參數

在形參中有些值類型是我們已經确定的,但是不能确定是多少或具體内容,這是可以使用非類型模闆參數

編譯器會使用字面常量的大小代替N和M,執行個體化模闆

//example2.cpp
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    cout << N << " " << M << endl; // 6 4
    return strcmp(p1, p2);
}

int main(int argc, char **argv)
{
    cout << compare("hello", "abc") << endl; // 1
    //在此實際傳的實參為 char[6] char[4]
    return 0;
}      

上面編譯器會執行個體出​

​int compare(const char (&p1)[6], const char (&p2)[4])​

重點:非類型模闆參數的模闆實參必須是常量表達式

inline和constexpr的函數模闆

inline與constexpr普通函數關鍵詞的位置沒什麼差別

  • inline函數模闆
template <typename T>
inline int compare(const T&a,const T&b){
    return 1;
}      
  • constexpr函數模闆
template <typename T>
constexpr int compare(const T&a,const T&b){
    return 1;
}
constexpr int num=compare(19,19);
cout<<num<<endl;//1      

編寫類型無關的代碼

标準函數對象的内容在第14章 操作重載與類型轉換

在模闆程式設計中,我們力求編寫類型無關的代碼,盡可能減少對實參的依賴,​​

​總之模闆程式應該盡量減少對實參類型的要求​

在上面的代碼是有兩個特殊的處理

  • 模闆中的函數參數是const的引用(保證可處理不能拷貝的類型)
  • 函數體中的判斷條件僅使用​

    ​<​

    ​比較(使得類型僅支援<比較即可)

還有更優雅的寫法,使用标準函數對象

//example3.cpp
template <typename T>
int compare(const T &v1, const T &v2)
{
    if (less<T>()(v1, v2))
        return -1;
    if (less<T>()(v1, v2))
        return 1;
    return 0;
}

int main(int argc, char **argv)
{
    cout << compare(string{"121"}, string{"dsc"}) << endl; //-1
    return 0;
}      

函數模闆通常放在頭檔案

我們通常将類的定義與函數聲明放在都檔案,因為使用他們時,編譯器隻需掌握其形式即可即傳回類型,函數形參類型等,但是函數模闆不同,為了生成一個執行個體化版本,編譯器需要掌握函數模闆或類模闆成員函數的定義,模闆的頭檔案通常包括聲明與定義

編譯錯誤過程

對于函數模闆的錯誤,通常編譯器會在三個階段報告錯誤

1、編譯模闆本身,例如定義模闆本身的文法等

2、遇到模闆被使用時,通常檢查實參數目、檢查參數類型是否比對

3、編譯用函數模闆産生的函數代碼,與編譯實際的函數一樣,依賴于編譯器如何管理執行個體化,這類錯誤可能在連結時才報告

如下面的情況

Person a,b;
compare(a,b);      

compare中使用了​

​<​

​​,但是Person類并沒有​

​<​

​操作,那麼這樣的錯誤在第三階段才會報告

類模闆

經過上面的學習,函數模闆是用來生成函數的藍圖的。那麼類模闆有是怎樣的呢,類模闆(class template)是用來生成類的藍圖的,不像函數模闆一樣可以推算類型,類模闆使用時在名字後使用尖括号提供額外的類型資訊,正如我們使用過的list、vector等一樣,它們都是類模闆

定義類模闆

下面是一個定義類模闆的簡單例子,無須解釋即可學會

//example4.cpp
template <typename T>
class Data
{
public:
    T info;
    Data(const T &t) : info(t)
    {
    }
};

int main(int argc, char **argv)
{
    Data<int> data1(19);
    cout << data1.info << endl; // 19
    Data<string> data2("hello");
    cout << data2.info << endl; // hello
    return 0;
}      

執行個體化類模闆

在使用一個類模闆是,必須提供額外的資訊,在example4.cpp中提供的int就是顯式模闆實參,它們被綁定到模闆參數

每一個類模闆的每個執行個體都形成一個獨立的類,​​

​Data<int>​

​與其他的Data類型直接沒有關聯,也不會對其他Data類型的成員有特殊通路權限

模闆類型做實參

類模闆的類型實參可以為普通類型或者自定義類型,同時也可以為模闆類型

例如用vector<>來做實參類型

//example5.cpp
template <typename T>
class Data
{
public:
    T info;
    Data(const T &t) : info(t)
    {
    }
};

int main(int argc, char **argv)
{
    Data<vector<int>> data({1, 2, 3, 4});
    for (const int &item : data.info)
    {
        cout << item << endl; // 1 2 3 4
    }
    return 0;
}      

類模闆的成員函數

在類外定義的類模闆的成員函數必須添加template在函數定義前,在類内定義在與普通類一樣其被定義為隐式的内聯函數

//example6.cpp
template <typename T>
class Data
{
public:
    T info;
    Data(const T &t) : info(t)
    {
    }
    void print()
    {
        cout << "print" << endl;
    }
    T sayHello(const T &t); //類内聲明
};

//類外定義
template <typename T>
T Data<T>::sayHello(const T &t)
{
    info = t;
    cout << "hello" << endl;
    return this->info;
}

int main(int argc, char **argv)
{
    Data<int> data(19);
    data.print();                // print
    int res = data.sayHello(18); // hello
    cout << res << endl;         // 18
    return 0;
}      

模闆參數視為已知類型

在類模闆中像在函數模闆中一樣,将模闆參數視為已知就好,以至于可以進行複雜的情況使用

在類模闆中使用其他類模闆時,可以使用自己的模闆類型參數作為參數傳給其他類模闆,例如下面的​​

​vector<T>、initializer_list<T>​

​等。

//example7.cpp
template <typename T>
class Data
{
public:
    shared_ptr<vector<T>> vec;
    Data(const initializer_list<T> &list) : vec(make_shared<vector<T>>(list))
    {
    }
    vector<T> &get()
    {
        return *vec;
    }
};

int main(int argc, char **argv)
{
    Data<int> data({1, 2, 3, 4, 5});
    vector<int> &vec = data.get();
    for (auto item : vec)
    {
        cout << item << endl; // 1 2 3 4 5
    }
    return 0;
}      

預設情況下,對于一個執行個體化了的類模闆,其成員隻有在使用時才被執行個體化

類模闆内使用自身

類模闆類在類外定義的函數成員中,使用自己時的類型時可以不提供模闆參數,但是如果作為方法參數或者反回值類型,則需要寫尖括号,在函數體内不用寫尖括号

在類内定義的成員中,則可以省略寫尖括号

最佳實踐:都寫上尖括号就好了,也會使得看代碼的人更容易了解

//example8.cpp
template <typename T>
class Data
{
public:
    T info;
    Data(const T &t) : info(t)
    {
    }
    Data print(const T &t) //類内定義成員方法
    {
        Data data(t);
        return data;
    }
    Data<T> sayHello(const T &t); //類内聲明
};

//類外定義
template <typename T>
Data<T> Data<T>::sayHello(const T &t)
{
    Data d(t); //與Data<T> t(t)等價
    info = t;
    return d;
}

int main(int argc, char **argv)
{
    Data<int> data(19);
    Data<int> data1 = data.print(20);
    Data<int> data2 = data.print(18);
    cout << data1.info << endl;              // 20
    cout << data2.sayHello(18).info << endl; // 18
    return 0;
}      

類模闆和友元

當類模闆有一個非模闆友元,則這個類模闆的所有執行個體類對此友元友好

//example9.cpp
template <typename T>
class Data
{
private:
    T t;

public:
    Data(const T &t) : t(t) {}
    friend void print();
};

void print()
{
    Data<int> data(19);
    Data<string> data1("oop");
    cout << data.t << " " << data1.t << endl; // 19 oop
}

int main(int argc, char **argv)
{
    print();
    return 0;
}      

一對一友好關系

類模闆與另一個(類或函數)模闆間友好關系的常見形式為建立對應執行個體及其友元間的友好關系

//example10.cpp
#include <iostream>
using namespace std;

//模闆類與函數模闆聲明
template <typename>
class A;
template <typename>
class B;
template <typename T>
void print(T t);

template <typename T>
class A
{
public:
    void test()
    {
        B<T> b;
        b.b = 888;
        cout << b.b << endl;
        // B<string> b1;//錯誤與B<string>不是友元關系
        // b1.b = "oop";
    }
};

template <typename T>
class B
{
public:
    T b;
    friend class A<T>; //将A<T>稱為B<T>的友元
    friend void print<T>(T t);
};

template <typename T>
void print(T t)
{
    B<T> b;
    cout << b.b << endl;
    B<string> b1; //為什麼是B<stirng>的友元
    //因為在此使用B<string>時,B内生成了friend void print(string t);
    b1.b = "oop";
    cout << b1.b << endl;
}

int main(int argc, char **argv)
{
    A<int> a;
    a.test();  // 888
    print(19); // 888 oop
    return 0;
}      

通過和特定的模闆友好關系

讓另一個類模闆的所有執行個體都都稱為友元

下面的代碼比較長,總之最重要的就是形如一下兩種友元聲明

friend class B<A>;
template<typename T> friend class B;      

的差別

//example11.cpp
template <typename T>
class B;
template <typename X>
class C;

class A
{
    friend class B<A>; //聲明 B<A>為A的友元
    template <typename T>
    friend class B; // B模闆的所有執行個體都是A的友元
    A(int a) : n(a) {}

private:
    int n;
};

template <typename T>
class B
{
    friend class A; //聲明A為B的友元
    template <typename X>
    friend class C;
    // 所有執行個體之間都是友元關系 B<int> 與 C<string>之間也是友元
    // friend class B<T>;
    //與上一句截然不同 此作用隻是如B<int>與C<int>之間為友元
private:
    T t;

public:
    B(T t) : t(t) {}
    void test()
    {
        A a(19);
        cout << a.n << endl; // B<T>為A的友元
    }
};

template <typename X>
class C
{
public:
    void test()
    {
        B<int> b1(19); //所有B<T>執行個體的友元都包括C<X>
        B<string> b2("oop");
        cout << b1.t << " " << b2.t << endl;
    }
};

int main(int argc, char **argv)
{
    C<int> c;
    c.test(); // 19 oop

    B<int> b1(0);
    B<string> b2("oop");
    b1.test(); // 19
    b2.test(); // 19
    return 0;
}      

令模闆自己的類型參數成為友元

在新标準中,可以将模闆類型參數聲明為友元,當然隻有其模闆實參為複合自定義類型時才顯得有意義

//example12.cpp
template <typename Type>
class A;
class Data;

template <typename Type>
class A
{
    friend Type; //重點
public:
    A(int n) : n(n) {}

private:
    int n;
};

//A必須放在Data前面否則會出現不玩增類型因為在遇見A<Data>時編譯器需要知道A<T>的定義
class Data
{
public:
    void test(A<Data> *p);
};

void Data::test(A<Data> *p)
{
    cout << p->n << endl;
}

int main(int argc, char **argv)
{
    Data data;
    A<Data> a(19);
    data.test(&a); // 19
    return 0;
}      

模闆類型别名

1、為類模闆執行個體起别名

typedef A<string> AString;
AString a;//等價于A<string> a;      

2、不能為類模闆本身起别名,因為模闆不是一個類型

3、為類模闆定義類型别名

template<typename T> using twin=pair<T,T>;
twin<string> data;//等價于 pair<string,string> data;

template<typename T> using m_pair=pair<T,usigned>;
m_pair<int> a;//等價于 pair<int,unsigned>a;      

類模闆的static成員

對于類模闆的static成員,每種模闆執行個體有自己的static執行個體

//example13.cpp
template <typename T>
class Data
{
public:
    T t;
    Data(T t) : t(t)
    {
        i++;
    }
    static size_t i;
    static std::size_t get_i()
    {
        return i;
    }
};

template <typename T>
size_t Data<T>::i = 0;

int main(int argc, char **argv)
{
    cout << Data<int>::i << endl; // 0
    Data<int> d1(10);
    cout << d1.i << endl;            // 1
    cout << Data<string>::i << endl; // 0
    Data<string> d2("ui");
    cout << Data<string>::i << endl; // 1

    cout << d1.get_i() << endl;         // 1
    cout << d2.get_i() << endl;         // 1
    cout << Data<int>::get_i() << endl; // 1
    // Data::get_i();//錯誤 不知道調用哪一個Data執行個體中的get_i
    return 0;
}      

模闆參數

一個模闆參數的名字沒有什麼内在含義,我們通常在一個模闆參數的情況下,将參數命名為T

template<typename T> void func(const T&t){

}      

模闆參數與作用域

模闆參數的作用域在其聲明之後,至模闆聲明或定義結束之前

模闆參數名不能重用,一個模闆參數名在特定模闆參數清單中隻能出現一次

//example14.cpp
double T;

template <typename T, typename F>
void func(const T &t, const F &f) // typename T覆寫double T
{
    cout << t << " " << f << endl;
    T t1;
}      

模闆聲明

模闆内容的聲明必須包括模闆參數

一個給定模闆的每個聲明和定義必須擁有相同的數量和種類的參數

//example15.cpp
//聲明函數模闆
template <typename T>
void func(const T &t);

//聲明類模闆
template <typename T>
class A;

//模闆定義
template <typename F>
void func(const F &f)
{
    cout << f << endl;
}

//類模闆定義
template <typename T>
class A
{
public:
    void func(const T &t);
};

template <typename T>
void A<T>::func(const T &t)
{
    cout << t << endl;
}

int main(int argc, char **argv)
{
    func(19);            // 19
    func("hello world"); // hello world
    A<int> a;
    a.func(19); // 19
    return 0;
}      

使用類的類型成員

再掉用類靜态成員時,因為類的類型為一個模闆類型參數時

編譯器不知道是調用函數名為​​

​T::mem​

​​的函數還是​

​T​

​​類的靜态成員​

​mem​

​,如果需要使用模闆參數類型的靜态成員,需要進行顯式的聲明,使用關鍵字typename

T::mem();//錯誤
typename T::mem();//正确      
//example16.cpp
template <typename T>
class A
{
public:
    typename T::size_type func(const T &t)
    {
        typename T::size_type size; //正确
        // T::size_type size;//錯誤
        size = t.size();

        return size;
    }
    static void hi()
    {
        cout << "hi" << endl;
    }
};

int main(int argc, char **argv)
{
    vector<int> vec{1, 2, 3, 4};
    A<vector<int>> a;
    cout << a.func(vec) << endl; // 4

    A<std::vector<int>>::hi(); // hi
    return 0;
}      

預設模闆實參

如同函數參數一樣,也可以像模闆參數提供預設實參,但實參不知值而是類型

如下面代碼樣例,首先在compare被調用時,編譯器通過實參類型與模闆函數形參類型比對,将能夠推算出的模闆參數推算出來,然後将模闆參數清單内的全部typename進行初始化,然後确定了所有模闆參數類型,然後進行實參的初始化,要知道這些操作都是在編譯階段完成的

//example17.cpp
template <typename T, typename F = less<T>>
int compare(const T &t1, const T &t2, F f = F())
{
    if (f(t1, t2)) // v1<v2
        return -1;
    if (f(t2, t1)) // v2<v1
        return 1;
    return 0;
}

int main(int argc, char **argv)
{
    cout << compare(1, 3) << endl; //-1
    cout << compare(3, 1) << endl; // 1
    cout << compare(1, 1) << endl; // 0
    return 0;
}      

模闆預設實參與類模闆

與函數模闆預設參數同理,在參數清單内進行類型指派

//example18.cpp
template <typename T = int>
class A
{
public:
    void func(const T &t) const
    {
        cout << t << endl;
    }
};

int main(int argc, char **argv)
{
    A<> a;
    a.func(19); // 19
    // a.func("dcs"); //錯誤
    A<string> a_s;
    a_s.func("hello world"); // hello world
    return 0;
}      

成員模闆

一個類(無論是普通類還是類模闆),本身可以含有模闆的成員函數,這總成員稱為成員模闆(member template),成員模闆不能是虛函數

普通類的成員模闆

将成員函數直接定義為函數模闆

//example19.cpp
class A
{
public:
    template <typename T>
    void func(const T &t) const
    {
        cout << t << endl;
    }
    template <typename T>
    void operator()(T *p) const
    {
        delete p;
    }
};

int main(int argc, char **argv)
{
    A a;
    a.func(12);    // 12
    a.func("oop"); // oop
    //類A本身擁有了 func(const int&t)與func(const string&)的兩個重載
    unique_ptr<int, A> num1(new int(19), a);
    unique_ptr<float, A> num2(new float(19.0), a);
    return 0;
}      

類模闆的成員模闆

類模闆與成員模闆二者擁有自己的模闆參數,當存在typename的名字相同時會産生沖突編譯不通過,因為在一個範圍内相同名字typename隻能用一次

如下樣例中,函數成員在類作用域下,類的模闆參數名不能與内部的沖突,但是hello與hi是兩個獨立的作用域,二者之間不會影響

//example20.cpp
template <typename T>
class A
{
public:
    static void func(const T &t)
    {
        cout << t << endl;
    }

    template <typename F>
    void hello(const F &f)
    {
        cout << f << endl;
    }

    template <typename F>
    void hi(const F &f)
    {
        cout << f << endl;
    }
};

int main(int argc, char **argv)
{
    A<int> a;
    a.func(19); // 19
    // a.func("oop");//錯誤

    a.hello("sds"); // sds
    a.hello(19);    // 19

    a.hi(19);    // 19
    a.hi("oop"); // oop
    return 0;
}      

執行個體化與成員模闆

成員模闆的具體應用,最熟悉的就是容器的清單初始化操作中,有時不能提前知道初始化清單中存儲的那種類型的資料,或者根據疊代器範圍進行初始化時,隻要它們内置的元素可以向目标容器的資料類型轉換就可以實作這種操作,在容器的初始化中有學習到

//example21.cpp
int main(int argc, char **argv)
{
    initializer_list<int> list = {1, 2, 3, 4, 5};
    int arr[] = {1, 1};
    //為什麼不能用{1.0,1.0}因為float到int需要進行強制轉換,不能自動轉換
    cout << arr[0] << " " << arr[1] << endl; // 1 1
    vector<float> vec = {1, 2, 3, 4};
    //背後的構造原理就是使用了initializer_list<T> 在未知具體類型下定義模闆成員
    //由編譯器自動生成
    for (const auto &item : vec) // 1 2 3 4
    {
        cout << item << endl;
    }

    vector<int> vec1{1, 2, 3};
    vector<float> vec2(vec1.begin(), vec1.end());
    //這種背後也是模闆成員的應用 接收vector疊代器 但用模闆參數解決vector中的資料類型
    for (const auto &item : vec2) // 1 2 3
    {
        cout << item << endl;
    }
    return 0;
}      

背後是怎樣的呢,大緻原理是什麼?

//example22.cpp
class A
{
public:
    vector<float> vec;
    template <typename T>
    A(const initializer_list<T> &t)
    {
        vec.assign(t.begin(), t.end());
    }
    void print()
    {
        for (const auto &item : vec)
        {
            cout << item << " ";
        }
        cout << endl;
    }
};

int main(int argc, char **argv)
{
    initializer_list<int> m_list = {1, 2, 3, 4};
    A a(m_list);
    A b({1.0, 2.0, 3.0, 4.0}); // A b(initializer_list<float>)
    a.print();                 // 1 2 3 4
    b.print();                 // 1 2 3 4
    return 0;
}      

控制執行個體化

當模闆被使用時才會被進行執行個體化,則相同的執行個體可能出現在多個對象檔案中,兩多個獨立編譯的源檔案中使用了相同的模闆,并提供相同的模闆參數時,每個檔案中都會有該模闆的一個執行個體,這樣的開銷可能非常嚴重,在C++11中可以通過顯式執行個體化(explicit instantiation)來避免這種開銷

extern template declaration;//執行個體化聲明
template declaration;       //執行個體化定義      
//example23/main.cpp
#include <iostream>
#include <string>
#include "main.h"
using namespace std;

template class A<string>;         //定義模闆執行個體
template void func(const int &t); //定義模闆執行個體

extern void m_func();

int main(int argc, char **argv)
{
    m_func();
    return 0;
}
// g++ -c main2.cpp
// g++ -c main.cpp
// g++ main.o main2.o -o main.exe
// ./main.exe      

編譯器遇見定義模闆執行個體時會生成代碼,是以A的func執行個體在main.o内

//example23/main.h
#ifndef main_h
#define main_h
#include <iostream>
void m_func();
//定義類模闆
template <typename T>
class A
{
public:
    void func(const T &t)
    {
        using namespace std;
        cout << t << endl;
    }
};

//定義函數模闆
template <typename T>
void func(const T &t)
{
    using namespace std;
    cout << t << endl;
}
#endif      

extern表示其定義在其他源檔案定義,想要程式完整必須進行連結

//example23/main2.cpp
#include "main.h"
#include <string>
#include <iostream>
using namespace std;
extern template class A<string>;
extern template void func(const int &t);

void m_func()
{
    A<string> a;
    a.func("hello world"); // hello world
    func(12);              // 12
}      

重點概念:與普通的模闆執行個體化不同,執行個體化定義會執行個體化所有成員,普通的使用執行個體化僅僅執行個體化我們有使用到的成員,而在顯式執行個體化中,編譯器不知道我們需要使用哪些成員,是以它直接會将所有成員進行執行個體化,包括内聯的成員 。

進而在一個類模闆的顯式執行個體化定義中,提供的模闆類型參數必須能用于模闆的所有成員函數

shared_ptr與unique_ptr中的模闆知識

已經學習過shared_ptr與unique_ptr,它們提供了自定義删除器的方法

1、shared_ptr可以在定義是提供删除器,例如下面格式

//example19.cpp
struct Person
{
    int *ptr;
    Person()
    {
        ptr = new int(888);
    }
};

void deletePerson(Person *ptr)
{
    if (ptr->ptr)
    {
        delete ptr->ptr;
        ptr->ptr = nullptr;
        cout << "delete ptr->ptr;" << endl;
    }
    delete ptr;
}

void func()
{
    shared_ptr<Person> ptr(new Person(), deletePerson); //釋放時使用deletePerson
    cout << ptr.unique() << endl;                       // 1
    Person *p = new Person;
    // delete ptr->ptr;
    ptr.reset(p, deletePerson); // 釋放p時使用deletePerson
    // delete ptr->ptr;
}      

shared_ptr也可以在reset時提供删除器,可見shared_ptr是在運作時綁定删除器的

del?del(p):delete p;      

2、unique_ptr隻能在定義時在見括号内提供自定義删除器

//example20.cpp
struct Person
{
    int *ptr;
    Person()
    {
        ptr = new int(888);
    }
};

void deletePerson(Person *ptr)
{
    if (ptr->ptr)
    {
        delete ptr->ptr;
        ptr->ptr = nullptr;
        cout << "delete ptr->ptr;" << endl;
    }
    delete ptr;
}

void func()
{
    unique_ptr<Person, decltype(deletePerson) *> u2(new Person(), deletePerson);
}      

shared_ptr是将删除器的指針或引用等存儲到了對象内部,當删除是需判斷,而unique則是使用了類模闆參數,并且為删除器提供了預設參數為delete,可見二者删除器的綁定原理是不一樣的,前者是運作時綁定,後者是使用模闆編譯器在編譯階段進行了代碼級别的綁定

模闆實參推斷

在函數模闆中,編譯器利用調用中地函數地實參類型來确定模闆參數,這一過程稱為​

​模闆實參推斷​

​。在類模闆中是通過尖括号進行初始化模闆參數清單

類型轉換與模闆類型參數

當使用模闆時提供地模闆實參之間可以進行類型轉換時,隻有有限地幾種類型會自動地應用于這些實參,編譯器通常不是對實參進行類型轉換、而是生成一個新的模闆執行個體

可以進行類型轉換的情況有兩種

1、const轉換:非const對象的引用或指針,傳遞給一個const的引用或指針形參

2、數組或函數指針轉換:如果函數形參不是引用類型、則可以對數組或函數類型的實參應用正常的指針轉換,一個數組實參可以轉換為一個指向其首元素的指針、一個函數實參可以轉換為一個該函數類型的指針、而不是不同長度的數組或者不同函數傳遞時都會産生新的模闆執行個體

//example24.cpp
//拷貝
template <typename T>
T f1(T t1, T t2)
{
    return t1;
}
//引用
template <typename T>
const T &f2(const T &t1, const T &t2)
{
    return t1;
}
//接收可調用對象
template <typename T>
void f3(const T &f)
{
    f();
}

void func()
{
    cout << "hello world" << endl;
}

int main(int argc, char **argv)
{
    string s1("oop");
    const string &s2 = f2(s1, s1);
    cout << s2 << endl; // oop
    s1 = "hello world";
    cout << s2 << endl; // hello world

    int a[10], b[20];
    int *arr_a = f1(a, b); //按照首位址指針處理
    arr_a[0] = 999;
    cout << a[0] << endl; // 999

    //錯誤 按照數組的引用處理錯誤 const T &t1, const T &t2
    //實參 t1 t2類型不同 因為a與b的大小不同
    // const int *arr_a_ptr = f2(a, b);
    // cout << arr_a_ptr[0] << endl; // 999

    //函數到函數指針的轉換
    f3(func); // hello world
    return 0;
}      
重點:将實參傳遞給帶模闆類型的函數形參時,能夠自動進行類型轉換隻有const轉換與(數組或函數)到指針的轉換

使用相同的模闆參數類型

當形參清單中多次使用了模闆參數類型時,在傳遞實參時這些位置的實參的類型在不進行類型轉換的情況下,應該相同

//example25.cpp
template <typename T>
void func(T t1, T t2)
{
    cout << t1 * t2 << endl;
}

int main(int argc, char **argv)
{
    // func(long(12), int(12));
    // no matching function for call to 'func(long int, int)'

    float num = 99.0;
    // func(num, 12);
    // no matching function for call to 'func(float&, int)'

    func(long(19), long(32)); // 608
    return 0;
}      

非模闆類型參數可正常類型轉換

在函數模闆形參中,如果有非模闆參數類型的形參,則其正常類型轉換不會受到影響

//example26.cpp
template <typename T>
void func(float num, ostream &os, const T &t)
{
    os << num << " " << t << endl;
}

int main(int argc, char **argv)
{
    func(int(19), cout, 12); // 19 12
    ofstream f("output.iofile");
    func(unsigned(12), f, 12); //在檔案output.iofile内 12 12
    f.close();
    return 0;
}      

可見func函數模闆的形參中 float num 與 ostream&os 都可以進行正常的類型轉換,追溯原理還要從模闆編譯說起,在編譯器檢測到模闆被調用時,先檢測實參清單是否比對,對于非模闆參數類型還要進行是否可以進行類型轉換,而不是簡單的類型比對

函數模闆顯式實參

有沒有想過當函數模闆參數類型中,有些沒有被使用到函數形參内,編譯器就不能自動推斷出類型,這樣的情況應該怎樣處理,是以允許使用者進行使用函數模闆顯式實參

//example27.cpp
template <typename T1, typename T2, typename T3>
T1 sum(T2 t1, T3 t2)
{
    return t1 + t2;
}

int main(int argc, char **argv)
{
    // sum(12, 32);// couldn't deduce template parameter 'T1'
    long long res = sum<long long>(12332, 23);
    cout << res << endl; // 12355
    return 0;
}      

那麼尖括号中提供的顯式實參與模闆參數類型的比對機制是怎樣的呢?

顯式模闆實參按左至右順序與對應模闆參數比對,第一個顯式實參與第一個參數比對、第二個與第二個,以此類推,隻有最右的顯式模闆實參才能忽略

//example28.cpp
//糟糕的用法
template <typename T1, typename T2, typename T3>
T3 func(T2 t2, T1 t1)
{
    return t1 * t2;
}
//需要使用者顯式為T3提供實參
//因為想要為T3提供實參就必須為其前面的模闆參數提供實參

int main(int argc, char **argv)
{
    auto res = func<int, int, int>(12, 21);
    cout << res << endl; // 252
    // func<int>(21, 32);//couldn't deduce template parameter 'T3'
    return 0;
}      

最佳實踐就是,将模闆參數清單中需要顯式提供實參的參數放到清單前面去

類型轉換應用于顯式指定的實參

與非模闆參數類型一樣,提供顯式類型實參的參數也支援正常的類型轉換

//example29.cpp
template <typename T1>
T1 mul(T1 t1, T1 t2)
{
    return t2 * t1;
}

int main(int argc, char **argv)
{
    // mul(long(122), 12);
    // error:deduced conflicting types for parameter 'T1' ('long int' and 'int')

    auto res = mul<int>(long(122), 12);
    cout << res << endl; // 1464

    auto res1 = mul<double>(23, 32);
    cout << res1 << endl; // 736
    return 0;
}      

尾置傳回類型與類型轉換

有時需要傳回未知的資料類型,但是使用參數類型推斷并不能很好解決問題,使用顯式模闆實參又顯得負擔很重,那麼尾置傳回類型就要顯現出其作用了

//example30.cpp
template <typename Res, typename T>
Res &func(T beg, T end)
{
    return *beg;
}

int main(int argc, char **argv)
{
    vector<int> vec = {1, 2, 3};
    auto res = func<int>(vec.begin(), vec.end());
    cout << res << endl; // 1
    return 0;
}      

有沒有更好的辦法解決問題呢,yes!使用尾置傳回(在第6章 函數時就有接觸到)

//example31.cpp
template <typename T>
auto func(T beg, T end) -> decltype(*beg)
{
    return *beg;
}

int main(int argc, char **argv)
{
    vector<int> vec = {1, 2, 3};
    auto res = func(vec.begin(), vec.end());
    // auto func<std::vector<int>::iterator>(std::vector<int>::iterator beg, std::vector<int>::iterator end)->int &
    cout << res << flush; // 1

    decltype(vec) r;              // std::vector<int> vec
    decltype(vec.begin()) t;      // std::vector<int>::iterator t
    decltype(0 + 1) y;            // int y
    decltype(*vec.begin() + 1) u; // int u

    return 0;
}      

類型轉換模闆

在上面我們發現了,還是我有解決問題,隻能獲得再怎麼操作都也隻能使用引用類型,怎樣獲得元素類型呢,這就要使用标準庫的​

​類型轉換模闆​

//example32.cpp
template <typename T>
auto func(T beg, T end) -> typename remove_reference<decltype(*beg)>::type
{
    return *beg;
}

int main(int argc, char **argv)
{
    vector<int> vec = {1, 2, 3};
    vector<int>::value_type res = func(vec.begin(), vec.end());
    cout << res << endl; // 1

    int num = 999;
    int &num_ref = num;
    //脫去引用
    remove_reference<decltype(num_ref)>::type num_copy = num_ref;
    // int num_copy=num_ref;
    return 0;
}      

類似的模闆類有很多,其都在頭檔案​

​type_traits​

​内

第16章 模闆和泛型程式設計【C++】
//example33.cpp
int main(int argc, char **argv)
{
    //脫引用
    remove_reference<int &>::type t1;  // int t1
    remove_reference<int &&>::type t2; // int t2
    remove_reference<int>::type t3;    // int t3

    //加const
    int num = 1;
    add_const<int &>::type t4 = num;     // int &t4
    add_const<const int>::type t5 = num; // const int t5
    add_const<int>::type t6 = num;       // const int t5

    //加左值引用
    add_lvalue_reference<int &>::type t7 = num;  // int &t7
    add_lvalue_reference<int &&>::type t8 = num; // int &t8
    add_lvalue_reference<int>::type t9 = num;    // int &t8

    //還有如
    // add_rvalue_reference加右值引用
    // remove_pointer 移除指針(從指針類型退出值類型)
    // make_signed 去unsigned
    // make_unsigned 從帶符号類型退出相應的unsgined
    // remove_extent 根據數組類型得到元素類型
    // remove_all_extents 根據多元數組推斷

    remove_extent<int[10]>::type item1;          // int item1
    remove_all_extents<int[10][10]>::type item2; // int item2
    return 0;
}      

先知道有這麼個東西吧,其實很少用到的,除非想要開發一個高複用的庫可能會用到

函數模闆與函數指針

函數模闆可以與函數指針進行操作時,也會涉及模闆參數類型的推斷問題

//example34.cpp
template <class T>
T big(const T &t1, const T &t2)
{
    return t1 > t2 ? t1 : t2;
}

int main(int argc, char **argv)
{
    int (*pf1)(const int &t1, const int &t2) = big;
    auto res = (*pf1)(12, 32);
    cout << res << endl; // 32
    return 0;
}      

函數模闆在賦給函數指針時,相關的模闆參數推斷是根據左邊的函數指針類型進行推斷的

當作為函數模闆作為函數參數傳遞時可能會遇見的問題

//example35.cpp
template <typename T>
T big(const T &t1, const T &t2)
{
    return t1 > t2 ? t1 : t2;
}

void func(int (*p)(const int &t1, const int &t2))
{
    cout << (*p)(12, 32) << endl;
}

void func(string (*p)(const string &s1, const string &s2))
{
    cout << (*p)("23", "dsc") << endl;
}

int main(int argc, char **argv)
{
    // func(big); // error: call of overloaded 'func(<unresolved overloaded function type>)' is ambiguous
    //可見func傳遞big在确定重載時是模棱兩可的

    //如何解決,使用顯式模闆參數
    func(big<int>);    // 32
    func(big<string>); // dsc
    return 0;
}      

從左值引用函數參數推斷類型

主要讨論的就是,T&與const T&在使用中的類型推斷

//example36.cpp
// T&
template <typename T>
void func1(T &t)
{
    cout << t << endl;
}

// const T&
template <typename T>
void func2(const T &t) // t具有底層const
{
    cout << t << endl;
}

int main(int argc, char **argv)
{
    // func1(12);//錯誤12不是左值引用
    int num = 19;
    func1(num); // 19
    const int num1 = 999;
    // T 按const int處理
    func1(num1); // void func1<const int>(const int &t)

    func2(18);   // 18
    func2(num);  // void func2<int>(const int &t)
    func2(num1); // 999

    return 0;
}      

從右值引用函數參數推斷類型

讨論T&&用作右值引用時的情況

//example37.cpp
template <typename T>
void func(T &&t)
{
    t = 999;
    cout << t << endl;
}

int main(int argc, char **argv)
{
    // void func<int>(int &&t)
    func(11); // 999

    int num = 888;
    func(num); // 999
    // void func<int &>(int &t)
    cout << num << endl;  // 999
    func(std::move(num)); // void func<int>(int &&t)

    func(12.0f); // void func<float>(float &&t)
    func(23.32); // void func<double>(double &&t)

    return 0;
}      

右值引用折疊與模闆參數

在上面代碼example37.cpp中可以發現,為什麼普通的int num可以傳遞給func,而且推斷出的T&&實際為int&,這是怎麼回事呢?

1、将一個左值傳遞給函數的右值引用參數,且右值引用指向模闆類型參數時,編譯器推斷類型參數實參為左值引用類型,如傳遞int類型的num,則T為int&

2、不能直接定義引用的引用,但是如果間接建立了引用的引用,則會折疊,如int& &&則實際為int&,int&& &,int& &都會折疊為int &, 類型int&& &&折疊為int &&

//example38.cpp
template <typename T>
void func(T &&t)
{
    cout << t << endl;
}

/*void f(int &&&num){}不允許直接使用引用的引用*/

int main(int argc, char **argv)
{
    int num = 999;
    func(num); // T被推斷為int& ,int& &&折疊為int&
    const int n1 = 888;
    func(n1); // T被推斷為 const int& ,const int& &&折疊為 cosnt int&
    func(88); //正常右值引用 int&&

    func<int>(12);    // void func<int>(int &&t) T推斷為int
    func<int &>(num); // void func<int &>(int &t) 折疊 int& &&
    func<int &&>(12); // void func<int &&>(int &&t) 折疊 int&& &&
    return 0;
}      

因為有這個特性,當我們在func中使用T關鍵詞時,它又會代表着怎樣的特性呢?

//example39.cpp
template <typename T>
void func(T &&t)
{
    T t1 = t;
    t1 = 888;
    cout << (t1 == t ? "true" : "false") << endl;
}

void f(const int &t)
{
    cout << "const int & " << t << endl;
}

void f(int &&t)
{
    cout << "&& " << t << endl;
}

int main(int argc, char **argv)
{
    int num = 999;
    func<int>(12);    // false T被推斷為 int
    func<int &>(num); // true T被推斷為int& ,t實際類型為int&
    // func<int &&>(12); //錯誤 T被推斷為 int&& 不能将左值t賦給int&&t1

    const int i = 888;
    // func(i);//錯誤 T被推斷為 const int,t1=888發生錯誤

    int &&j = 999;
    f(j); // const int& 999
    j = 888;
    f(j); // const int& 888

    f(99); //&& 99
    return 0;
}      

了解std::move

回顧一下std::move

//example40.cpp
int main(int argc, char **argv)
{
    int &&i = 999;
    int num = 999;
    // int &&j = num;//錯誤 不能将左值綁到右值引用上

    int &&j = std::move(num); //使用move
    cout << j << endl;        // 999
    num = 888;
    cout << j << endl; // 888

    return 0;
}      

std::move可以使得傳入的實參作為右值,綁定到右值引用,背後的原理是怎樣的呢?下面将進行學習相關知識

std::move是如何定義的

//example41.cpp
template <typename T>
typename remove_reference<T>::type &&func(T &&t)
{
    return static_cast<typename remove_reference<T>::type &&>(t);
}

int main(int argc, char **argv)
{
    int num = 999;
    int &&i = func(num);
    num = 888;
    cout << i << endl; // 888
    return 0;
}      

其中使用了remove_reference移除引用擷取資料類型,傳回其相應資料類型的右值引用,傳回值使用static_cast進行強制轉換得到

std::move是如何工作的

總之就是背後remove_reference與static_cast的功勞

//example42.cpp
int main(int argc, char **argv)
{
    int num = 999;
    int &&i = move(num);
    // T推斷為int&
    // remove_reference傳回int 确定函數傳回類型為 int&&

    int &&j = move(88);
    // T推斷為int
    // remove_reference傳回int 确定函數傳回類型為 int&&
    j = 666.;
    cout << j << endl; // 666
    return 0;
}      

static_cast左值轉右值引用

通常static_cast隻能用在如float->int等其他合法的類型轉換,但是有一個特殊的規則

static_cast可以将一個左值轉換為一個右值引用

//example43.cpp
int main(int argc, char **argv)
{
    int num = 999;
    // int &&i = num; // error
    int &&i = static_cast<int &&>(num);
    i = 888;
    cout << num << endl; // 888
    return 0;
}      

以上的内容确實是相當無趣的,可能過幾天就會忘記,在開發中也會用得很少,但是别忘記有這樣得操作,時不時得回來看一看

轉發

首先我們先了解一下在此得“轉發”是什麼意思呢?

當在函數模闆内使用形參作為調用函數時的實參,即需要将其中一個或多個實參連同類型不變地轉發給其他函數

//example44.cpp
void fi(int v1, int &v2)
{
    cout << v1 << " " << ++v2 << endl;
}

//接收可調用對象f和其他兩個參數
//翻轉參數調用給定的調用對象
template <typename F, typename T, typename N>
void func(F f, T t, N n)
{
    f(n, t);
}

int main(int argc, char **argv)
{
    func(fi, 12, 21); // 21 13
    int num1 = 99, num2 = 88;
    func(fi, num1, num2);                // 88 100
    cout << num1 << " " << num2 << endl; // 99 88

    const int n1 = 999;
    func(fi, n1, n1); // 999 1000
    // void func<void (*)(int v1, int &v2), int, int>(void (*f)(int v1, int &v2), int t, int n)
    // 在此出現了頂層const被忽略的情況

    return 0;
}      

如何盡可能保持參數的類型呢

保持類型資訊的函數參數

使用右值引用做參數即可實作

//example45.cpp
void fi(int v1, int &v2)
{
    cout << v1 << " " << ++v2 << endl;
}

void fir(int v1, const int &v2)
{
    cout << v1 << " " << v2 << endl;
}

//接收可調用對象f和其他兩個參數
//翻轉參數調用給定的調用對象
template <typename F, typename T, typename N>
void func(F f, T &&t, N &&n)
{
    f(n, t);
}

int main(int argc, char **argv)
{
    const int num1 = 1, num2 = 2;

    // func(fi, num1, num2);//錯誤 fi(int v1,int&v2); 不能用num1對v2初始化
    //  void func<void (*)(int v1, int &v2), const int &, const int &>
    //         (void (*f)(int v1, int &v2), const int &t, const int &n)
    // T與N被推斷為 const int&類型 然後進行了引用折疊 為 const int&
    // const int&不能初始化int&

    func(fir, num1, num2); // 2 1
    //使用右值引用可以保證const得以保留
    //在傳遞 常量表達式如123 時為 int&&
    // const int時 為 const int&
    // int 時 為 int&
    // int& 時折疊為 int&
    // const int& 時折疊為const int&

    return 0;
}      

在調用中使用std::forward保持類型資訊

至此還是沒有解決問題,在傳遞右值時會出錯,為了解決問題,在func中傳遞參數時使用forard或者move獲得臨時右值對目标函數形參初始化

//example46.cpp
template <typename T, typename F>
void fi(T &&v1, F &&v2)
{
    cout << v1 << " " << v2 << endl;
}

template <typename F, typename T, typename N>
void func(F f, T &&t, N &&n)
{
    f(std::forward<N>(n), std::forward<T>(t));
    // f(t, n);
    // 當 t n為右值引用時 fi的形參也被推斷為右值引用類型
    // 可見右值引用是不能初始化右值引用的
}

int main(int argc, char **argv)
{
    func(fi<int &&, int &&>, 12, 32); // 32 12
    //  func=>(void (*f)(int &&, int &&), int &&t, int &&n)
    //  void fi<int &&, int &&>(int &&v1, int &&v2)
    //  func使用forward得以轉發右值引用

    const int &&num1 = 888;
    const int &&num2 = 999;
    func(fi<const int, const int>, std::forward<const int>(num1), std::forward<const int>(num2)); // 999 888
    // void fi<const int, const int>(const int &&v1, const int &&v2)
    // func=>(void (*f)(const int &&, const int &&), const int &&t, const int &&n)

    // std::move與std::forward最主要的差別 forward為顯式指定類型
    std::move(12);
    int &&i = std::forward<int>(12);
    const int &&j = std::forward<const int &&>(12);
    cout << j << endl; // 12
    // j = 888;//錯誤
    i = 888;
    cout << i << endl; // 888

    return 0;
}      

到此,可能腦袋要爆了!不知道你怎麼樣,反正我快崩潰了,在中文翻譯版的書籍,我認為描述的是非常模糊的。甚至我認為翻譯得不流暢,沒有生動得描述出知識。是在太難了,先堅持吧!後面再進行回顧與複習,與閱讀其他書籍或資料進行深入學習

重載與模闆

函數模闆可以被另一個普通模闆或普通函數重載,名字相同的函數必須具有不同數量或類型的參數

//example47.cpp
template <typename T>
string debug_rep(const T &t)
{
    ostringstream ret;
    ret << t;
    return ret.str();
}

template <typename T>
string debug_rep(T *p)
{
    ostringstream ret;
    if (p)
    {
        ret << debug_rep(*p); //調用string debug_rep(const T &t)
    }
    else
    {
        ret << " null pointer";
    }
    return ret.str();
}

int main(int argc, char **argv)
{
    cout << debug_rep("hello world") << endl;         // h std::string debug_rep<const char>(const char *p)
    cout << debug_rep(string("hello world")) << endl; // hello world std::string debug_rep<std::string>(const std::string &t)
    cout << debug_rep(1) << endl;                     // 1 std::string debug_rep<int>(const int &t)
    int num = 999;
    cout << debug_rep(num) << endl;  // 999 std::string debug_rep<int>(const int &t)
    cout << debug_rep(&num) << endl; // 999 std::string debug_rep<int>(int *p)
    return 0;
}      

多個可行模闆

再對模闆重載比對時可能存在多個比對都是符合要求的

const int *ptr = &num;
debug_rep(ptr);       // std::string debug_rep<const int>(const int *p)      

理論上可以比對為debug_rep(const string*&)或debug_rep(const string*),但根據重載函數模闆的特殊規則,此調用被解析為後者,因為後者更特例化

//example48.cpp
template <typename T>
string debug_rep(const T &t)
{
    ostringstream ret;
    ret << t;
    return ret.str();
}

template <typename T>
string debug_rep(T *p)
{
    ostringstream ret;
    if (p)
    {
        ret << debug_rep(*p); //調用string debug_rep(const T &t)
    }
    else
    {
        ret << " null pointer";
    }
    return ret.str();
}

int main(int argc, char **argv)
{
    int num = 999;
    const int *ptr = &num;
    int *const ptr1 = &num;
    *ptr1 = 888;
    cout << *ptr << endl; // 888
    debug_rep(ptr);       // std::string debug_rep<const int>(const int *p)
    return 0;
}      
Note: 當有多個重載模闆對一個調用提供同樣好的比對時,應選擇最特例化的版本

模闆與非模闆重載

完全可以存在與函數模闆相同名稱的普通函數

//example49.cpp
template <typename T>
string debug_rep(const T &t)
{
    ostringstream ret;
    ret << t;
    return ret.str();
}

string debug_rep(const string &t)
{
    cout << "debug_rep(const string &t)\n";
    return t;
}

int main(int argc, char **argv)
{
    cout << debug_rep(string("cd")) << endl; // debug_rep(const string &t) cd
    cout << debug_rep("ds") << endl;         // ds
    //能夠比對到普通函數就不會使用模闆
    return 0;
}      
Note: 對于一個調用,如果一個非函數模闆與一個函數模闆提供同樣好的比對,則選擇非模闆版本

重載模闆和類型轉換

對于debug_rep(“hello world”),存在多個比對都是可行的

debug_rep(const T&);
debug_rep(T*);
debug_rep(const string&);      
//example50.cpp
// 1
template <typename T>
void debug_rep(const T &t)
{
    cout << t << endl;
}

// 2
template <typename T>
void debug_rep(T *t)
{
    cout << t << endl;
}

// 3
void debug_rep(const string &t)
{
    cout << t << endl;
}

int main(int argc, char **argv)
{
    // 1 2 3 存在
    debug_rep("oop"); // oop void debug_rep<const char>(const char *t)
    // 1 3存在
    debug_rep("oop"); // oop void debug_rep<char [4]>(const char (&t)[4])
    // 3存在
    debug_rep("oop"); // oop void debug_rep(const std::string &t)
    //發生 const char* 到 const char&的轉換

    return 0;
}      

缺少聲明可能導緻程式行為異常

現在已經學習,再對重載進行比對時,如果非模闆比對成功則會調用非模闆,但是,如果調用函數前并沒有非模闆的聲明,則會使用模闆進行生成執行個體,有時可能會出現預料之外的結果

//example51.cpp
template <typename T>
void func(const T &t)
{
    cout << "1 " << t << endl;
}

// TAG::聲明
//  void func(const string &t);

int main(int argc, char **argv)
{
    func("hello world");
    // 1 hello world
    // void func<char[12]>(const char(&t)[12])

    func(string("hello world")); // 2 hello world
    //如果 TAG::聲明被注釋掉将會輸出1 hello world
    //采用模闆執行個體而不是非模闆

    return 0;
}

void func(const string &t)
{
    cout << "2 " << t << endl;
}      
Note: 在定義任何函數前,記得聲明所有重載的函數版本,這樣就不用擔心編譯器由于未遇到你希望調用的函數而用模闆執行個體化一個并非你所需的版本。

可變參數模闆

可變參數模闆為解決接收未知的參數類型未知的參數數量問題而生,進一步可以提高程式的複用性

​​

​可變參數模闆(variadic template)​

​​就是一個接收可變數目參數的模闆函數或模闆類。

可變數目的參數被稱為​​

​參數包(parameter packet)​

​​,​

​模闆參數包(template parameter packet)​

​​表示零個或多個模闆參數。​

​函數參數包(function parameter packet)​

​,表示零個或多個函數參數

//example52.cpp
//  foo為可變參數模闆
//  Args為模闆參數包
//  rest為函數參數包
template <typename T, typename... Args>
void foo(const T &t, const Args &...rest)
{
}

int main(int argc, char **argv)
{
    // void foo<int, double, std::string>
    foo(int(12), double(23), string("wew")); //模闆參數包中有兩個參數 double string

    // void foo<double, int>
    foo(double(23), int(3232)); //模闆參數包中有一個參數int

    // void foo<double, int, int, int>
    foo(double(232), int(323), int(343), int(4334)); //模闆參數包中有三個參數int

    // void foo<std::string>
    foo(string("dscs")); //模闆參數包為空

    // void foo<char [4]>
    foo("oop"); //模闆參數包為空

    return 0;
}      

擁有這樣的特性,存在着巨大的潛在能力

sizeof…運算符

使用sizeof…運算符可以知道參數包内有多少個參數

//example53.cpp
template <typename T, typename... Args>
void func(const T &t, Args... args)
{
    cout << sizeof...(Args) << " " << sizeof...(args) << endl;
}

int main(int argc, char **argv)
{
    func(12, 32, 43);         // 2 2
    func(12);                 // 0 0
    func(23, 43, 43.f, 78.f); // 3 3
    return 0;
}      

包擴充

之前有接觸過initializer_list用于接收未知數量但類型相同的參數

//example54.cpp
void func(initializer_list<int> m_list)
{
    for (auto &item : m_list)
    {
        cout << item << " ";
    }
    cout << endl;
}

int main(int argc, char **argv)
{
    func({12, 32, 43}); // 12 32 43
    return 0;
}      

已經學習了怎麼接收參數包,但是怎樣利用參數包内的内容呢

擴充一個包就是将其分解為構成的元素,對每個元素應用模式,獲得擴充後的清單

//example55.cpp
// 1
template <typename T>
void print(const T &t)
{
    cout << t << " ";
}

// 2
template <typename T, typename... Args>
void print(const T &t, const Args&... args)
{
    cout << t << " ";
    print(args...); //解構參數包
}

int main(int argc, char **argv)
{
    print(12, 32, 43, 23.f, 43); // 12 32 43 23 43
    //調用過程
    /*
    print(12,32,43,23.f,43) 使用2
    print(32,43,23.f,43) 使用2
    print(43,23.f,43) 使用2
    print(23.f,43) 使用2
    print(43) 使用1
    */
    //如果沒有定義1會怎樣呢
    /*
    在print(43)時隻能調用2,此時Args與args為空包,然後函數内部再次調用了print(args...)
    造成錯誤 print() 即沒有相比對的函數
    */
    return 0;
}      

再來看個簡單的例子吧

//example56.cpp
void print(int n, int i, float j, double k)
{
    cout << n << " " << i << " " << j << " " << k << endl;
}

template <typename T, typename... Args>
void func(const T &t, const Args &...args)
{
    print(t, args...);
}

int main(int argc, char **argv)
{
    func(12, 23, 23.f, 23.43); // 12 23 23 23.43
    return 0;
}      

進階包擴充

認識​

​func(args...)​

​​與​

​func(args)...​

​的差別

//example57.cpp
template <typename T>
T addOne(const T &t)
{
    return t + 1;
}

template <typename T, typename Y, typename U, typename I>
void print(const T &t, const Y &y, const U &u, const I &i)
{
    cout << t << " " << y << " " << u << " " << i << endl;
}

template <typename... Args>
void func(const Args &...args)
{
    print(addOne(args)...);
    //等價于 print(addOne(arg1),addOne(arg2),addOne(arg3),addOne(arg4))
}

int main(int argc, char **argv)
{
    func(1, 2, 3, 4); // 2 3 4 5
    return 0;
}      

轉發參數包

轉發參數包就是将接收到的參數包,調用另一個函數時将包傳遞出去

在标準容器中emplace_back方法就利用了轉發參數包的特性

//example58.cpp
class A
{
public:
    int a;
    string b;
    A(int a, string b) : a(a), b(b)
    {
    }
};

int main(int argc, char **argv)
{
    list<A> m_list;
    m_list.emplace_back(19, "hi");
    cout << m_list.size() << endl; // 1
    return 0;
}      

可見emplace_back接收參數包,然後将内容轉發到了調用A的構造函數

轉發就要保證明參中的類型資訊,是以其模闆類型參數應該為右值引用

而且使用std::forward對内容進行轉發

//example59.cpp
void func(int i, int j, float k)
{
    cout << i << " " << j << " " << k << endl;
}

void func(int &i, int j)
{
    cout << i << " " << j << " " << endl;
    i++;
}

template <typename... Args>
void emplace_back(Args &&...args) //相當于 T1&&arg1,T2&&arg2...
{
    func(std::forward<Args>(args)...);
    //相當于std::forward<T1>(arg1),std::forward<T2>(arg2)...
}

int main(int argc, char **argv)
{
    emplace_back(12, 32, 34.f); // 12 32 34
    int n = 999;
    emplace_back(n, 12);
    cout << n << endl; // 1000
    return 0;
}      

到此是不是更懵逼了,不要慌慢慢學,在實際項目中嘗試使用就好了,要記得多回來翻一翻,多複習。

模闆特例化

一個模闆使其對所有模闆實參都最合适,這部總是能辦到,當不是(不希望)使用模闆時,可以定義類或函數模闆地一個特例化版本

//example60.cpp
template <typename T>
int m_compare(const T &t1, const T &t2)
{
    if (t1 < t2)
    {
        return -1;
    }
    else if (t1 > t2)
    {
        return 1;
    }
    return 0;
}

int main(int argc, char **argv)
{
    cout << m_compare(1, 2) << endl;                         //-1
    cout << m_compare(string("abc"), string("abc")) << endl; // 0
    cout << m_compare("oop", "fop") << endl;                 //錯誤 字元數組不能用< > ==直接比較
    // int m_compare<char [4]>(const char (&t1)[4], const char (&t2)[4])
    return 0;
}      

怎樣可以解決這樣地問題呢,有多種辦法可以解決

//example61.cpp

template <size_t N, size_t M>
int m_compare(const char (&arr1)[N], const char (&arr2)[M])
{
    return strcmp(arr1, arr2);
}

int main(int argc, char **argv)
{
    cout << m_compare("oop", "fop") << endl; // 1
    return 0;
}      

還可以進行定義函數模闆特例化,如下

定義函數模闆特例化

​template<>​

​為原模闆的所有模闆參數提供實參,進行定義函數模闆特例化

//example62.cpp
template <typename T>
int m_compare(const T &t1, const T &t2)
{
    if (t1 < t2)
    {
        return -1;
    }
    else if (t1 > t2)
    {
        return 1;
    }
    return 0;
}

template <> //<>表示我們将為原模闆的所有模闆參數提供實參
int m_compare(const char *const &p1, const char *const &p2)
{
    return strcmp(p1, p2);
}

int main(int argc, char **argv)
{
    cout << m_compare(1, 2) << endl;                         //-1
    cout << m_compare(string("abc"), string("abc")) << endl; // 0

    //  cout << m_compare("oop", "fop") << endl;//當沒有特例化模闆時使用模闆執行個體化
    //  int m_compare<char [4]>(const char (&t1)[4], const char (&t2)[4])

    const char *str1 = "oop", *str2 = "oop";
    cout << m_compare(str1, str2) << endl; // 0 使用模闆特例化
    // template<> int m_compare<const char *>(const char *const &p1, const char *const &p2)
    return 0;
}      

函數重載與模闆特例化

本質:特例化的本質是執行個體化一個模闆,而非重載它。是以,特例化不影響函數比對

//example63.cpp
template <typename T>
int m_compare(const T &t1, const T &t2)
{
    if (t1 < t2)
    {
        return -1;
    }
    else if (t1 > t2)
    {
        return 1;
    }
    return 0;
}

template <> //<>表示我們将為原模闆的所有模闆參數提供實參
int m_compare(const char *const &p1, const char *const &p2)
{
    return strcmp(p1, p2);
}

int main(int argc, char **argv)
{
    m_compare("wdw", "cds");
    // 此時模闆與其特例化二者都是可行的,提供同樣好的比對
    // 但接收數組參數的版本更特例化,編譯器會選擇
    // int m_compare<char [4]>(const char (&t1)[4], const char (&t2)[4])
    return 0;
}      

如果還存在非模闆函數,調用情況又會不同

//example64.cpp
template <typename T>
int m_compare(const T &t1, const T &t2)
{
    if (t1 < t2)
    {
        return -1;
    }
    else if (t1 > t2)
    {
        return 1;
    }
    return 0;
}

template <> //<>表示我們将為原模闆的所有模闆參數提供實參
int m_compare(const char *const &p1, const char *const &p2)
{
    return strcmp(p1, p2);
}

int m_compare(const char *const &p1, const char *const &p2)
{
    cout << "it's not template" << endl;
    return strcmp(p1, p2);
}

int main(int argc, char **argv)
{
    m_compare("wdw", "cds"); // it's not template
    return 0;
}      

當模闆、模闆特例化、非模闆交雜在一起程式變得複雜起來,可閱讀性也會大大下降

還有關于模闆特例的作用域問題,想要使用模闆特例就要在調用模闆函數前,存在模闆特例的聲明,否則編譯器會使用模闆進行執行個體的生成

最佳實踐:模闆及其特例化版本應該聲明在同一個頭檔案中,所有同名模闆的聲明應該放在前面,然後是模闆的特例化版本。

類模闆特例化

類模闆特例化與函數模闆特例化類似

//example65.cpp
//聲明
template <typename T>
class A;
template <>
class A<int>;

template <typename T>
class A
{
public:
    T t;
    A(const T &t) : t(t)
    {
        cout << "template<typename T>" << endl;
    }
};

//類模闆特例化定義
template <>
class A<int>
{
public:
    int t;
    A(const int &t) : t(t)
    {
        cout << "template <>" << endl;
    }
};

int main(int argc, char **argv)
{
    A<int> a(12); // template <>
    return 0;
}      

實戰類模闆特例化

下面來做些有趣的事情,我們對标準庫内的模闆進行特例化

//example66.cpp
class A
{
public:
    int a;
    float b;
    unsigned int c;
    A(int a, float b, unsigned int c, int d) : a(a), b(b), c(c), d(d)
    {
    }
    friend class std::hash<A>;

private:
    int d;
};

//打開std命名空間 以便特例化std::hash
namespace std
{
    template <>
    class hash<A>
    {
    public:
        typedef size_t result_type;
        typedef A argument_type;
        size_t operator()(const A &a) const;
    };
    size_t hash<A>::operator()(const A &a) const
    {
        return hash<int>()(a.a) ^ hash<float>()(a.b) ^ hash<double>()(a.c) ^ hash<int>()(a.d);
    }
}

int main(int argc, char **argv)
{
    A a(1, 2.f, 4, 6);
    std::hash<A>::result_type res = std::hash<A>()(a);
    cout << res << endl; // 1000015520
    A b(2, 4.f, 5, 7);
    cout << std::hash<A>()(b) << endl; // 672367880
    return 0;
}      

定義hash有什麼用呢,當使用A作為容器的關鍵字類型時,編譯器就會自動使用此特例化版本,而不是編譯器自動生成的

//example67.cpp
class A
{
public:
    int a;
    float b;
    unsigned int c;
    A(int a, float b, unsigned int c, int d) : a(a), b(b), c(c), d(d)
    {
    }
    friend class std::hash<A>;
    bool operator==(const A &other) const
    {
        return other.a == a && other.b == b && other.c == c && other.d == d;
    }

private:
    int d;
};

//打開std命名空間 以便特例化std::hash
namespace std
{
    template <>
    class hash<A>
    {
    public:
        typedef size_t result_type;
        typedef A argument_type;
        size_t operator()(const A &a) const;
    };
    size_t hash<A>::operator()(const A &a) const
    {
        cout << "m_hash" << endl;
        return std::hash<int>()(a.a) ^ std::hash<float>()(a.b) ^ std::hash<double>()(a.c) ^ std::hash<int>()(a.d);
    }
}

int main(int argc, char **argv)
{
    A a(1, 2.f, 4, 6);
    unordered_multiset<A> m_set; // m_hash 可見使用了特例化的std::hash
    m_set.insert(a);
    return 0;
}      

類模闆部分特例化

與函數模闆不同的是,類模闆的特例化不必為所有模闆參數提供實參,可以隻提供一部分而非所有模闆參數,被稱為部分特例化

//example68.cpp
template <typename T>
struct A
{
    A(T t)
    {
        cout << "T" << endl;
    }
};

template <typename T>
struct A<T &>
{
    A(T &t)
    {
        cout << "T&" << endl;
    }
};

template <typename T>
struct A<T &&>
{
    A(T &&t)
    {
        cout << "T&&" << endl;
    }
};

int main(int argc, char **argv)
{
    A<decltype(42)> a1(12); // T
    int i = 999;
    int &n = i;
    A<decltype(n)> a2(n);                       // T&
    A<decltype(std::move(i))> a3(std::move(i)); // T&&
    return 0;
}      

特例化類成員

//example69.cpp
template <typename T>
struct A
{
    A(const T &t = T()) : mem(t)
    {
    }
    T mem;
    void func();
};

//通用型定義
template <typename T>
void A<T>::func()
{
    cout << "A<T>" << endl;
}

//成員特例化
template <>
void A<int>::func()
{
    cout << "A<int>" << endl;
}

int main(int argc, char **argv)
{
    A<float> a1(float(234));
    a1.func(); // A<T>

    A<int> a2(23);
    a2.func(); // A<int>

    A<string> a3(string("scsd"));
    a3.func(); // A<T>
    return 0;
}      

小結