模板只有类模板和函数模板,以下是各种黑科技。
①把实现的模板类或函数拆分到h和cpp中
首先需要明确,传统上我们在h中写完整的definition,对于包这个头文件的模板使用者来说,编译器模板展开后就是内联的用法。
如果我们只在h中写declaration,对于包这个头文件的模板使用者,展开后就是使用的声明,编译阶段没有任何问题;在链接阶段会去链接展开后的实现的。
// A.hpp class A { public: template <bool isPrepareStage> void consume(int64_t size); }
// A.cpp #include "A.hpp"; template <bool isPrepareStage> void A::consume(int64_t size) { // 实现 } template void A::consume<true>(int64_t); template void A::consume<false>(int64_t);
这个例子演示了模板函数(方法)拆开declaration和definition的一种方式,主要就是在cpp中用template前缀显式地生成若干种展开后的模板函数(方法),编译到该cpp的object文件中,这样可以被其他的调用者链接上。
模板类的拆分方法也类似。
顺便说下C++的内联。
C++标准规定,任何使用内联的地方,都要有该内联函数/方法的完整而相同的definition(如果definition不一样最后程序的行为是未知的)。
要实现这一点,我们常用的方法就是把内联函数/方法的definition全部写在头文件里(正如我们写模板时常用的那样)。而且实现在头文件时,对于方法来说,不用写inline关键字就是内联的;对于函数,则必须使用inline关键字(否则就违反了C/C++的一次定义原则,编译器只会对inline的函数来规避重复定义的问题)。
很多代码洁癖患者在实现类的时候,把方法照旧拆分到h和cpp中,但是又都标记上内联,这是又想马儿跑又想马儿不吃草。如果这个方法只有这个cpp用,那么没啥问题,正常内联cpp内的实现;如果别人包了头文件,使用这个方法的时候却只有一个inline的方法declaration,所以编译的时候就会报错了,因为这时候顶多只能走后期的链接,无法展开代码来内联。
另外C++标准还规定,内联的写法对于编译器只是一个“建议”,具体是否要内联,完全是编译器自己优化决定;甚至有些本来不内联的方法,编译器反而会内联,你标记了内联的方法,编译器却走了传统的链接。
②模板类的静态成员变量初始化
例如一个面试题:请写一个singleton的实现,最好用模板或者继承让它更有扩展性,最好能线程安全。
答:模板的实现没写出来主要在于模版类的静态变量的初始化不会写,格式是这样的(忽略线程安全)
template<typename T> class Singleton { public: static T* getInstance() { if (!ms_ins) { ms_ins = new T(); } return ms_ins; } private: static T* ms_ins; }; template<typename T> T* Singleton<T>::ms_ins = NULL; // 依然是写在头文件里面
包含了这个头文件之后,调用Singleton<ClassA>::getInstance()的时候,编译器就展开了这个类的定义和方法的内联定义,从而通过编译;猜测这时候并没有静态成员初始化的展开,编译器应该会记录所有实例化这个Singleton模板的类,然后在链接之前对于每个实例化的类,只展开一份该初始化的代码进行编译,从而链接成功。
另外,因为C++11规定了static局部变量是线程安全的,那么完全可以用这个来实现,lazy initialize并且能保证在程序关闭的时候会调用instance的析构。
③特例化或显式定义
这分为两个部分,一个是编写模板类或者函数的时候,一个是实例化模板类或者函数的时候。
首先是编写的时候:
我们在之前的文章中已经见识过以下的模板函数特例化了:
template<typename T> void fn(T) { std::cout<<"template function"<<std::endl; } template<> void fn(const std::string&){ std::cout<<"normal function"<<std::endl; }其实这是一个简化的写法。无论是编写模板类还是模板函数,假设之前是template<typename T> XXX...,要特例化的时候,例如特例化int的版本,应该写成template<> XXX<int>...。
之所以例子中可以简化掉fn后面的<const std::string&>,是因为可以从参数列表推导出来。如果T不是用在参数列表里面,就得写全了,下文紧接着会说明。
另外在编写特例化的模板类时,往往我们会感觉存在大量的共通代码,特例化的只是几个方法,这种时候有两种基于继承来共享代码的方案。一种是共享代码位于同一个父类,子类来特例化成不同版本,不过似乎继承模板父类时,在子类中要访问父类的成员,必须得前缀父类的名字;一种是共享代码位于同一个子类,而特例化不同版本的父类,并且通过CRTP来访问子类中共通代码,这时候我们往往需要http://en.cppreference.com/w/cpp/language/partial_specialization这种部分模板参数特例化的方式来特例化父类(特例化原本的模板参数,保留CRTP模板参数不变)。
然后是实例化的时候:
template <typename T> void func() { cout << "normal version" << endl; } template <> void func<int>() { cout << "explicit speciallized version" << endl; } int main(int , char* []) { func<double>(); func<int>(); return 0; }
这次的func就是上文说的T不在参数列表里,因此在实例化的时候,就像跟实例化类似的,要在func<>中写明要实例化的T的类型。同时我们也看到在编写func的时候,特例化int版本的时候,也需要在func<>中写上int。
当然这种模板函数(template<typename xxx>)比较少见,更常见的是上文①中用template<bool xxx>或者template<size_t xxx>这种基本类型形式的模板,需要显式实例化(正如①中演示的)。
用bool一般是考虑编译器可以优化成两个函数,避免了分支跳转;用size_t一般是用来传入数组的长度之类的编译期信息,例如在此文中提到的_countof的实现,甚至还避免了显式的实例化。