模板提供了一个用途广泛且强大的能力,即在编译时生成代码。它们对生成大量形式相似但只类型不同的代码尤其有用。
模板一般使用会在头文件声明和定义,声明和定义放在一起,不够好。真正使用采取以下两种方式。
一、隐式实例化
如果想允许用户用他们自己的类型去实例化类模板,那么就需要使用隐式模板实例化。例如,假设你提供一个智能指针类模板smart_pointer<T>,那么你就不能预知用户会用什么类型去实例化它。但是,编译器需要在使用它时访问到此模板定义。这意味若必须在头文件中暴露模板定义。这是隐式实例化方法在程序设计健壮性方面的最大缺点。不过在这种情况下,即使不能隐藏实现细节,也应当努力去隔离它们。
假设你需要在头文件中包含模板定义,轻松而诱人的做法是在类定义中直接内联模板定义。这是一种我已经归类为拙劣设计的做法,而这在模板中也不例外。我的建议是,将所有的模板实现细节包含在单独的实现头文件中,该头文件被主要公有头文件包含。以Stack类模板为例,可以提供如下主要公有头文件:
#ifndef STACK_H #define STACK_H #include<vector> template<typename T> class Stack { public: //Stack(); void Push(T val); T Pop(); bool IsEmpty()const; private: std::vector<T> mStack; }; //使用单独的头文件隔离所有实现细节 #include "stack_priv.h" #endif // STACK_H
#ifndef STACK_PRIV_H #define STACK_PRIV_H template<typename T> void Stack<T>::Push(T val) { mStack.push_back(val); } template<typename T> T Stack<T>::Pop() { if(IsEmpty()) { return T(); } T val=mStack.back(); mStack.pop_back(); return val; } template<typename T> bool Stack<T>::IsEmpty()const { return mStack.empty(); } #endif // STACK_PRIV_H
应用:
#include "stack.h" void MainWindow::on_pushButton_clicked() { Stack<int> intStack; qDebug()<<"Empty:"<<intStack.IsEmpty(); intStack.Push(10); qDebug()<<"Empty:"<<intStack.IsEmpty(); int val=intStack.Pop(); qDebug()<<"Popped off:"<<val; qDebug()<<"Empty:"<<intStack.IsEmpty(); }
很多基于模板的高质量API使用了这种技巧,比如Boost的各种头文件。它具有保持主要公有头文件整洁的好处,其方法是在实现细节的同时,将必须暴露的内部细节隔离到单独的头文件中,这个头文件被明确指定为包含私有细节。(同样的技巧可以用于将有意内联的函数细节与其声明相隔离。)
把模板定义包含在头文件中的技巧称为包含模型。值得注意的是,有一个可供选择的方法称作分离模型,它允许在.h文件中以export关键字开头来声明类模板,这样模板函数的实现就可以出现在.cpp文件中了。站在API设计的角度,这是一种更可取的模式,因为它允许我们从公有头文件中移除所有实现细节。大部分编译器都不支持这个功能。(备注:C++11标准中已经废除此特性,export关键字保留待用。)
二、显式实列化
如果只想为API提供一部分预设的模板特化集合,而不允许用户创建其他模板特化,那么可以选择完全隐藏私有代码。比如,如果已经创建了3D向量类模板Vector3D<T>,你也许只想提供int、short、float及double这些类型的模板特化,并且不想让使用者创建其他特化。
这种情况下,可以把模板定义放进.cpp文件,并使用显式模板实例化那些你需要作为API的一部分导出的特化。template关键字可以用来创建显式实例化。比如上面给出的Stack模板为例,可以使用下而的语句为int类型创建显式实例化: template class Stack<int>;
这会促使编译器在代码的这个位置为特化int类型生成代码。这样一来,编译器以后不会尝试在代码的其他位置隐式实例化这种特化,所以使用显式实例化可以减少构建时间。
#ifndef STACK_H #define STACK_H #include<vector> template<typename T> class Stack { public: Stack(); void Push(T val); T Pop(); bool IsEmpty()const; private: std::vector<T> mStack; }; #endif // STACK_H
#include "stack.h" template<typename T> Stack<T>::Stack() { } template<typename T> void Stack<T>::Push(T val) { mStack.push_back(val); } template<typename T> T Stack<T>::Pop() { if(IsEmpty()) { return T(); } T val=mStack.back(); mStack.pop_back(); return val; } template<typename T> bool Stack<T>::IsEmpty()const { return mStack.empty(); } // explicit template instantiation //显式模板实例化 template class Stack<int>; template class Stack<double>; template class Stack<std::string>;
应用
#include "stack.h" typedef Stack<double> DoubleStack; void MainWindow::on_pushButton_clicked() { Stack<int> intStack; qDebug()<<"Empty:"<<intStack.IsEmpty(); intStack.Push(10); qDebug()<<"Empty:"<<intStack.IsEmpty(); int val=intStack.Pop(); qDebug()<<"Popped off:"<<val; qDebug()<<"Empty:"<<intStack.IsEmpty(); DoubleStack doubleStack; qDebug()<<"Empty:"<<doubleStack.IsEmpty(); doubleStack.Push(9.91); qDebug()<<"Empty:"<<intStack.IsEmpty(); int dval=doubleStack.Pop(); qDebug()<<"Popped off:"<<dval; qDebug()<<"Empty:"<<doubleStack.IsEmpty(); }
//显式模板实例化
template class Stack<int>;
template class Stack<double>;
template class Stack<std::string>;
这里重要的是最后三行,它们为int、double及std::string类型创建Stack类模板的显式实例化。用户不能创建其他特化(编译器也不能为用户创建隐式实例化),因为
实现细节隐藏在.cpp文件中。总之,实现细节确实成功地隐藏在.cpp文件中了。
为了向用户指出可以使用哪些模板特化(即那些已经显式实例化的模板特化),可以在公有头文件的末尾添加一些类型定义(typedef),比如:
typedef Stack<int> IntStack;
typedef Stack<double> DoubleStack;
typedef Stack<std::string> StringStack;
值得注意的是,采用这种模板风格不仅使你的代码构建由于移除了隐式实例化而变得更快,而且通过从头文件中移除模板定义,降低了#include与API的耦合度,同时减少了客户程序因为包含API头文件而必须每次编译的额外代码.
如果只需要一些确定的特化集合,那么尽量选择显式模板实例化。这样做刻意隐藏私有细节并降低构建时间。