元函数简单来说就是一种可以在编译期调用的函数,它操作的对象一定是编译期可以确定的,比如类型和常量。元函数也是模板元编程中最基础的组成部分,甚至代码中的选择和循环等控制流都是由元函数来完成。而序列则进一步释放了元函数的威力,我们可以通过元函数为序列提供丰富的算法以解决各种实际问题。
元函数
一个普通的C++函数通常由函数名称、参数、返回类型以及函数主体这4个部分组成,比如:
int plus(int a, int b) |
作为对比,C++模板元编程的元函数同样也存在函数名称、参数以及函数主体3个部分,只不过它们的形式有一些不同罢了。还是以plus
函数为例,将其改写为元函数后的代码如下:
template <int a, int b> |
看到以上代码,有人可能会惊呼道:“这哪是什么函数,它明明就是一个类模板!”。是的没错!它确实是一个类模板,可同时也是一个元函数。让我们先接受这个定义,这样可以让我们很容易的找到它和普通函数版本的对应关系:首先plus
作为类模板名也是元函数名,然后类模板形参int a, int b
对应是元函数的形参,最后元函数的函数体就是类模板的定义。虽然在元函数中我们没有办法定义返回类型(实际上也没有必要定义,因为大多数时候元函数返回的就是类型本身),但是可以通过约定元函数的函数体中静态变量名称的方法定义数值的返回值,体现在plus
中即为static constexpr int value = a + b;
,通常情况下,元函数会约定value
为数值类型的返回值名称。
另外还有一种特殊的情况,当元函数没有形参时,对应的则是类或者实例化的类模板。比如:
struct kilometer { |
在C++模板元编程中,对于数值计算元函数的调用实际上就是访问起静态成员,根据约定会获取value
的值:
auto x1 = plus<7, 11>::value; |
到此为止,我们看到的都是有关数值计算的元函数,但是我又强调过数值计算并非元函数的重点工作。其实这是有意为之,目的是为了方便元函数与普通函数的对比,接下来让我把数值转换为类型以帮助我们讨论类型计算元函数。
这里我并不打算让类型计算元函数的例子显得过于跳跃,所以还是以plus
为例将其修改为:
template <int n> |
在上面的代码中新增了一个类模板int_
。int_
的实现非常简单,只是将数值转换成了类型。不过请不要小看了它的作用,因为它为类型计算元函数提供了计算数值的桥梁。我们发现元函数plus
的参数不再是数值类型而是类型本身,更有意思的是它也不再返回数值而是返回类型本身。请注意,对于类型计算的元函数,返回的是可公开访问的嵌套类型,我们通常约定该类型名为type
。在这个例子中即是:using type = int_<T1::value + T2::value>;
。当然,除了使用using
来定义别名以外,使用typedef
也是允许的。我选择使用using
,一方面因为它是新标准推荐的做法,另一方面从语法上看它更像是一个赋值语句,作为元函数的一部分它更容易理解。
同样的道理,调用类型计算元函数也需要用到using
或者typedef
,例如:
using new_type = typename plus<int_<7>, int_<11>>::type; |
上面的代码可以成功编译,这表明通过元函数plus<int_<7>, int_<11>>::type
得出的新类型new_type
和预期结果int_<18>
是符合的。
还有需要解释的一点是,虽然上文中介绍一些编写元函数的常规约定,但是有时候也可以灵活处理。比如,作为元函数的本体类模板,完全有能力返回多个嵌套类型或者常量数值:
template <class T1, class T2> |
这样修改的好处是能够直接访问元函数计算的数值结果,而不必通过::type
间接的访问:
auto x = plus<int_<7>, int_<11>>::value; |
最后也是元函数最重要的一个特点:特化。特化是C++模板元编程能够成立的根基。我们知道通过特化可以针对类模板的特定参数来规定类模板的具体行为,而元函数正是利用这一点来实现选择和循环等控制流的。关于特化在元函数中具体的使用方法,我将在后面的内容中详细介绍。