关于C++模板的几点小结
我一直认为模板是C++语言的精髓,为我们提供了无比强大的泛型编程功能。
- 数据类型级别的代码复用,使用模板为不同的数据类型提供相关类和函数实现。这种复用是在编译阶段的复用,编译器对模板进行实例化,自动生成若干相应的类和函数,避免重复编码。
- Java和C#中的对象基本上都是引用类型(引用其实相当于一种特殊的指针,其内存大小等特征是一定的),并且都继承一个object基类,设计的类和算法很容易操纵所有的数据类型。而C++中基本都是值类型,其内存大小等特征不一样,因此需要进行严格类型检验,所以需要模板机制来为不同的数据类型提供相同的接口和实现,模板机制可以看做是一种编译阶段的多态特征,会为每一个数据类型参数生成一个模板类或函数。
- 模板可以看作成一种元数据类型,提供了一种根据参数生成类和函数的模板。
一、模板定义(Template Definition)
- 类模板
//declaration
template<class C> class String /*<class C>也可以写成<typename C>*/
{
public:
String(const C*);
C read(int i) const;
private:
C *base;
};
//implementation
template<class C> String::String(const C*)
{
…
}
template<class C> C String::read(int i) const
{
…
} - 函数模板
template<class T > copy(const std::vector<T> &, std::vector<T> &)
{
…
}
复杂些的定义:
template<class T, class C, C i> …
template<class T, int size> …
根据《The C++ Programming Language》,模板的声明和定义可以分别放在.h和.cpp文件中,但是我发现目前GCC和VC都不支持这种代码安排方式,因此一般采用将其声明和实现同时放在一个hpp或者cpp文件中。
二、 模板的使用和实例化(Template Instantiation)
模板形参和实参(Template Parameter and Argument):
template<class T> class String;
String<char> str;
String<wchar_t> wstr;
其中T为形参,char和wchar_t为实参。
模板实例化就是根据实参,自动生成模板实例的过程。以上例子就会生成两个模板类String<char>, String<wchar_t>, 相当于根据char和wchar_t将String<T>的代码生成了两份。
实例化的一个重要原则是:未被使用的成员函数将不会被实例化,即最后生成的文件中不包含该模板成员函数。这同时也引出了另外一个问题,编译器在对模板进行语义检查的时候,也不会检查该未使用的成员函数,因此只有其被使用的时候才会检查其错误。
模板的实参可以是:常量表达式, 具有外部链接的对象或函数地址, 非重载的函数指针, 模板。
通过typedef以及常量表达式计算结果相同的参数对应的模板实例相同。比如:
template<class T, int i> class Buffer;
Buffer<char, 10> cbuffer; //具有10个字符的缓冲区,将缓冲区大小作为参数,可以在编译时候确定缓冲区大小,而不需要运行时进行new和delete操作。
typedef unsigned char uchar;
Buffer<unsigned char, 20> 与 Buffer<uchar, 20>等效,是同一个类。
Buffer<char, 20-10> 与 Buffer<char, 10>等效。
三、 函数模板的参数推导(deduction)
使用类模板的时候,都需要显示制定模板实参,但是在使用函数模板的时候,不需要显示指定,编译器会根据传入函数的实参数据类型推导出函数模板的实参(Function arguments->Template arguments),从而完成函数的实例化。
当然使用函数模板也可以显示指定模板实参,全部或者一部分,如果函数模板的部分实参可以根据函数调用推导出来,那么只需要指定剩下不能推导出来的模板实参。这里也有一个跟函数缺省参数类似的原则,就是显示指定的实参只能是位于左边,右边未指定的需要推导。如:
template<class T, class C> T implicit_cast(C c) { return c; }
int i = implicit_cast<int>(2.0); //其实就是调用implicit<int, double>(2.0);
template<class T> T* create() { return new T(); }
int k = create<int>(); //参数是返回值,需要显示指定
四、函数模板可以重载
与类和函数的规则相似,类模板不允许重载,即一个类模板的名字不允许出现多次(不过模板特殊化的时候例外Specialization),但是函数模板可以重载,通过给定不同的模板形参组合定义多个函数模板。因此在函数模板进行实例化的时候,就有一个Overload Resolution的过程,即如何根据函数实参推导出最合适的函数模板。
template<class T> T sqrt(T);
template<class T> complex<T> sqrt(complex<T>);
double sqrt(double);
complex<double> z(1.0, 2.0);
sqrt(2); //sqrt<int>(int)
sqrt(2.0);//sqrt(double);
sqrt(z); //sqrt<double>(complex<double>);
首先推导出每一个可能的候选模板函数:
如sqrt(z) 得到两个候选:sqrt<double>(complex<double>)和sqrt<complex<double>>(complex<double>)
优先顺序:
1. 普通函数。如:sqrt(double)优于sqrt<double>(double);
2. 最特殊化的,如:template<class T> complex<T> sqrt(complex<T>);比template<class T> T sqrt(T);更加特殊化,所以前者优先。
解决歧义的方法:
1. 显示指定模板参数
2. 进一步重载,不过以内联函数的方式,避免多级别的函数调用带来的开销。如:
template<class T> T max(T, T);
max(1, 2.0); //产生歧义,max<int>(int, int) or max<double>(double, double)?
解决方法:
1. max<double>(1, 2.0)
2. inline double max(int, double) { return max<double>(1, 2.0); }