template的妙用

template的妙用

游戏引擎开发网 龚敏敏

本文发表于《开发高手》2004年第2期 如有转载请声明出处

也许在很多C++程序员的心目中,template只是一个用于泛型的关键字而已。其实,它的作用远不止此。下面就让我们看看它的不可思议。

FOURCC的生成

FOURCC全称Four-Character Codes,是在编程中非常常用的东西,一般用作标示符。比如wav、avi等RIFF文件的标签头标示,Quake 3的模型文件.md3中也大量存在等于“IDP3”的FOURCC。它是一个32位的标示符,其实就是

typedef unsigned long FOURCC

FOURCC是由四个字符拼接而成的。生成FOURCC的传统方法是:

// 来自mmsystem.h
#define MAKEFOURCC(ch0, ch1, ch2, ch3) /
((DWORD)(BYTE)(ch0) | ((DWORD)(BYTE)(ch1) << 8) | /
((DWORD)(BYTE)(ch2) << 16) | ((DWORD)(BYTE)(ch3) << 24 ))

这种方法很简单直观,而且以下代码:

switch (val)
{
case MAKEFOURCC('f', 'm', 't', ' '):
...
break;


case MAKEFOURCC('d', 'a', 't', 'a'):
...
break;


...
}

能顺利通过编译,因为宏能在编译期生成常量,符合case的条件。

如果你是一个纯粹的C++用户,那么你一定会对带参数的宏非常反感,无论是《The C++ Programming Language》、《Effective C++》还是其他好的C++书籍,都会劝导你不要使用带参数的宏,还会列举出很多很多例子,来说明宏的坏处(如果你看的入门书没有这么说,就扔掉它)。但 是这里能用inline函数完全代替宏吗?很遗憾,不行。在赋值等情况下还可以,但遇到用作case的条件时就傻了。因为即使是inline,它的返回值 仍然不能在编译期得出,不是常量,不能用于case等需要真正的常量的情况。更要说明的是,“inline”关键字只是一个建议,而不是一个命令。它只能建议编译器:“如果允许的话,就请把它内联吧。”而编译器完全有权不去内联一个函数。所以,这个方案不成立。

C++的模板机制给程序员带来的无限的空间,它不光能让类型作为参数,还能将常量作为参数(这一点常常被人遗忘),而且这一切都是编译期决定的!这是我们用它来生成FOURCC的第一个基础。

于是,你就迫不及待的利用模板来改写上面的函数:

template <char ch0, char ch1, char ch2, char ch3>
inline FOURCC MakeFOURCC()
{
return (ch0 << 0) + (ch1 << 8) + (ch2 << 16) + (ch3 << 24);
}

可是错误照旧。虽然这次可以保证返回值能在编译期计算出来,但可惜的是那个return语句却要等到运行才能运行(也有可能在优化阶段就能消除这个语句,但优化是在编译之后的)。

别急,还有第二个基础才可以。那是什么?是一个从C语言继承来的东西——enum。很多朋友认为,它不是很重要,因为很多情况下可以用别的方法来取代它,比如const。但是它有一个经常被人忽略的特性,而且这个特性非常重要,那就是——它的值必须在编译期就得出,即它是个编译期常量!这不是正符合我们的需要吗?请看下面的模板:

template <char ch0, char ch1, char ch2, char ch3>
struct MakeFOURCC
{
enum { value = (ch0 << 0) + (ch1 << 8) + (ch2 << 16) + (ch3 << 24) };
};

核心还是和上面一样,通过表达式(ch0 << 0) + (ch1 << 8) + (ch2 << 16) + (ch3 << 24)计算FOURCC(那当然是一样的)。但是计算的时机从运行期或者优化期移到了编译期。编译器在编译时,通过模板带入的char常量计算出表达式的 值,并把它保存在枚举值value里。看看现在的代码:

const FOURCC fccFMT  = MakeFOURCC<'f', 'm', 't', ' '>::value;
const FOURCC fccDATA = MakeFOURCC<'d', 'a', 't', 'a'>::value;
...

switch (val)
{
case fccFMT:
...
break;

case fccDATA:
...
break;

...
}

成功了,MakeFOURCC模板顺利地完成了任务。FOURCC的模板生成法既让我们抛弃了那个不安全的宏,又让我们看到了inline的局限性,还让我们重新认识了enum的一些特性。其它许多类似的问题也能通过template + enum来解决。

上面那个例子知识简单的应用。下面,你将看到一些模板的高级技巧。由于用了特化,所以必须在vc7等支持特化的编译器上才能编译通过。

编译器断言

大家一定都知道assert,它可以在debug版运行中断言一个值的真假,如果为假,就中断程序。在这里,我要演示一个非常简单的模板,它可以在编译期作出断言,如果值为假,就直接中断编译。

template <bool T>
class StaticAssert;

template <>
class StaticAssert<true>
{
};

那么如果是StaticAssert<true>,就和没有这句一样,但如果是StaticAssert<false>, 编译就出错了。这是因为StaticAssert只有声明没有定义,StaticAssert<true>有定义,但 StaticAssert<false>则没有,所以没有会因为没有默认构造函数而编译失败。这样就可以在编译期对一些常量做出断言了。

判断两个类型是否相同

有时候,我们需要知道两个类型是否相同。也就是说,如果

typedef A B;

我们就说类型A和类型B相同。请看下面的模板类:

template <typename T, typename U>
struct IsSameType
{
private:
template <typename>
struct In
{
enum { value = false };
};

template <>
struct In<T>
{
enum { value = true };
};

public:
enum { value = In<U>::value };
};

由于目前还有很多编译器不支持偏特化,所以在这里通过内嵌类把偏特化用特化来实现。总的来说这个模板类还是比较简单的。如果T和U相等,那么value == true,否则value == false。这是因为在

enum { value = In<U>::value }

这行中,U的类型被带入了In模板,如果U等于T,那么编译器将处理的是In<T>这个特化,所以In::value == true,否则将处理的是上面那个In,所以In::value == false。接着,编译器把结果传给IsSameType::value,于是便可以通过IsSameType<T, U>::value来判断T是否和U相等了。

如果编译器支持偏特化,那么程序将简化为:

template <typename T, typename U>
struct IsSameType
{
enum { value = false };
};

template <typename T>
struct IsSameType<T, T>
{
enum { value = true };
};

在两个类型中选择一个

你有没有想过,可以在编译期从两个类型中选择一个,就像if一样?不同的是不是两段程序,而是两个类型。让我们来看看吧:

template <bool condition, typename T, typename U>
struct If
{
private:
template <bool>
struct In
{
typedef T Result;
};

template <>
struct In<false>
{
typedef U Result;
};

public:
typedef typename In<condition>::Result Result;
};

当condition == true时,If::Result就是T,否则就是U。是不是很像if?同样,在支持偏特化的编译器上可以这么写:

template <bool condition, typename T, typename U>
struct If
{
typedef T Result;
};

template <typename T, typename U>
struct If<false, T, U>
{
typedef U Result;
};

判断两个类型之间是否能进行隐式转化,是否存在继承关系

这个例子是本文所演示的最难懂的例子,请擦亮显示器,不要错过什么细节!

在C++中隐式转化很常见,比如char可以隐式转化为int,派生类指针可以隐式转化给基类指针。在实现模板函数和模板类的过程中,我们常常遇到 一个问题:面对两个陌生的类型T和U,怎么知道U是否能由T隐式转换呢?如果能在编译期就发现它,也就不必等到运行期用dynamic_cast这个又大 又慢的家伙了。

有个方法可以解决问题,而且只依赖sizeof。sizeof很神奇,它可以用在任何表达式上,不论有多复杂,sizeof会在编译期直接返回大小。这意味着sizeof可以检测出重载、模板实现、转换,或其他任何可能发生在C++表达式身上的机制。让我们看看结果:

template <class T, class U>
class Conversion
{
typedef char Small;
struct Big { char dummy[2]; };
static Big Test(...);
static Small Test(U);
static T MakeT();

public:
enum { exists = sizeof(typename Small) == sizeof(Test(MakeT())) };
};

很神奇是吧,我会一步一步揭开谜底的。

首先是Small和Big。Small的大小是sizeof(char),而Big的大小是sizeof(char[2]),显然Big比 Small大。接下来的两个Test函数,返回值类型分别是Big和Small。注意它们只有声明,不需要实现。第一个Test的参数是...,也就是说 可以接受任何参数,而第二个是U,它只能接受可以隐式转化为U的类型。这是本问题的关键所在。如果T可以隐式转换为U,则MakeT返回T后Test (MakeT())将指向的是Small Test(U),否则将是Big Test(...)。因为它们的返回值类型不同,所以能用sizeof探测出来,并且把最终结果传给exists,完成了任务。

总结

C++中的template是整个C++最深奥的部分。它独特的编译器评估能制造出不少“神迹”。它可以带你进入奇妙的“编译期编程”的世界,把原 来属于外星球的技术搬到了地球上:)。利用“编译期编程”所创造的奇迹,能极大的提高开发效率和运行效率,降低代码重复量。学好template对理解 STL和Boost也有很大的帮助。

当然,要顺利地利用本文中的那些代码,一个好的编译器是不可缺少的。VC6的编译器是不行的,因为不支持特化。至少要VS.NET中的C++编译 器,最好用VS.NET 2003(虽然VS.NET 2003对偏特化的支持还是很不好)。如果你用的是gcc,那么就选择3.0以上的版本。

参考资料

《Modern C++ Design》Andrei Alexandrescu

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值