相信大家对模板并不陌生,模板的基本概念我想就不用多说了。大多数人包括我自己对模板的理解就是“T容器”。
请看下面的代码:
template<int m1, int l1, int t1, int m2, int l2, int t2>
Physical<m1+m2, l1+l2, t1+t2> operator *( Physical<m1, l1, t1 > lhs, Physical<m2, l2, t2> rhs)
{
return Physical<m1+m2, l1+l2, t1+t2>::unit * lhs.value() * rhs.value();
}
头一次见到template的这种用法时,我确实有点目眩。看来我们对模板的认识只是皮毛而已 J
在当今的程序设计中模板已经显露出它的价值来了,比如STL全部都是以模板架构的。自VS2003(VC7)以来的编译器也对模板提供了更多的支持。
我们需要对模板有更进一步的认识,下面就让我们进入模板的世界。
§1 模板的特性
首先来看一个例子:
template<int n>
int Func1() { return n; }
int Func2(int n) { return n; }
你能看出这两个函数的区别在哪里吗?
它们的区别就在于,Func1的参数是编译期指定,如Func1<0>() ;而Func2的参数则是运行期指定。
这正体现了模板的特性或者说是它的技术核心,就是编译期的动态机制,这种机制使程序在运行期具有更大的效率优势。
到此你是不是对本文开头那段代码有了更深入的理解了呢?
模板的另一个特性是,如果一个模板没有被特化,那么编译器根本不会去理会它,也就是说模板内的代码被隐藏了。
§2 函数模板与类模板
这是一个函数模板:template<class T> Func(T param) {}
函数模板的模板参数是隐式的,编译器会自动根据传入值的类型来确定模板参数的类型。因此函数模板的模板参数不能有默认值。
这是一个类模板:template<class T> class MyClass {};
类模板的模板参数是显式的,使用一个模板类时必须指明其模板参数,因此类模板的模板参数可以有默认值。
我们还可以做更多的事情,比如MyClass可以派生自T(XTP界面库就是这么做的),在MyClass内部可以使用关于T的enum、typedef等等。这些将在下文一一谈到。
(我在这里提出一个建议,创建一个类模板,请记得第一件事就是对模板参数进行typedef定义。)
§3 模板的部分特化与应用
模板最有价值的地方就是它的部分特化,也是应用最广泛的特性。
所谓“部分特化”也就是说,一个模板有多个参数,但我们只对其中一部分参数进行特化,或者是只针对常量模板参数的某种情况进行特化。
?编译期ASSERT
这是对bool型模板参数部分特化的一个例子。最简单的实现:
template<bool> struct CompileTimeAssert;
template<> struct CompileTimeAssert<true> {};
当我们将一个表达式作为模板参数,而这个表达式的值为false时,编译器就找不到合适的实现,便会报错了。
我们可以将它扩展一下:
template<bool> struct CompileTimeAssert { CompileTimeAssert(…) ;}; // 这里使用了C++支持的任意参数表
template<> struct CompileTimeAssert<false> {};
#define COMPILE_CHECK(expr, msg) /
{/
class ERROR_##msg {}; /
(void) CompileTimeAssert<((expr)!=0)>(ERROR_##msg()); /
}
// 这里不直接传入expr是为了获得更大的适应性,因为expr有可能是无法隐式转换为bool型的(比如指针),笔者曾参与的
// 一个项目在由VC6平台升级到VS2003平台时,由于后者ASSERT宏实现的变化,就遇到了这一问题。
当然,事实是编译期可用的表达式或函数(如sizeof、__alignof)数量上并不太多,但是这种方式是有着积极意义的。
?编译期分派与类型选择
常量映射为类型
请看这样一个模板:template<int v> struct Int2Type{ enum{ value = v }; };
模板参数的不同数值,就会产生不同类型的Int2Type。即Int2Type<0>不同于Int2Type<1>,以此类推。
我们可以利用这个模板实现编译期分派。看这样一个例子:
template<class T, bool bPolymorphic>
class MyClass
{
……
void Func(T* pObj)
{
if (bPolymorphic)
{
T* pNewObj = pObj->Clone(); // 多态类型
……
}
else
{
T* pNewObj = new T(*pObj); // 非多态类型
……
}
}
};
显然,编译器不会让你侥幸成功。而使用了Int2Type就不一样了:
template<class T, bool bPolymorphic>
class MyClass
{
……
void Func(T* pObj)
{
Func(pObj, Int2Type< bPolymorphic >);
}
private:
void Func(T* pObj, Int2Type<true>)
{
T* pNewObj = pObj->Clone(); // 多态类型
……
}
void Func(T* pObj, Int2Type<false>)
{
T* pNewObj = new T(*pObj); // 非多态类型
……
}
};
类型映射为类型
也就是这样一个模板:template<typename T> struct Type2Type{ typedef T _MyType; };
其用法跟Int2Type类似,但我们可以利用它进而实现类型选择。
举一个例子,我们使用vector来存储数据,如果数据是非多态类型,存储其实体比较有效率;如果数据是多态类型,显然只能存储其指针。使用Type2Type实现如下:
template<bool flag, typename T, typename U> struct TypeSelector { typedef T _Result; };
template<typename T, typename U> struct TypeSelector<false, T, U> { typedef U _ Result; };
template<typename T, bool bPolymorphic>
class MyClass
{
……
typedef TypeSelector< bPolymorphic, T*, T>::_Result _MyType;
}
模板的部分特化与枚举的结合使用
这是一种具有极大的可扩展性的方式。比如我们要检查两个类型是否是相同的类型,可以这样实现:
template<class T, class U> struct CompileCheck { enum{ same_type = false }; };
template<class T> struct CompileCheck<T, T> { enum{ same_type = true }; };
使用这种方式还可以检查两个类型是否可以转换、是否可以双向转换、是否具有派生关系,可以判断一个类型是否是基本类型、属于那种基本类型、是否是指针,等等。当然这些都是在编译期进行的。请大家参阅附注中说明的原著,这里就不详述了。
一个另类的技巧
代码如下:
template<class T> struct UnConst { typedef T _Result; };
template<> struct UnConst <const T> { typedef T _ Result; };
template<class T>
class MyClass
{
……
typedef UnConst <T>::_Result _MyType;
}
看,我们巧妙的去掉了const修饰符。用同样的方法我们还可以去掉volatile修饰符。