我们先定义一个函数模板,看看长什么样子
复制内容到剪贴板
代码:
template<typename T>
T max(T a, T b) {
return b < a ? a : b;
}
有个template,它后面的是类型参数列表,在里面可以用typename(class也可以,但是struct可不行)引入一个类型参数,这里叫T,其实叫什么都可以,反正只是一个类型占位符,不过恰当地命名会大大增强代码的可读性,“好的代码本身就是注释”(不过,写代码时命名总是让我头疼的一件事)。
好了,我们成功地引入了一个类型参数,在其后的函数定义中,任何需要类型的地方我们都可以使用这个T了。那这个T到底是什么呢?可以是任何类型,也可以什么都不是(void也是有效的类型哟)。如果你max(3, 4),那T就是int,如果max(0.3, 0.4),那T就是double。这些都不用你操心了,你定义好这个函数群组的样子就可以了,编译器在编译你的代码时会按照你的实际使用需求,生成相应类型版本的函数定义,一个类型生成一个定义,不多不少(感谢勤勤恳恳,任劳任怨的编译器)。如果你在代码中根本没有用到,那编译器也就乐得假装没有看见了。
太好了,我刚刚实现了一个类,Person,来,来,咱俩PK一下,max(my, you)。很不幸,这次你得到的是一大段编译错误,怎么回事?稍微观察一下函数模板的定义,你就会发现,其实这个T是有要求的,不是随便一个类型扔进去,编译器都会欣然接受。首先,这个类型需要定义操作符<,也就是这个类型是可比较大小的;其次,这个类型必须是可复制的,满足函数参数的传递和值的返回。
复制内容到剪贴板
代码:
class Person {
public:
Person(std::string name, const unsigned int age)
: name_(std::move(name)), age_(age) {}
bool operator<(const Person &other) const { return age_ < other.age_; }
private:
std::string name_;
unsigned int age_;
};
这次PK成功了。Nice!
最后说一点题外话。
与普通代码不同,对于模板代码,编译器的编译过程通常分为两个阶段。第一阶段仅仅检查模板定义的语法错误,还有那些不依赖于类型参数的代码。如果你并没有实际使用模板的定义,也就是说编译器不需要用某个特定的类型完成模板的实例化,编译器的工作就到此结束了。在具体类型参数实例化时,编译器进入到第二阶段,这时会用特定的类型替换模板的类型参数,对第一阶段遗漏的所有与类型参数相关的代码进行编译。
所以有时你可能先定义了一个函数模板或类模板,但并没有用到它。代码编译运行一切正常,你开开心心地努力工作着。直到有一天你增加了一点点功能,或者修改了一点点,可能只有一行代码,结果编译器无情地扔给你一大段篇幅的编译错误信息。怎么回事?不可能吧?你反复检查刚才的改动N遍,没有问题啊。很有可能是你的这一点点改动触发了编译器的模板实例化,启动了之前未有过的针对某个模板定义的第二阶段编译,而这次的实例化的类型并不满足模板定义的约束。
还有一点需要说明的是,编译器在实例化模板的过程中需要模板的完整定义,原因很简单,编译器要给你生成这个类型版本的完整定义,所以模板通常都是完整地定义在头文件中。
好了,先到这里,希望学习始终是轻松快乐的。