C++模板与范型编程
模板定义以关键字template开始,后接模板形参表(template parameter list), 模板开参表是用尖括号括住的一个或多个模板形参的列表,形参之间用逗号分隔。
模板形参表:
模板形参表与函数形参表类似,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化形参。同样,模板形参表示可以在类或函数的定义中使用的类型或值。
模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。非类型形参跟在类型说明符之后声明。类型形参跟在关键字class或typename之后定义,如class T 是名为T的类型的形参,在这里class和typename没有区别。
使用函数模板时,编译器会推断哪个(或哪些)模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。实质上,编译器会确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生并编译该版本的函数,编译器承担了为我们使用的每种类型而编写函数的单调工作。
如: template <typename T> int compare(const T &v1,const T &v2){ …. }
调用时: compare(1,0); 编译器会自动为我们产生一个整型在的此版本函数。
类模板:
可以像定义函数模板一样,我们也可以来定义类模板。类模板本身也是模板,必须以关键字template开头,后接模板形参表。我们来看下面的例子:
template< class Type> class Queue
{
public:
Queue();
Type& front();
const Type& front() const;
void push(const Type &);
void pop();
bool empty() const;
private:
};
类模板可以定义数据成员、函数成员和类型成员,也可以使用访问标号控制对成员的访问,还可以定义构造函数和析构函数等。在类和类成员的定义中,可以使用模板形参作为类型或值的点位符,在使用类时再提供那类型或值。
模板形参:
像函数形参一样,程序员为模板形参选择的名字没有本质含义,在上面的例子中我们将形参命名为Type, 其实我们可以将它命名为任意名字。可以给模板形参赋予的唯一含义是区别形参是类型形参还是非类型形参,如果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知道它是一个未知值。
模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。模板形参遵循常规名字屏蔽规则,与全局作用域中声明的对象、函数或类型同名的模板形参或屏蔽全局名字。 用作模板形参的名字不参在模板内部重用,因为重用就相当于定义了多个同名的变量。
与其它任意函数或类一样,对于模板可以只声明而不定义。声明必须指出函数或类是一个模板(前面要有 template <模板形参列表>)。
Typename与class的区别:
在函数模板形参表中,关键字typename 和 class具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用。使用关键字typename代替关键字class指定模板类型形参可能更为直观,毕竟,可以使用内置类型(非类类型)作为实际的类型形参,而且 typename是作为标准c++的组成部分加入到c++ 中的,因此旧的程序更有可能只用关键字class。
非类型模板形参:
模板形参不必都是类型。在调用函数时非类型形参将用值代替,值的类型在模板形参表中指定。如下面的例子中N就是一个非类型的形参的函数模板。函数本身接受一个形参,该形参是数组的引用 :
template <class T , size_t N> void array_int(T (¶m)[N])
{
for(size_t i=0;i!=N;i++)
{
param[i] = 0;
}
}
模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可以使用非类型形参指定数组的长度,当调用此函数时,编译器从数组实参计算非类型形参的值。
实例化:
模板是一个蓝图,它本身不是类或函数,编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程称为实例化,这个术语反映了创建模板类型或模板函数的新”实例”的概念。 模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。
当编写Queue<int> qi时,编译器自动创建名为Queue<int>的类,实际上,编译器通过重新编写Queue模板,用类型int代替模板形参的每次出现而创建Queue<int>类,实例化的类就像我们自己编写的一样。
要想使用类模板,就必须显式地指定模板实参,类模板不定义类型,只有特定的实例才定义了类型。特定的实例化是通过提供模板实参与每个模板形参匹配而定义的。
要确定应该实例化哪个函数,编译器会查看每个实参,如果相应形参声明为类型形参的类型,则编译器从实参的类型推断形参的类型。从函数实参确定模板实参的类型和值的过程叫做模板实参推断。
模板编译模型:
当编译器看到模板定义的时候,它不立即产生代码。只有在看到用到模板时,如果调用了函数模板或调用了类模板的对象的时候,编译器才产生特定类型的模板实例。一般而言,当调用函数的时候,编译器只需要看到函数的声明,类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。模板则不同,要进行实例化,编译器必须能够访问定义模板的源代码。
标准 c++为编译模板代码提供了两种模型。在两种模型中,构造程序的方式在很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义。所有编译器都支持“包含模型“,只有一些编译器支持”分加模型“。
(1) 包含型编译: 编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条#include 指示使定义可用. [即在头文件中include对应的源文件,如在a.h中用#include “a.cc”]。 这一策略使我们能够保持头文件和实现文件的分离,并且保证在编译时使用模板的代码时能看到两种文件。
(2)分别编译模型: 编译器会为我们跟踪相关的模板定义,但是我们必须让编译器知道要记住给定的模板定义,可以使用export关键字来做这件事。
模板特化:
类模板特化的意思是,对于某个特定的类型,需要对模板进行特殊化,即特殊的处理。例如,stack类模板针对bool类型有特化,因为实际上bool类型只需要一个二进制位,就可以对其进行存储,使用一个字或者一个字节都是浪费存储空间的.
同样,函数模板特化也是针对某个特定类型的特殊处理,一个比较经典的例子:
template <class T> T mymax(const T t1, const T t2)
{
return t1 < t2 ? t2 : t1;
}
main()
{
int highest = mymax(5,10);//正确结果
char c = mymax(‘a’, ’z’);//正确结果
const char* p1 = “hello”;
const char* p2 = “world”;
const char* p = mymax(p1,p2); //错误结果,因为比较的是指针,而不是内容
}
如果需要得到正确结果就需要针对const char*的函数模板特化:
const char* mymax(const char* t1,const char* t2)
{
return (strcmp(t1,t2) < 0) ? t2 : t1;
}
模板偏特化,partial specialization of template
模板的偏特化是指需要根据模板的某些但不是全部的参数进行特化。
(1) 类模板的偏特化
例如c++标准库中的类vector的定义
template <class T, class Allocator> class vector { // … // };
template <class Allocator> class vector<bool, Allocator> { //…//};
这个偏特化的例子中,一个参数被绑定到bool类型,而另一个参数仍未绑定需
要由用户指定。
(2) 函数模板偏特化
严格的来说,函数模板并不支持偏特化,但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。
template <class T> void f(T); (a)
根据重载规则,对(a)进行重载
template <class T> void f(T*); (b)
如果将(a)称为基模板,那么(b)称为对基模板(a)的重载,而非对(a)的偏特化。C++的标准委员会仍在对下一个版本中是否允许函数模板的偏特化进行讨论
★模板特化时的匹配规则
(1) 类模板的匹配规则:
最优化的优于次特化的,即模板参数最精确匹配的具有最高的优先权。例子:
template <class T> class vector{//…//}; // (a) 普通型
template <class T> class vector<T*>{//…//}; // (b) 对指针类型特化
template <> class vector <void*>{//…//}; // (c) 对void*进行特化
每个类型都可以用作普通型(a)的参数,但只有指针类型才能用作(b)的参数,而只有void*才能作为(c)的参数
(2) 函数模板的匹配规则
非模板函数具有最高的优先权。如果不存在匹配的非模板函数的话,那么最匹配的和最特化的函数具有高优先权
例子:
template <class T> void f(T); // (d)
template <class T> void f(int, T, double); // (e)
template <class T> void f(T*); // (f)
template <> void f<int> (int) ; // (g)
void f(double); // (h)
bool b;
int i;
double d;
f(b); // 以 T = bool 调用 (d)
f(i,42,d) // 以 T = int 调用(e)
f(&i) ; // 以 T = int* 调用(f)
f(d); // 调用(h)
★模板特化时的三种常见类型
(1)特化为绝对类型: 也就是说直接为某个特定类型做特化,这是我们最常见的一种特化方式,如特化为float, double等
template<> class Compare<float>
{
public:
static bool IsEqual(const float& lh, const float& rh)
{
return abs(lh - rh) < 10e-3;
}
};
(2) 特化为引用,指针类型
这种特化我最初是在stl源码的的iterator_traits特化中发现的,如下:
template <class _Iterator> struct iterator_traits {
typedef typename _Iterator::iterator_category iterator_category;
typedef typename _Iterator::value_type value_type;
typedef typename _Iterator::difference_type difference_type;
typedef typename _Iterator::pointer pointer;
typedef typename _Iterator::reference reference;
};
这种特化其实是就不是一种绝对的特化,它只是对类型做了某些限定,但仍然保留了其一定的模板性,这种特化给我们提供了极大的方便,如这里,我们就不需要对int*, float*, double*等等类型分别做特化了。
(3)特化为另外一个类模板
这其实是第二种方式的扩展,其实也是对类型做了某种限定,而不是绝对化为某个具体类型,如下:
// specialize for vector<T>
template<class T> class Compare<vector<T> >
{
public:
static bool IsEqual(const vector<T>& lh, const vector<T>& rh)
{
if(lh.size() != rh.size()) return false;
else
{
for(int i = 0; i < lh.size(); ++i)
{
if(lh[i] != rh[i]) return false;
}
}
return true;
}
};
这就把IsEqual的参数限定为一种vector类型,但具体是vector<int>还vector
<float>,我们可以不关心,因为对于这两种类型,我们的处理方式是一样的,我们可以把这种方式称为“半特化”。当然,我们可以将其“半特化”为任何我们自定义的模板类类型。