模板与泛型


函数模板

函数模板定义为

template<class 参数1, class 参数2,...>
返回值类型 模板名(形参表)
{
	函数体
}

函数模板可以重载,只要它们形参表不同即可,下面两个模板可以同时存在:

template<class T1, class T2>
void print(T1 arg1, T2 arg2){
	cout << arg1 << "" << arg2<<endl;
}
template<class T>
void print(T arg1, T arg2){
	cout << arg1 << "" <<arg2 <<endl;
}

当我们调用函数模板时,编译器会对函数的实参进行类型推断,从而得出模板参数T的类型,这个过程称为模板的实例化(instantiate)。当编译器实例化一个模板后,得到的函数是一个有确定签名的函数,这个函数称为模板的实例(instance)

函数模板匹配规则

C++编译器如何决定选用选用哪个函数,遵循以下优先顺序

  1. 先找参数完全匹配普通函数(非由模板实例化而得的函数)
  2. 再找参数完全匹配的模板函数
  3. 再找实参经过自动类型转换后能够匹配的普通函数
  4. 上面的都找不到,则报错
template<class T>
T max(T a, T b){
	return 0;
}
template<class T1, clas T2>
T max(T1 a, T2 b){
	return 0;
}
double max(double a, double b){
	return 0;
}

int main(){
	int i=4, j=5;
	max(1.2,3.5);//调用max(double, double)
	max(i,j);//调用第一个max函数
	max(1.3,2);//调用第二个max函数
	return 0;
}

非类型模板参数

除了在函数模版中定义模板类型参数外,还可以在模板中定义非类型参数

当模板被实例化时,这些参数会被自动推断出来,例如下面例子中,compare函数用来比较两个字符数组,由于数组不能拷贝,因此参数为两个数组的引用,数组的长度用两个非类型参数表示:

tempplate<unsigned N, unsigned M>
compare(const char(&p1)[N], const char(&sp2)[M]){
	return strcmp(p1,p2);
}
compare("h1","h11")

当调用compare时,编译器会推断出NM的值来实例化模板,上述函数模板会被编译器实例化为

compare(const char(&p1)[3], const char(&sp2)[4]) //考虑\0

使用非类型的模板参数,需要注意的是这些参数只能是值或者常量表达式

inline与const

函数模板可以被声明为inline的,inline说明符放到模板参数列表之后

tempalte <typename T>
inline T min(const T& , const T&);

另一个需要注意的点是,可以将函数模板的参数声明为const用来满足一些不支持拷贝的数据类型。

模板的编译

当编译器遇到一个模板定义时,并不会立刻进行模板展开,只有当模板被使用时(被实例化出一个特定版本),编译器才会生成相应的代码。这一特性决定了模板的使用方式。

通常,我们在调用函数时有声明就够了,声明包含了这个函数的签名信息以及符号。类似的,当我们使用一个类对象时,类对象的定义必须是可用的,但成员函数的定义可不需要,道理和普通函数相同。因此,在C++中我们可以将类定义和函数声明放在头文件中,而将普通函数的定义和成员函数的定义放在源文件中。

对于模板类或者函数,情况则不一样,为了实例化一个模板,编译器必须要了解函数的定义才能推断出类型参数。因此,函数模板或类模板成员函数的定义通常放在头文件中

类模板

类模板与函数模板类似,是一种泛型技术,但是和函数模板不同的是,类模板的类型参数无法靠编译器推断,必须由使用者指定。类模板的定义方式如下

template<类型参数表>
class 类模板名
{
	成员函数和成员变量
};

其中,类型参数表可以有多个参数,比如class T, class M,...。本节中,我们以一个Blob类为例

template <typename T>
class Blob{
    typedef typename std::vector<T>::size_type size_type;
private:
    std::shared_ptr<vector<T>> data;
public:
    Blob():data(std::make_shared<vector<T>>()){};
    Blob(std::initializer_list<T> il):data(std::make_shared<vector<T>>(il)){}
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    void push_back(const T& ele){ data->push_back(ele);}
    void push_back(T&& ele){ data->push_back(ele);}
    void pop_back();
    T& back();
    T& operator[](size_type i);
};

上面代码中,我们定义了一个Blob的类模板,由类模板实例化得到的类叫模板类。例如,我们对Blob模板进行int型的实例化,则会产生一个新的模板类Blob<int>,相应的,编译器会为其生成如下代码

template<>
class Blob<int>{
private:
	std::shared_ptr<vector<int>> data;
public:
	...
	Blob(std::initializer_list<int> il);
	...
	int& operator[](size_type i);
}

上述代码形式也叫做模板的特例化,我们在后面还会讨论这种形式。需要注意的是,同一个类模板的两个模板类是不兼容的。比如:

Blob<int>
Blob<string>

此外,上述代码中,有些成员的定义未在类定义中展开,如果要在类外定义成员函数,则要使用下面语法:

template<形参表>
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表){}

例如,我们可以继续实现back,pop_back方法和[]符号重载

template<typename T>
T& Blob<T>::back(){
    return data->back();
}
template<typename T>
T& Blob<T>::operator[](size_type i){
    return (*data)[i];
}
template<typename T>
void Blob<T>::pop_back(){ data->pop_back(); }

正如前面一节在介绍模板编译时曾提到,模板函数的实例化发生在该函数被调用的时候,例如下面代码

//1. 实例化Blob<string>
Blob<string> articles = {"a","an","the"}; //2. 实例化构造函数

如果一个成员函数没有被调用,那么它将不会被实例化出来,也就不会进行语法检查,因此对于没有被使用的模板函数,即是它出现模板类型不匹配的情况,也不会被编译器发现,而拥有该成员函数的类模板仍然可以被正常的实例化。

最后,如果在一个类模板中出现该类本身,则可以忽略类型参数,例如下面代码

template <typename T>
class Blob{
	//...
    Blob getBlob(){ //此处可直接返回Blob,而不必使用Blob<T>
        return Blob();
    }
}

模板参数

成员模板

  • 函数模板作为类模板成员
template<class T>
class A
{
pubic:
	template<class T2> //类模板和函数模板的参数不能一致
	void Func(T2 t){cout<<t;}; //成员函数模板
};
int main(){
	A<int> a;
	a.Func('k');
}
  • 类模板的参数声明中可以包括非类型参数
template<class T, int elements>
  • 非类型参数:用来说明类模板中的属性
  • 类型参数:用来说明类模板中的属性类型,成员操作的参数类型和返回值类型
template<class T, int size>
class CArray{
	T array[size];
public:
	void print(){
		for(int i=0;i<size;++i>){
			cout<<array[i]<<endl
		}
	}
}
CArray<double,40> a2;
CArray<int,50> a3;
//注意:CArray<double,40>和CArray<int,50>完全是两个类,这两个类对象之前不能相互赋值 
  • 类模板与继承
    • 类模板派生出类模板
      tempalte<class T1, class T2>
      class A{
          T1 v1;
          T2 v2;	
      };
      template <class T1, class T2>
      class B: public A<T2,T1>{
          T1 v3;
          T2 v4;
      };
      template <class T>
      class C:public B<T,T>{
          T v5;
      };
      int main(){
          B<int,double> obj1;
          C<int> obj2;
          return 0;
      }
    
    • 模板类派生出类模板
      template<class T1, class T2>
      class A{
          T1 v1;
          T2 v2;
      };
      template<class T>
      class B:publicA<int, double> {
          T v;
      }
      int main(){
          B<char> obj1;
          return 0;
      }
      //自动生成两个模板类:A<int,double>和B<char>
    
    • 普通类派生出类模板
      class A{ 
          int v1;	
      };
      template<class T>
      class B:public A{
          T v;
      };
      int main(){
          B<char> obj1;
          return 0;
      }
    
    • 模板类派生出普通类
      template<class T>
      class A{
          T v1; int n;
      };
      class B:public<int>{
          double v;
      };
      int main(){
          B obj1;
          return 0;
      }
    

Resources