只有使用C++语言的少数用户才努力尝试去理解模板的基本原理。然而那些希望去探索更多高级用法的人往往发现自己需要努力去理解模板是如何被语言所支持的,因为缺乏明确的说明。一个很大的问题在于一些工具只实现了C++标准的一个子集。本文将指出它们共同的缺陷并深入剖析如何使用C++模板快速产生可重用和高效的代码。
模板功能应用的典型是通过一系列模板类形成的完整类库,特别是STL和ATL。标准C++库(STL)提供了很多可重用和灵活的类及算法,而ATL则是使用C++进行COM编程的事实标准。要掌握这些及其它的模板库,理解模板是如何工作的这一基础是非常重要的。
函数模板
int main()
{
0 cout<<add(2,3)<<endl;
1 cout<<add(2.1,3)<<endl;
2 cout<<add(2,3.2)<<endl;
3 cout<<add(2.2,3.3)<<endl;
4 cout<<add("hello eone ","world")<<endl;
return 0;
}
也可以通过宏定义#define add(a,b) ((a)+(b))来实现,但是指针(字符串)不能直接相加.对于2,3,4需要进行模板特化.
通过重载函数,我们能够完成多种不同数据类型的相同操作。要实现两个double数值的加法和两个整数类型的加法,我们可以使用一个重载函数:
int add(const int x, const int y)
{
return x + y;
}
double add(const double x, const double y)
{
return x + y;
}
这时,编译器将根据它们的参数正确地解决这一问题。
// 调用int add(const int, const int);
const int z1 = add(3, 2);
// 调用double add(const double, const double);
const double z2 = add(3.0, 2.0);
如果我们需要处理其它类型,我们就不得不提供其他函数重载。对每个不同的数据类型实现一个函数重载,它们都遵循相同的模式,每当我们需要调用针对某一数据类型的函数时,原则上编译器为我们生成相应的代码。而一个模板函数则以如下方式实现:
template<class T>
const T add(const T &t1, const T &t2)
{
return t1 + t2;
}
从概念上来说,编译器通过模板关键字(后面跟随着模板由一或多个模板参数构成的参数列表)来识别模板。当为某一具体类型调用add时,编译器将根据模板定义并用给定的类型替换出现在模板中的参数。在这个例子中,模板参数列表由一个独立的类型模板参数T构成。使用一个模板函数替代函数重载,编译器可以自动为所需的新类型生成代码
我们可以对任何拥有+操作符定义的类型使用add模板。假设一个自定义的String类提供了字符串连接并知道如何将自身写入到std::ostream。因为String与该模板函数兼容,因此我们可以调用它来实现字符串相加:
// 示例字符串
const string strBook("book");
const string strWorm("worm");
// 显示 "bookworm".
cout << add(strBook, strWorm) << endl;
Seeing that we intended to add two String values, the compiler will generate the appropriate add function on our behalf, which would look something like:
const String add(const String &t1, const String &t2)
{
return t1 + t2;
}
显式实例化
调用模板函数时,编译器将先把正确的类型实例化模板。虽然标准允许显式模板实例化,然而并非所有厂商都能够正确地实例它。例如,Visual C++ 6.0 会潜在地调用错误的函数::
template<class T>
int getSize(void) {
return sizeof(T);
}
// 输出4,应该为8
cout << "double: " << getSize<double>() << endl;
// 输出4,正确
cout << "int: " << getSize<int>() << endl;
跨平台代码设计者不希望依赖于显式模板函数实例化,除非有更好的编译器能够对它提供有效的支持
类似于函数模板,模板也可以应用于类。模板可以用于根据普通模式提供一系列类。如果我们需要一套完整的算术运算来补充add函数,我们可以考虑使用一个类。通过模板,它就可以根据类型参数化为一个普通类:
template<class T>
class CCalculator
{
public:
CCalculator(const T &x, const T &y) : m_x(x), m_y(y){ }
~CCalculator(void){ }
const T add(void){ return m_x + m_y; }
const T sub(void){ return m_x - m_y; }
const T mult(void){ return m_x * m_y; }
const T div(void){ return m_x / m_y; }
private:
const T m_x;
const T m_y;
};
要实例化模板类,我们需要提供一个指定类型:
// 创建一个整数计算对象
CCalculator<int> calc(5, 2);
// 结果应该为 10
const int z = calc.mult();
如函数模板一样,编译器为模板不同类型的引用创建不同的类。这为代码重用提供了一个强大的机制,允许单个模板用于任何兼容的数据类型
模板编辑模型
在编写模板类时,函数定义通常与它们的声明一起保存在头文件中,而不使用另外的.cpp文件。否则可能会导致链接错误。这是因为大多数编译器要求模板定义在以头文件为单位的转译单元中有效
这个行为的原因是模板只是一个模式,同样它们不直接产生代码(直到编译器遇到一个应用实例)。如果我们创建一个CCalculator<int> 实例并调用其中的某个类方法,编译器将需要找到函数定义。如果头文件中包含了该定义则一切都会是正确的。但是如果定义存在于.cpp文件中,编译器不能期望在此时找到匹配的模式并利用其产生所需的代码。然而,C++标准提供了一个机制对编译器进行辅助。Export关键字可以使通知编译器我们提供了一个分离的编辑模板:
// MyTemplateFunction.h
template<class T>
void myTemplateFunction(const T &t1);
// MyTemplateFunction.cpp
export template <class T>
void myTemplateFunction(const T &t1)
{
...
}
现在,大多数编译器要求模板定义通过头文件包含被显式添加到转译单元,虽然标准期望能够独立定义于.cpp文件中。这两个不同的模板编辑模型即为包含模型和分离模型。在编写时,我所知的支持分离模型的唯一的编译器是Comeau C++。Comeau 的使用了不少方法来实现对标准中所定义的export关键字用法,但目前也还只是beta版本而已
typename关键字
另一个与模板相关的关键字是typename关键字,它有两种用法。参数下面的模板类:
template<class T>
void myFunction(void)
{
// 这里可能会有问题
T::x1 * x2;
}
初次讲到的时候可能会以为myFunction声明了一个T::x1类型的指针变量x2。然而,这个函数也能够表示类T的成员变量x1与全局变量x2的二进制乘法操作。使用typename关键字可以告诉编译器某个未知标识符是一个类型:
// T:x1 是一个类型,而x2是一个指针
typename T:x1* x2;
第二种用法是在指定模板参数时替换class关键字:
// 下面的两种方法是等效的...
template<class T1, class T2>;
template<typename T1, typename T2>;
标准允许以上任意一种方法,它们都是合法的。
成员函数模板
除了全局模板函数外,语言也支持成员模板函数:一个类可以拥有带有模板参数列表的成员函数。参考下面的非模板类,它的构造函数被模板化:
class CTypeSize
{
public:
template<class T>
CTypeSize(const T &t1) :
m_nSize(sizeof(t1))
{
}
~CTypeSize(void){ };
int getSize(void) const{ return m_nSize; }
private:
const int m_nSize;
};
当模板成员函数被调用时,编译器使用模板模式为给定类型生成代码。这种情况下,我们能够使用任意类型变量创建一个CtypeSize实例:
// 显示12
CTypeSize t1("Hello World");
cout << t1.getSize() << endl;
// 在VC++6/Win32中显示8
CTypeSize t2(7.0);
cout << t2.getSize() << endl;
某些时候,成员模板是实现拷贝构造函数最有效的方法,参考一个只有一个交易会的简单容器类:
template<class T>
class CSingle
{
public:
CSingle(const T &t1) : m_Value(t1) { }
~CSingle(void){ }
T m_Value;
};
它导致下面的问题:
// 创建一个整数容器
CSingle<int> x(7);
// 这里需要一个拷贝构造...
CSingle<double> y(x);
通过使用成员模板,拷贝构造就能够轻松完成:
template<class S>
CSingle(const CSingle<S> &s1) : m_Value(s1.m_Value) { }
当编辑器能够将类型T的实例转换为类型S时,这是可行的;这是因为double可以从一个整数构造。
总结
模板是C++的一个强大特征,它允许从数据类型中抽象出算法。本文介绍了模板定义和实例化的基础,包括函数、类及成员模板的区别。