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

阅读更多
个人分类: stl
想对作者说点什么? 我来说一句

sql语法的各种妙用

2009年03月02日 79KB 下载

google的妙用

2008年01月04日 5KB 下载

记事本的一些妙用

2011年10月18日 17KB 下载

mssql server语句妙用指导

2010年05月08日 403KB 下载

shift键的十一个妙用

2010年03月20日 929B 下载

隐藏域的妙用.doc

2011年06月01日 26KB 下载

函数指针数组的妙用

2013年03月14日 146KB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭