元编程技法(1)——if_c

类别:编程语言 点击:0 评论:0 推荐:

一直想整理一下对于meta-programming的一些想法,就从这个最简单的开始吧!

if_c是boost::mpl库提供的一个元编程算法,它接受三个模板参数,第一个是bool类型,后面两个是类型名:

template<
      bool c
    , typename T1
    , typename T2
    >
struct if_c
{
    typedef unspecified type;
};

它的作用是,如果c为true,则type为T1;反之type为T2。如果要用伪代码来表达这个算法的意思,就是

if(c)
    type = T1
else
    type = T2

现在可以来看看这个算法的用途了,下面的模板函数PrintLarger接受两个类型模板参数,并且在控制台输出较大的一个类型的名字。

template <typename T1, typename T2>
void PrintLarger()
{
 typedef boost::mpl::if_c<(sizeof(T1)>sizeof(T2)),
                        T1,
                        T2
                      >::type T;

 std::cout << typeid(T).name() << std::endl;
 
}

例如,在Win32平台下,PrintLarger<int, double>会输出double,而PrintLarger<int, bool>输出int。不要小看这个算法,在元编程的世界里,它扮演的角色非同小可。作为一个基础构件,它提供了在编译期间进行逻辑判断并选择编译“路径”的方法,对于编译期计算来说,其意义不下于运行时计算中的if...else语句。

这个算法是怎样实现的呢?很简单,它用到的是在C++元编程中最常用的武器:模板偏特化。通过模板偏特化来影响编译器行为的做法在STL里面比比皆是,只不过STL并没有把它作为一个算法而已。而boost::mpl库则把它提炼出来,为更高阶的抽象提供了一个很好的基础。下面就来看看if_c算法的实现。

利用模板偏特化的算法一般分为两个阶段:一般定义阶段和特化定义阶段。一般定义提供了算法的一般“形状”,而特化定义则提供特殊路径下的行为。if_c的一般定义是这样的:

template<
      bool C
    , typename T1
    , typename T2
    >
struct if_c
{
    typedef T1 type;
};

也就是说,直接令type=T1就可以。但是我们希望C==false的时候,type=T2,该如何处理呢?这就是特化定义上场的时候了。它特别指出,当C==false的时候,type应该为T2:

template<
      typename T1
    , typename T2
    >
struct if_c<false,T1,T2>
{
    typedef T2 type;
};

注意这里的语法,特化值false不出现于模板参数列表中,而是直接跟在if_c之后。这两个定义合起来告诉编译器:if_c的第一个模板参数是一个bool值,如果它为false,那么采取特化定义,也就是typedef T2 type这一句;在其他的情况下(在这里,当然只有C==true啦),采取一般定义,也就是typedef T1 type。整个判断在编译期间就已经完成,不但不会消耗运行时期的时间,而且还可以轻易地办到在运行时很不好实现的一些事情,如上述的根据大小选择类型。

从if_c算法中,我们可以一窥元编程的一些特点:首先,元编程操作的往往是类型,一个算法通过模板机制来接受参数,而通过内部的typedef来返回结果类型;其次,统一的命名对于元编程具有重要意义,在if_c中,结果类型被命名为type,而这个命名就像运行时算法中的函数命名一样,只要大家都知道并且遵守,就可以很好的交流。如果一个使用者不知道if_c的计算结果类型叫做type,那就根本无法使用它。在元编程的世界里,这样一些约定叫做Concept,Concept可以看作是比类型更高的一层抽象,它规定了一个类型应该像什么样子(有点类似于接口)。如果一个类型满足一个Concept的要求,那么它就叫做这个Concept的一个Model,从而就可以参与所有要求这个Concept的计算。这一点有点类似于实现了一个接口的类实例可以参与所有要求该接口的计算一样。

了解了这个最简单的算法,下面可以对它进行一些推广:我们希望让第一个模板参数不只是单纯的bool类型,而是“任何可以被转换成bool类型的”类型。可以把这个算法命名为if_,这也是boost::mpl库提供的算法之一。我们希望这个算法的形状是:

template< 
      typename T1
    , typename T2
    , typename T3
    >
struct if_
{
    typedef unspecified type;
};

现在,希望if_的行为是:如果T1可以被转换成true,则type=T2;如果T1可以被转换成false,则type=T3;如果都不可以,那么就应该报错。幸运的是,编译期算法如果出错,一定是编译错误,不会造成运行时候的麻烦。

在这里,需要解决的第一个难题是:T1是一个类型,而true和false都是一个值,这两者之间怎么可能转换呢?命名规范又要大显身手了,我们可以约定:被用作T1的类型必须定义一个“可被转换成true或false的静态成员”,名字叫value。有了这个约定,我们就可以放心大胆的编写if_算法,而用户如果强行把不符合该约定的类型放到T1的位置,那么编译器一定会阻止这种愚蠢的做法。说到这里,if_算法的实现已经呼之欲出了,它直接利用了if_c算法:

template<
      typename T1
    , typename T2
    , typename T3
    >
struct if_
{
    typedef typename if_c<(bool)T1::value, T2, T3>::type type;
};

boost::mpl库的if_算法实现与这个异曲同工,只不过它的实现考虑了更多可移植性问题,支持Lambda表达式,并且使用了boost::static_cast函数,代码比较复杂。

现在只需要定义一个带有成员value的类型,就可以使用if_算法了。

struct TrueT
{
    static const int value = 1;
};
...
if_<TrueT, int, double>::type v; //此处v为int类型的变量

在这个简单的例子中,似乎if_比if_c麻烦了许多,因为if_c根本无须定义什么TrueT,只需要直接传入一个true就可以达到目的。但是,使用类型作为参数让if_可以直接使用其他的编译期算法,如果这些算法都遵守关于定义一个value成员的约定,那就可以节省不少的代码。更重要的是,它提升了抽象的级别:判断的依据不但可以是bool值,而且是“任何可以得出bool值的类型”。当然,我们往往不需要更高的抽象,所以正如Windows API很多都有增强的“Ex版本”一样,可以把if_看做if_c的增强版本,而我们应该根据场合选择合适的版本。

元编程为我们打开了一个神奇的世界,在这里可以实现许多在运行时世界里不可思议的事情。最妙的是,这些实现都不会消耗运行时间,它们所占用的资源仅仅是空间和编译时间而已。

本文地址:http://com.8s8s.com/it/it23741.htm