模板是C++泛型编程的基础,一个模板就是一个创建类或函数的公式或者蓝图。
1.1 函数模板(function template)
函数模板就像是一个公式,可用来生成针对特定类型的函数版本。例如下面的compare模板版本:
template<typename T>
int compare(const T &v1, const T &v2){
if(v1 < v2) return -1;
if(v2 < v1) return 1;
return 0;
}
模板定义以关键字template开始,后跟一个模板参数列表,这是一个用逗号分隔的一个或者多个模板参数的列表,用<>包围起来。在模板定义中,模板参数列表不能为空。
模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们,运行时使用实参来初始化形参。
模板参数表示在类或者函数定义中用到的类型或者值,使用模板时,(隐式或者显式)指定模板实参将其绑定到模板参数上。T表示一个类型,T的实际类型在编译时根据compare的使用情况而定。
实例化函数模板
当调用函数模板时,编译器通常用函数实参可以推断出模板实参,例如:
cout<< compare(1,0)<<endl; // T为int 实参类型为int 推断出模板实参为int并绑定到模板参数T
编译器用推断出的模板参数来实例化一个特定版本的函数:
int compare(const int&,const int&);
模板类型参数
上面的 compare
函数有一个模板类型参数(type parameter),可以将类型参数看作类型说明符。类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
template <typename T> T foo(T* p)
{
T tmp = *p; // tmp的类型将是指针P指向的类型
// ...
return tmp;
}
类型参数前必须使用关键字class
或者template
,这两者没有不同,但template
比class
更直观。
非类型模板参数
一个非类型参数表示一个值而非一个类型,通过一个特定的类型名而非关键字class
或者template
指定。
template<unsigned N, unsigned M>
// 将参数定义为数组的引用
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1,p2);
}
调用时编译器会使用字面常量的大小来代替N和M,从而实例化模板,编译器会在一个字符串常量的末尾插入一个空字符作为终结符,于是实例化出如下版本:
compare("hi","mom");
int compare(const char (&p1)[3], const char (&p2)[4])
非类型模板参数的模板实参必须是常量表达式。
inline
和constexpr
函数模板
函数模板可以声明为inline
和constexpr
的,如同非模板函数一样。inline
和constexpr
说明符放在模板参数列表之后,返回类型之前。例如:
template<typename T> inline T min(const T&, const T&);
编写类型无关的代码
泛型编程的重要原则:
1、模板中的函数参数是const
的引用
2、函数体的条件判断仅使用<
比较运算
3、模板程序应该尽量减少对实参类型的要求
模板编译
函数模板和类模板成员函数的定义通常放在头文件中
大多数编译错误在实例化期间报告
类模板(class template)用来生成类的蓝图,与函数模板不同的是,编译器不能为类模板推断模板参数类型,为了使用类模板,必须在模板名后的<>中提供额外信息用来代替模板参数的模板实参列表。类似于标准库容器,需要指出元素类型。
定义类模板
类模板以关键字template开始,后跟模板参数列表,将模板参数当作替身,代替使用模板时用户需要提供的类型或值。
template <typename T> class Blob{
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type ;
// 构造函数
Blob();
Blob(std::initializer_list<T> il;)
...
...
}
Blob
模板有一个名为T的模板类型参数,用来表示Blob保存的元素的类型,实例化时,T就会被替换为特定的模板实参类型。实例化类模板
用上面的模板定义一个类型,必须提供元素类型:
Blob<int> ia; // 空Blob
Blob<int> ia2 = {0, 1, 2, 3}; // 有5个元素的Blob
当编译器从模板实例化出一个类时,会重写Blob模板,将模板参数的T每个实例替换为给定的模板实参。
Blob<string> names;
Blob<double> prices;
注意:一个类模板的每个实例都会形成一个独立的类,它们之间没有任何关联,它们之间的成员也不会有特殊访问权限
类模板的成员函数
与其他任何类一样,既可以在类模板内部,也可以在类模板外部为其定义成员函数,在内部的成员函数被隐式地声明为内联函数,在类外定义时,需要说明成员属于哪个类。
template <typename T>
ret-type Blob<T>::member-name(param-list)
类模板成员函数地实例化
默认情况下,对于一个实例化了地模板,其成员只有在被使用时才会被实例化。
在类模板内简化模板类名的使用
使用一个类模板类型时必须提供模板实参,但在类模板的作用域内是例外,可以直接使用而不用提供实参
在类模板外使用 类模板名
在类模板外定义成员时,并不在类的作用域中,直到遇到类名才进入类的作用域
template <template T>
Blob<T> Blob<T>::operator++(int)
{
Blob ret = *this;// 等价于
// Blob ret = *this;
++*this;
return ret;
}
由于返回类型不在类的作用域内,必须指定模板实参,函数体内部已经进入类的作用域,不用提供。
类模板和友元
当一个类包含一个友元声明时,类与友元各自是否是模板相互无关。
一对一友好关系
类模板与另一个(类或函数)模板间友好关系最常见的形式是建立对应实例及其友元间的友好关系。
通用与特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
令模板自己的类型参数成为友元
模板类型别名
typedef Blob<strring> StrBlob;// 模板不是一个类型,不能定义一个typedef引用一个模板
新标准允许我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>; //将twin定义为成员类型相同的pair的别名 twin用户只需指定一次类型
twin<int> win_loss; // pair
// 定义模板类型别名时,可以固定一个或多个模板参数
template<typename T> using partNo = pair<T, unsigned>;
类模板的static成员
与其他任何static数据成员相同,模板类的每个static数据成员必须有且只能有一个定义,但类模板的每个实例都有一个独有的static对象,
模板参数与作用域
在模板内不能重用模板参数名,一个模板参数名只能在一个特定模板参数列表中出现一次。
模板参数会隐藏外层作用域中声明的相同名字
template<typename A, typename B> void f(A a, B b)
{
A tmp = a;// tmp的类型为参数A的类型,而非double
double B;// 错误 重声明模板参数
}
使用类的类型成员
希望通知编译器一个名字表示类型时,必须使用关键字typename,而不是class。
函数默认实参与类模板
无论何时使用一个类模板,都必须在模板名之后加上<>,指出类必须由一个模板实例化而来,如果我们使用了所有的默认实参,就必须在模板名之后加上一个空的<>
一个类(无论是普通类或者是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板,成员模板不能是虚函数。
普通(非模板)类的成员模板
类模板的成员模板
类和成员各自有各自的独立的模板参数。
// 构造函数有自己的模板类型参数It 作为它的两个函数参数的类型
template <typename T> class Blob{
template <typename It> Blob(It b, It e);
//
...
}
当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表,类模板参数列表在前,后跟成员自己的模板参数列表:
template <typename T> // 类的类型参数
template <typename It> // 构造函数的类型参数
Blob<T>::Blob(It b, It e);
实例化与成员模板
实例化一个类模板的成员模板,必须同时提供类和函数的模板的实参。
int ia[] = {0,1,2,3,4,5};
Blob<int> a1(begin(ia), end(ia));
定义a1时,显式地指出编译器应该实例化一个int版本地Blob,构造函数自己地类型参数则通过begin(ia)和end(ia)地类型来判断,结果为int*。因此,a1的定义实例化了如下版本:
Blob<int>::Blob(int*,int*);
大系统中在多个文件中实例化相同模板的额外开销比较大,可以通过显式实例化来避免开销。
extern template declaration; // 实例化声明
template declaration; // 实例化定义
// declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参
extern template class Blob<string>;
template int compare(const int&, const int&);
当编译器遇到extern时模板声明时,不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明,对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数。因为编译器会实例化该类的所有成员。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。