c++ 模板詳解

2023-06-09 12:00:22
  • 模板就是將型別進行引數化

函數模板

//函數模板的定義格式
template<class 形參名,class 形參名...>
返回值型別 函數名(參數列){
    函數體;
}
  • 模板形參不能為空,並且函數模板中每一個型別引數在函數參數列中至少使用一次,只有這樣才能推斷出具體的型別
template <class T>
T Add(T x1, T x2){
    return x1+x2;
}

int main(){
    cout<<Add(2,3)<<endl;//5
    cout<<Add(3.2,1.4)<<endl;//4.6
    return 0;
}

函數模板範例化

  • 不能直接使用函數模板實現具體操作,必須對模板進行範例化,即將模板引數範例化,就是用具體的型別引數去替換函數模板中的模板引數,生成一個確定的具體型別的真正函數,才能實現運算操作

  • 範例化方式

    • 隱式範例化:根據具體的函數呼叫形式,推演出模板引數型別
    • 顯式範例化:通過顯式宣告形式指定模板引數型別
  • 隱式範例化

    • 上述程式碼中呼叫Add(2,3)的過程

    • 編譯器根據傳入的實參2和3推斷出模型形參型別是int

    • 會將函數模板範例化出一個int型別的函數

    • int Add(int x1,int x2){
          return x1+x2;
      }
      
    • 編譯器生成具體型別函數的過程稱為範例化,生成的函數稱為模板函數

    • 生成int型別的函數後,再將2和3傳入進行計算

    • 當呼叫Add(3.2,1.4)又會生成一個float的函數

    • 每次呼叫都會根據不同的型別範例化出不同型別的函數,所以最終可執行程式的大小和過載方式相比並不會減少,只是提高了程式設計師對程式碼的複用

    • 問題:隱式範例化不能為同一個模板形參指定兩種不同的型別,例如Add(2,3.2),此時編譯器會報錯,因為編譯器不能推斷出T的型別

  • 顯式範例化

    • cout<<Add<int>(2,3.2)<<endl;//5  cout << Add<int>(2, static_cast<int>(3.2)) << endl;最好的方式是進行一個顯式轉換
      cout<<Add<float>(3,2)<<endl;//5
      
  • 函數模板也可以進行過載

template <class T>
T Add(T x1, T x2){
    return x1+x2;
}

template <class T>
T Add(T x1, T x2, T x3){
    return x1 + x2 + x3;
}

int main(){
    cout<<Add(2,3)<<endl;//5
    cout<<Add(3.2,1.4)<<endl;//4.6
    cout<<Add<int>(2,3.2)<<endl;//5  cout << Add<int>(2, static_cast<int>(3.2)) << endl;最好的方式是進行一個顯式轉換
    cout<<Add<float>(3,2)<<endl;//5
    cout<<Add(1,2,3)<<endl;//6
    return 0;
}
  • 當模板函數和自己寫的Add具有具體的int型別的函數而言,此時如果傳入的引數是int則會呼叫具體化的int函數,即C++編譯器優先考慮普通函數

類別範本

//類別範本定義格式
template <class 形參名, class 形參名>
class 類名{
    
}
  • 由於類別範本包含型別引數,因此也稱為引數化類,如果說類是物件的抽象,物件是類的範例,則類別範本是類的抽象,類是類別範本的範例
template <class T1, class T2>
class Add{
private:
    T1 x1;
    T2 x2;
public:
    Add(T1 x1, T2 x2):x1(x1),x2(x2){}
    T1 get(){
        return x1 + x2;
    }
};

int main(){
    Add<double,int> a(1.2,2);
    cout<<a.get()<<endl;//3.2
    return 0;
}

類別範本範例化

  • 使用類別範本建立物件時,必須指明具體的資料型別,這和函數模板不一樣,這是因為函數模板中一定會傳遞實參,可以根據實參推斷出所有的資料型別,但是類別範本中,建構函式時不一定會同時傳遞T1 T2,所以不一定會百分百推斷出資料型別,因此需要顯式傳遞
  • 可以使用物件指標進行範例化Add<double,int> *p1 = new Add<double,int>(1.2,2);但是需要注意兩邊的模板引數需要相同
template <class T>
class A{
public:
    T Add(T t1, T t2){
        return t1 + t2;
    }
};

int main(){
    A<int> a;
    cout<<a.Add(1.2,2)<<endl;
}
  • 在函數模板中,不能進行自動轉換,這是因為函數模板中需要根據實參來推斷資料型別,而在類別範本中,因為已經顯式指定了int,所以會建立一個int的類,然後可以自動轉換

  • 在類別範本外定義成員函數,格式

template <模板形參表>
函數返回型別 類名<模板形參名>::函數名(參數列){}
template <class T>
class A{
public:
    T Add(T t1, T t2){
        return t1 + t2;
    }
    T sub(T t1, T t2);
};

template<class T>
T A<T>::sub(T t1, T t2) {
    
}
  • 類別範本在範例化時,帶有模板形參的成員函數並不對著自動被範例化,只有當它被呼叫或取地址時才會被範例化。因為不被呼叫的時候,你根本不知道模板形參表中的資料型別是什麼,想範例化就需要把所有的資料型別組合都範例化一遍,這顯然是不可接受的

類別範本與友元函數

非模板友元函數

  • 在類別範本中宣告一個普通的友元函數,該函數時類別範本所有範例的友元函數,可以存取全域性物件,也可以使用全域性指標存取非全域性物件,可以建立自己的物件,也可以存取獨立於物件模板的靜態資料成員
#include <iostream>

using namespace std;

template <class T>
class A{
private:
    T x1,x2;
    static T x3;
public:
    A(T x1,T x2):x1(x1),x2(x2){}
    friend void func();
};
//這裡進行特化賦值
template<>
int A<int>::x3 = 10;

template<>
double A<double>::x3 = 100;


void func(){
    cout<<A<int>::x3<<endl;//10
    cout<<A<double>::x3<<endl;//100
    A<char> a('A','a');
    cout<<a.x1<<" "<<a.x2<<endl;//A a
}

int main(){
    func();
    return 0;
}
  • 如果func()中有一個特定型別的引數,則只能存取特定型別的資料
#include <iostream>

using namespace std;

template <class T>
class A{
private:
    T x1,x2;
    static T x3;
public:
    A(T x1,T x2):x1(x1),x2(x2){}
    friend void func(A<T> a1);
};
//這裡進行特化賦值
template<>
int A<int>::x3 = 10;

template<>
double A<double>::x3 = 100;


void func(A<int> b){
    cout<<A<int>::x3<<endl;//10
//    cout<<A<double>::x3<<endl;//100此時就不可以存取double型別了
    A<char> a('A','a');//可以正常建立別的型別的物件,因為這個建立是在任何地方都可以
//    cout<<a.x1<<" "<<a.x2<<endl;//A a 這裡也不可以存取了,因為不是char型別的友元函數
}

int main(){
    func(A<int>(1,2));
    return 0;
}

約束模板友元函數

  • 這樣的友元函數本身就是一個函數模板,但其範例化型別取決於類被範例化時的型別(被約束),每個類的範例化都會產生一個與之匹配的具體化的友元函數
#include <iostream>

using namespace std;

template <class T>
void func(T x1, T x2);

template <class T>
class A{
private:
    T x1,x2;
public:
    A(T x1, T x2):x1(x1),x2(x2){}
    friend void func<T>(T x1,T x2);//這裡也可以寫成friend void func<>(T x1, T x2);因為可以推斷出T的型別來
};

template <class T>
void func(T x1,T x2){
    A<T> a1(x1,x2);
    cout<<a1.x1<<" "<<a1.x2<<endl;
    cout<<sizeof(A<T>)<<endl;
}

int main(){
    func(1,2);//1 2      8
    func<double>(1,2);// 1 2     16

}

非約束模板友元函數

  • 在類內部宣告友元函數模板,友元函數的模板形參與類別範本的形參沒有關係,此時友元函數為類別範本的非約束模板友元函數
#include <iostream>

using namespace std;

template <class T>
void func(T x1, T x2);

template <class T>
class A{
private:
    T x1,x2;
public:
    A(T x1, T x2):x1(x1),x2(x2){}

    template<class U,class V>
    friend void func(U u, V v);
};

template <class U, class V>
void func(U u, V v){
    cout<<u.x1<<endl;
    cout<<v.x1<<endl;
}

int main(){
    A<int> a1(1,2);
    A<double> a2(2.3,4.5);
    func(a1,a2);//1  2.3
}

模板的特化

  • 模板特化:在原模板類的基礎上,針對特殊型別所進行的特殊化的實現,分為函數模板特化和類別範本特化

類別範本特化

  • 類別範本的特化分為全特化和偏特化
    • 全特化:對類別範本參數列的型別全部都確定
    • 偏特化:對類別範本的參數列中的部分引數進行確定化。分為部分特化和引數進一步限制
#include <iostream>

using namespace std;


template <class A, class B>
class C{
public:
    C(){
        cout<<"template<class A, class B> class C"<<endl;
    }
};

//全特化
template <>
class C<int,double>{
public:
    C(){
        cout<<"template<> class C<int,double>"<<endl;
    }
};

//偏特化
template <class A>
class C<A,int>{
public:
    C(){
        cout<<"template<class A> class C<A,int>"<<endl;
    }
};

//偏特化不一定指的是特化部分引數,而是對模板型別的進一步限制
template <class A, class B>
class C<A*,B*>{
public:
    C(){
        cout<<"template<class A,class B> class C<A*,B*>"<<endl;
    }
};

int main(){
    C<int,int> c1;//template<class A> class C<A,int>
    C<double,double> c2;//template<class A, class B> class C
    C<int,double> c3;//template<> class C<int,double>
    C<int*,double> c4;//template<class A, class B> class C
    C<int*,double*> c5;//template<class A,class B> class C<A*,B*>
}

函數模板特化

  • 函數模板只支援全特化,不支援偏特化
#include <iostream>

using namespace std;

template <class T1,class T2>
int compare(const T1& v1, const T2& v2){
    cout<<"template <class T>"<<endl;
    return 0;
}

template <>
int compare<int,int>(const int& v1, const int& v2){
    cout<<"template<>"<<endl;
    return 0;
}

//不支援偏特化
//template <class T1>
//int compare<T1,int>(const T1& v1, const int& v2){
//    cout<<"template<class T1>"<<endl;
//    return 0;
//}

int main(){
    compare(1,1);//template<>
    compare(1.2,2.3);//template <class T>
}
  • 但是函數偏特化的功能也不是不能實現,並且有兩種方式進行實現
//方式1
//將第二個引數為int的情況排除掉
//然後再寫一個專門的一個引數的模板,這樣可以實現函數模板的功能
#include <iostream>
#include <type_traits>

template <typename A, typename B>
typename std::enable_if<!std::is_same<B, int>::value>::type f(A a, B b) {
    std::cout << "template <typename A, typename B>" << std::endl;
}

template <typename A>
void f(A a, int b) {
    std::cout << "template <typename A>" << std::endl;
}

int main() {
    f(10, 5);  // template <typename A>
    f(10, 5.5);  // template <typename A, typename B>
    return 0;
}
//方式2
//使用結構體進行封裝
#include <iostream>

using namespace std;

template<class A,class B>
struct C{
    C(A a,B b){}
    void operator()() {
        std::cout << "template<class A,class B>" << std::endl;
    }
};
template <typename A>
struct C<A,int>{
    C(A a,int b){}
    void operator()() {
        std::cout << "template <typename A>" << std::endl;
    }
};

int main(){
    C<int,int>(10,5)();//template <typename A>
    C<int,double>(10,5.5)();//template<class A,class B>
    return 0;
}