对于有着相同形式的函数,如果仅仅只是参数类型不同,那么给每个不同参数表的函数都重新写一份拷贝是很麻烦的事情,模板元编程允许我们用泛型的方式去编写一个函数,而模板与运算符重载配合起来的威力更是惊人,使代码的复用性大大提高,可读性也更好。
泛型编程可以说是C++最重要的性质之一,强调了面向对象的特性。下面我们来介绍模板的使用方法
1.函数模板
一个函数的模板如下:
template<typename T> int compare(const T& v1,const T& v2)
{
if(v1<v2)return -1;
if(v1>v2)return 1;
return 0;
}
template关键字后跟一个模板参数列表,其中包含了模板参数,根据这些参数来确定模板的实例
如果将模板看做函数的话,那么模板参数就是模板的形参
在这个例子里,我们定义了一个类型名称T,那么接下来所有的T代表了函数上下文中所有的T
在函数参数表中有两个T引用类型的形参,我们根据他们的比较结果返回相应的值(前提是T类型的对象允许比较运算符或者有比较运算符的重载)
函数模板的具体实例化过程发生在编译期,当编译器检测到函数模板的调用的时候,会检查函数参数的类型,然后寻找相对应的实例,例如我们调用compare(1,2),编译器发现这是两个int常量,将会编译将T替换成int的版本,并且让compare(1,2)调用这个版本的实例
模板参数包括类型模板参数和非类型模板参数,当我们使用关键字typename时,代表我们希望接下来出现的参数名T指代一个类型名。而非类型参数有着更明显的用途,它表示一个值,我们通过特定的类型名而非关键字class或者typename指定。
例如
template<unsigned N,unsigned M> int compare(const char (&p1)[N] , const char(&p2)[M])
{
return strcmp(p1,p2);
}
当我们使用compare("hi","mom");时,模板会实例化N=3,M=4的版本(因为字符串末尾还有一个空字符作为终结符)
/*形参是数组指针的时候,并不能根据数组长短判断模板的实例,但是若形参是数组引用,则可以,形参长度必须和实参一致,理由暂不清楚,听说Effective C++里讲了*/
非类型模板参数可以是一个整形,或者是一个指向对象或函数类型的指针或引用(即传递了我使用了什么样的对象和什么样的函数的信息),值得注意的是,当我们试图通过函数参数表中参数的各种信息判断模板的实例的时候,绑定到非类型的整形参数的实参必须是一个常量表达式,而绑定到指针或者引用(指向常量对象或者函数)的实参必须具有静态的生存期,即在整个编译期间,该实参都存在
/*对于非类型模板参数可以是指针或者引用,我是不太理解它解决了什么问题的,毕竟利用函数参数的传递也可以达到效果,可能的解释是模板的具体实例化允许不同的模板参数有完全不一样的执行代码,但是这样做和重新写另一个不同的函数并没有区别,也就是破坏了类型无关性,模板的特点没有体现,这个以坑后再填*/
函数模板是可以有constexpr和inline关键字的,这三者都是编译期的特性,由编译器管理
模板的一大好处就是可以编写类型无关的代码,例如上面的例子,int类型的两个量可以比较,unsigned,long,double都可以,也就是说这段代码的逻辑与类型无关,只要它们支持“<”运算符和“>”运算符,那么就可以用在不同的实例上,如果不支持,那么会出现编译错误
模板的错误报告:
通常,编译器会分三个阶段报告错误
1.在编译模板本身时,由于类参数和非类参数都未确定,所以对于很多可能是错误的地方,编译器将不报错,这时候发现的错误一般就是很基础的语法错误,例如分号,变量名,括号之类的parser错误
2.在编译器遇到模板使用的时候,编译器检查调用函数的实参数目和类型是否匹配,如果不匹配就代表没有相应的模板实例
3.当编译器确定了模板的实例,将之实例化的时候,这个时候会发现类型相关错误,和C++标准禁止的操作等,这个时候才能真正的找到所有语法错误,但是编译器管理实例化的方式是不同的,有可能在链接时才报告
2.类模板
与函数模板不同的是,类模板并不能从函数的实参推测模板参数从而实例化,而是在尖括号中直接给出相对应的模板参数从而实例化
定义类模板
template<typename T>class Blob
{public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;//这段话的意思是将vector实例化中定义的size_type类显式地建立一个名为size_type的别名,方便理解
Blob();//构造函数
Blob(std::initializer_list<T> il);//利用了标准库的initializer_list来初始化Blob,表示Blob构造函数接受的是一个以T实例化过的initializer_list类对象il
size_type size() const{ return data->size(); }
bool empty const{ return data->empty(); }
void push_back(const T& t) { data->push_back(t); }
void pop_back();//先声明,后面再定义
T& back();
T& operator[](size_type i);//重载"[]"运算符
private:
std::shared_ptr<std::vector<T>> data;//这里才是真正的数据,这个Blob类实际上是把vector又封装了一遍
void check(size_type i, const std::string& msg) const;//检查data[i]是否有效,若无效则抛出error message,显示msg字符串
}
如果一个模板中使用了另一个模板,那么我们会将带有参数的内层模板,也就是待定的实例当作参数传给外层模板,而不是简单的将内层模板的参数给到外层模板做参考
好处是,1.在外层模板实例化的过程中,会先检查内层模板,并且实例化内层模板,然后再实例化外层,避免了先外层后内层,带来不必要的潜在问题2.体现了封装的思想,作为内层模板的参数,默认是不应该被外层模板所直接可见的,除非需要这么做,外层模板的参数的最好只是内层的实例,而不包含内层实例化的具体实现
类模板的成员函数
每个类模板实例有自己版本的成员函数,所以在类定义之外定义成员函数,就需要带上template关键字
template<typename T> void Blob<T>::check(size_type i, const std::string &msg) const
{
}
我们来分析语法,template<typename T> Blob<T>::语句说明了作用域是某个实例,这个实例以参数T作为参考,void表示这个函数的返回值,check说明函数名。
乍一看语法可能比较别扭,因为一般来说函数返回类型打头,但是对于模板来说,是编译器特性(C++的特性大多是编译器特性,可以认为给C加了一个plugin,多了个中间层,我们通过LLVM也可以做到),所以必须template<typename T>打头表明模板参数,这个同时解决了模板作用域的问题,在类模板外使用类模板名的时候,我们要以template关键字和模板参数列表打头,表明模板的实例的参考,那么在定义函数的时候就可以使用模板返回类型,因为这个模板的参数已经给定,例如:
template<typename T> BlobPtr<T> BlobPtr<T>::operator++(int)
{
BlobPtr ret= *this;
++(*this);
return ret;
}
以上语法是函数返回类型BlobPtr<T>,函数所在域BlobPtr<T>,函数重载了++符号
在进入了函数作用域以内之后,我们无需重新强调模板实参T,除非我们将要返回的类型是其他模板的实例或者这个模板的其他实例,在默认不给定模板参数的情况下,这个模板的参数就是T
奇思妙想:
假如我们要一个实例的成员函数返回同一模板另一个实例类型的值,这是不可能的,因为:
实例不可见模板本身,也不可见其他实例,只能看见本实例,当你定义函数的时候,首先实例化,然后
关于类模板和友元的声明
在类模板中生命非模板友元,那么这个友元拥有所有类模板实例的友元权限
也可以直接给定模板参数,让特定的类模板实例拥有友元权限
甚至可以将模板参数T作为友元也可以
再次需要强调的是,类模板的不同实例之间并没有语义上的联系,也就是说属于完全不同的两个类,那么如果有静态成员,不同实例之间也是不共享的