Skip to content

模板

模板特化与偏特化

模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。

模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。


template<typename T1, typename T2>
class Test
{
public:
    Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
    T1 a;
    T2 b;
};

template<>
class Test<int , char>
{
public:
    Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
    int a;
    char b;
};

template <typename T2>
class Test<char, T2>
{
public:
    Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:
    char a;
    T2 b;
};

头文件源文件不可分离

“传统”编程方法(在.h文件声明,在.cpp文件中定义)不能用于模板。

// array.h
template <typename T, int SIZE>
class array
{
    T data_[SIZE];
    array (const array& other);
    const array& operator = (const array& other);
public:
    array(){};
    T& operator[](int i) {return data_[i];}
    const T& get_elem (int i) const {return data_[i];}
    void set_elem(int i, const T& value) {data_[i] = value;}
    operator T*() {return data_;}      
};  

然后在main.cpp文件中的主函数中使用上述模板:

// main.cpp
#include "array.h"

int main(void)
{
array<int, 50> intArray;
intArray.set_elem(0, 2);
int firstElem = intArray.get_elem(0);
int* begin = intArray;
}

这时编译和运行都是正常的。程序先创建一个含有50个整数的数组,然后设置数组的第一个元素值为2,再读取第一个元素值,最后将指针指向数组起点。

将array.h文件分裂成为array.h和array.cpp二个文件(main.cpp保持不变)

// array.h        
template <typename T, int SIZE>
class array
{
      T data_[SIZE];
      array (const array& other);
      const array& operator = (const array& other);
  public:
      array(){};
      T& operator[](int i);
      const T& get_elem (int i) const;
      void set_elem(int i, const T& value);
      operator T*();      
};  
// array.cpp
#include "array.h"

template<typename T, int SIZE> T& array<T, SIZE>::operator [](int i)
    {
    return data_[i];
    }

template<typename T, int SIZE> const T& array<T, SIZE>::get_elem(int i) const
    {
    return data_[i];
    }

template<typename T, int SIZE> void array<T, SIZE>::set_elem(int i, const T& value)
    {
    data_[i] = value;
    }
template<typename T, int SIZE> array<T, SIZE>::operator T*()
    {
    return data_;
    }

编译时会出现3个错误。

模板编译

​ 当编译器遇到一个模板定义时,并不生成代码。只有当我们实例化出一个模板的特定版本时,编译器才会生成代码。当我们使用模板时,编译器才生成代码,这个特性决定了我们如何组织代码以及错误何时被检测到。

​ 通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。

​ 模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。

函数模板和类成员模板的定义通常放在头文件中。

SFINAE(substitution failure is not a error)

​ 主要用于模板函数,它是指,编译器在使用具体类型来替换模板类型参数,对模板进行实例化(展开模板)时,如果发生替换失败,那么并不会直接引发编译错误(Error),而只是简单地把这个模板从重载候选者中去除掉。

​ 在真正发生调用(应该说真正需要被展开)之前,模板函数中的信息是不完备的,编译器无法为这些模板函数生成真正的执行代码,而只是进行一些很基本、简单的检查。所有的模板都不是“真正的代码”,它们是编译器用来生成代码的工具。在需要展开的时候,编译器从合适的候选者中选出优先级最高的一个来进行实例化(展开)。在展开后的代码如果不能正确被编译(像上面例子中 int::* 这种情况),编译器只是简单地放弃这次展开,转而寻找其它的模板。试想,如果编译器在展开失败后,直接产生一个编译错误的话,其它的函数就没有机会了,这是非常不合理的,因为:

1.本次展开失败并不意味着被展开的模板代码就有问题,因为用其它类型的话还是有可能展开成功的。

2.本次展开失败并不代表用于展开的类型无法找到合适的模板,其它模板可能合用。