C++ template metaprogramming 几年前写的,移过来

    meta,其意思是在…中,在…后,中文把它叫做“元”。例如,描述数据的数据,称之为元数据(meta data),而metaprogramming可以理解为运用程序语言自身的某种特性,对程序语句进行编程,更简洁些,就是对编程的编程――元编程。

    在C++领域,元编程有两种手段,一种是使用宏,一种是使用模板,template metaprogramming,也就是模板元编程,本文将以我使用C++模板元编程的经验简单举例说明。

    C++模板元编程(以下简称TMP)的由来有一段故事:当年C++为了向泛型编程推进,引入了模板,之后在一次世界性的学术研讨会议上,有牛人演示了一份程序代码。这份程序代码特别之处在于,它利用模板特化和偏特化机制,在编译的时输出质数列,这份奇特的代码立刻使人们意识到,C++编译器的能力已超出了它原本的设计范围,就是这样,C++TMP能力被发现了。

    TMP主要利用模板特化和偏特化机制,在编译时获取类型信息,并依照类型信息进行代码分发(dispatch),简化代码编写,提高代码可维护性以及省略不必要的运行时代码路径选择。这里提到了“编译时获取类型信息”,很多人可能觉得很奇怪:一般情况下,我们更感兴趣的是“运行时获取类型信息”,而在编译时,编译器是肯定知道类型信息的,根本不需要获取!没错,类型信息编译器是知道的,但我们不知道,更准确是我们的代码在编译时不知道类型信息,TMP就是获取类型信息的手段。

    以下将是一个最为经典的例子,它存在于现代任何一个STL源代码内,不过在讲述例子之前,有必要了解一下迭代器的concept:迭代器concept分为inputoutputforwardbidirectionalrandomaccess,任何一个迭代器都有concept属性,例如list的迭代器的concept就是bidirectional,因为你只能在list的迭代器上是用++—运算;istream_iteratorconcept就是input;同理vector的迭代器当然是randomaccess了,因为vector可以使用[]运算。

    迭代器的concept可以通过访问迭代器属性iterator_category获得,不过值得注意的是,iterator_category并不是#defineconst int或者enum这样的值,而是一个没有任何内容struct定义,为什么是一个类型定义而不是一个值?回到TMP上――TMP主要用于在编译时获取类型信息,并依照类型信息进行代码分发。iterator_category之所以是一个类型定义,就是为TMP准备――类型信息!这里有个小插曲,当迭代器是一个class的话,它当然可以有iterator_category这个属性,但是像char*这样的迭代器,如何获取它的iterator_category属性,更高要求地,如何以一致的方式访问任何一个迭代器的iterator_category,不管这个迭代器是native类型的指针还是一个class?软件工程里的一句金句再次被验证,STL引入了一层iterator_traits,泛化iterator_category的访问方式,它的实现方式很简单,但思维却是神,有兴趣的可以自行查阅STL源代码,这里不多说了。

    废话少说,立刻开始TMP之旅,这个经典例子就是STL内的advance函数。advance函数作用是在迭代器当前位置上向前或向后移动若干个位置。看似简单的功能实现却不简单!为什么,仔细想想,每个迭代器都能做――运算吗,至少forward迭代器不能;要移动的话是使用++运算符一步一步地移动还是使用+运算符一次移动到目的位置?实际上只有randomaccess可以使用+运算符。这些限制跟迭代器的concept相关,而迭代器的concept表现为迭代器的iterator_category,所以,advance需要通过判断iterator_category进行代码分发。看到这里,我们脑袋里可能出现了类似的代码片段:

if iterator_category == randomaccess

iterator += x

else if iterator_category == bidirectional

for i = 0 to x

++iterator

else if……

我们太过习惯于使用一个变量来选择代码的执行路径,这样的坏处每个人都清楚,但我还是要说一下,首先,这样做会带来维护问题,但这个问题其实不太要紧,而最大的问题是在于,这段代码是一段运行时路径选择代码,但它的作用只是用来识别一个在编译时就能获取得到的类型信息!!!作为STL,当然不会这样做!迭代器的concept在声明时就已经确定,在运行时刻是不会改变的!这也是为什么iterator_category不是一个#define或者const int的原因!来看STL的做法:

template<class _InIt,

class _Diff> inline

void __CLRCALL_OR_CDECL advance(_InIt& _Where, _Diff _Off)

{ // increment iterator by offset, arbitrary iterators

_Advance(_Where, _Off, _Iter_cat(_Where));

}

template<class _Iter> inline

typename iterator_traits<_Iter>::iterator_category

__CLRCALL_OR_CDECL _Iter_cat(const _Iter&)

{ // return category from iterator argument

typename iterator_traits<_Iter>::iterator_category _Cat;

return (_Cat);

}

template<class _InIt,

class _Diff> inline

void __CLRCALL_OR_CDECL _Advance(_InIt& _Where, _Diff _Off, input_iterator_tag)

{ // increment iterator by offset, input iterators

 #if _HAS_ITERATOR_DEBUGGING

// if (_Off < 0)

// _DEBUG_ERROR("negative offset in advance");

 #endif /* _HAS_ITERATOR_DEBUGGING */

for (; 0 < _Off; --_Off)

++_Where;

}

template<class _FI,

class _Diff> inline

void __CLRCALL_OR_CDECL _Advance(_FI& _Where, _Diff _Off, forward_iterator_tag)

{ // increment iterator by offset, forward iterators

 #if _HAS_ITERATOR_DEBUGGING

// if (_Off < 0)

// _DEBUG_ERROR("negative offset in advance");

 #endif /* _HAS_ITERATOR_DEBUGGING */

for (; 0 < _Off; --_Off)

++_Where;

}

#pragma warning(push)

#pragma warning(disable: 6295)

template<class _BI,

class _Diff> inline

void __CLRCALL_OR_CDECL _Advance(_BI& _Where, _Diff _Off, bidirectional_iterator_tag)

{ // increment iterator by offset, bidirectional iterators

for (; 0 < _Off; --_Off)

++_Where;

for (; _Off < 0; ++_Off)

--_Where;

}

#pragma warning(pop)

template<class _RI,

class _Diff> inline

void __CLRCALL_OR_CDECL _Advance(_RI& _Where, _Diff _Off, random_access_iterator_tag)

{ // increment iterator by offset, random-access iterators

_Where += _Off;

}

观察advance_Advance最后一个参数_Iter_cat(_Where)_Iter_cat是一个辅助函数,它根据迭代器的iterator_category返回这个iterator_category的实例,而_Advance有多个版本的重载,分别对应inputforwardbidirectionalrandomaccess这些iterator_category,因为有这些重载函数,advance就能通过迭代器自身iterator_category类型调用到相对应的_Advance实现,更有趣的是_Advance的重载参数根本就没有形参,它仅仅是为了捕捉类型信息。好,我们把这些连起来:advance调用_Advance_Iter_cat析出迭代器的类型信息,而_Advance的最后一个参数捕捉类型信息,完成编译时代码分发!!!

    当我首次接触这部分代码的时候,我惊呆了,这样的手法,这样的思维是我无法想象的,我立刻意识到我所谓掌握C++的知识是少之又少,思维方式也是少之又少!这部分代码是我印象最深刻的代码之一,而印象更深刻的是代码背后的思维方式!它把我带到TMP领域,我首次接触它是在2006年。

    再看以下这个例子,它是从VC2005标准库源代码内拷贝出来的代码片段,是对strcpy_s的安全封装。基本上,VC2005把所有带_s后缀的标准库函数都用类似的宏封了一层。strcpy_s用得不少了,比较烦人的是该函数的第二个参数――缓冲区长度。如果缓冲区是动态分配的,当然要给出长度了,但很多时候,缓冲区是一个数组,长度在声明的时候就知道了,还要不厌其烦地sizeof,多此一举啊。可能有人会说,使用宏来做sizeof啊,注意了,宏是没有类型检查的,如果你给一个指针变量给它,它也呆呆在指针变量上施加sizeof操作,这不死翘翘啦。

    来看微软是怎么做的:#define是宏定义,展开后会成为strcpy_s这个模板函数,这个模板函数才是重点!观察模板参数及函数参数,这个模板函数能捕捉到数组的类型信息,特别是数组的长度!另外注意的是,模板函数的参数是数组引用,为什么? 

#define __DEFINE_CPP_OVERLOAD_SECURE_FUNC_0_1(_ReturnType, _FuncName, _DstType, _Dst, _TType1, _TArg1) \

    extern "C++" \

    { \

    template <size_t _Size> \

    inline \

    _ReturnType __CRTDECL _FuncName(_DstType (&_Dst)[_Size], _TType1 _TArg1) \

    { \

        return _FuncName(_Dst, _Size, _TArg1); \

    } \

}

errno_t strcpy_s<size_t Size>(char (&_Dest)[_Size],const char* _Source)

{

    return strcpy_s(_Dest,_Size,_Source);

}

因为,如果参数是数组,会退化成指针,数组的类型信息就被阉割了。正是由于参数是数组引用,使用数组调用strcpy_s时就可以这样调:strcpy_s(destArray,source);如果你是指针,你这样调strcpy_s(pointer,source),编译器会报错,因为指针跟数组引用之间不能隐式转换,你还是要乖乖地使用原有的strcpy_s,完全不用担心会调用到数组引用的那个版本。

    通过上述两个例子,相信大家对TMP都有所认识了,TMP在编译时获取类型信息是一强大的功能,我们可以利用这些信息简化代码编写,提高代码可维护性。 

    TMP通过模板特化和偏特化,还可以用于编译时断言,想象这样一个情况,一段数据缓冲区用作接收数据,而这段数据可用一个结构体解析:

#define BUFFLEN = 100

char buffer[BUFFLEN];

struct PackHead

{

char id[8];

char revs[8];

char data[20];

char endMark[4];

};

Buffer可能是整段数据,而PackHead是数据头的结构,系统在这时是可以正常运行的,但改造后,PackHead结构发生了改变:

struct PackHead

{

char id[8];

char revs[8];

char data[200];

char endMark[4];

};

BUFFLEN长度忘记修改了,这时如果使用新的PackHead解析缓冲区,就会出现意想不到的后果。借助编译时断言,可以及早发现这一错误。

    编译时断言根据编译时类型信息,执行断言,如果断言失败,则直接报错,停止编译。例如上述情况可以加上这句:BOOST_STATIC_ASSERT(sizeof(buffer)>sizeof(PackHead))。当buffer空间小于PackHead时,编译报错。

以下是BOOST_STATIC_ASSERT的实现代码,来自Boost程序库。

#define BOOST_STATIC_ASSERT( B ) \

   typedef ::boost::static_assert_test<\

      sizeof(::boost::STATIC_ASSERTION_FAILURE< BOOST_STATIC_ASSERT_BOOL_CAST ( B ) >)>\

         BOOST_JOIN(boost_static_assert_typedef_, __COUNTER__)

template<int x> struct static_assert_test{};

template <bool x> struct STATIC_ASSERTION_FAILURE;

template <> struct STATIC_ASSERTION_FAILURE<true> { enum { value = 1 }; };

BOOST_STATIC_ASSERT_BOOL_CAST(x) (bool)(x)

首先看BOOST_STATIC_ASSERT_BOOL_CAST,这个宏只是把常量表达式转换成布尔类型;再看STATIC_ASSERTION_FAILURE模板类,注意它的泛化是没有定义的,只有声明,而它特化了一个参数为true的类。OK,我们把宏展开,把无关重要的部分去掉,得到如下核心代码:sizeof(STATIC_ASSERTION_FAILURE<(bool)( sizeof(buffer)>sizeof(PackHead))>)当表达式sizeof(buffer)>sizeof(PackHead)的值为true,模板STATIC_ASSERTION_FAILURE是可以实体化的,但如果是false,就不能了,因为没有定义,而sizeof一个没有定义的类,编译器是肯定报错。::boost::static_assert_test模板起到催化剂的作用,使得sizeof操作被编译器认为是有必要处理的,不会优化掉。

    经过上述的介绍,大家对TMP可能已经比较熟悉了,但事实并非如此,上述例子只是TMP的皮毛,以下才是真正热身(什么?才热身?是的,TMP博大精深,并不是一两天能掌握的^_^)

template <unsigned long N>

struct binary

{

static unsigned const value

= binary<N/10>::value << 1   // prepend higher bits

| N%10;                    // to lowest bit

};

template <>                           // specialization

struct binary<0>                      // terminates recursion

{

static unsigned const value = 0;

};

unsigned const one   =    binary<1>::value;

unsigned const three =   binary<11>::value;

unsigned const five  =  binary<101>::value;

unsigned const seven =  binary<111>::value;

unsigned const nine  = binary<1001>::value;

就以上代码大家可以发表自己的意见,不明白的我可以作出解释。

    最后,本文只起到抛砖引玉,拓宽视野的作用,但上述代码我都使用过,并且也了解其原理,所以就有了分享经验的打算。如果真正要掌握TMP,则需要系统学习,首先要认识STL,然后可以看看模板的高级用法及Boost程序库的源代码,说到Boost,则要特别关注它的MPL程序库。我在这里也说一下能使我感到震惊的两本C++书籍,一本是《Moden C++ Design》,另一本是《C++ Template Metaprogramming》,它们都讲述了TMP,前者结合了设计模式,而后者分析了Boost库的代码,都非常不错。

    TMP已名正言顺成为新ISO标准C++0x的特性,C++0x标准制定已接近尾声,预计今年或明年就可以发布,这是自2003年来,C++一次质的飞跃。本文写于2010/5/21

posted on 2013-03-21 21:48  rickerliang  阅读(379)  评论(0编辑  收藏  举报

导航