我们继续演进前面那个无聊的类型计算的例子,来得出元函数的定义。
前面我们实现了PointerOf,它对于传进的任意类型T可以计算出T的指针类型。
template<typename T>
struct PointerOf
{
using Result = T*;
};
现在我们想要实现一个能够计算T的指针的指针类型的模板,怎么做?
一种做法是直接定义一个新的模板:
template<typename T>
struct Pointer2Of
{
using Result = T**;
};
为了让类型计算结果更像是出自函数的返回值,我们将计算结果的类型别名后续统一叫做Result
。上述类模板本质上是一个对类型进行计算的函数:
Pointer2Of :: (typename T) => T -> T**
可以这样使用该函数:
int* pi;
Pointer2Of<int>::Result ppi = &pi
上述代码中Pointer2Of<int>::Result
的计算发生在编译期,当在C++运行期前它已经得到计算结果int**
了。所以上述代码在编译器计算完成后,就相当于如下代码:
int* pi;
int** ppi = &pi
虽然我们把类模板当做编译期函数来看,但是这种函数语法看起来和我们熟悉的函数相差较大,但究其本质和函数调用并无差异,都是为函数传入符合要求的实参,获得函数返回结果。
我们可以认为由于圆括号已经优先给了运行时C++函数,所以这种编译期C++函数的定义和调用都使用尖括号,并且需要显示调用Result才对函数进行运算求值。当使用这种编译期函数但并不调用Result时,和在“运行期C++”中使用一个函数指针类似,仅用做保存和传递用,但并不求值。
编译期函数计算,可以调用已有的其它编译期函数。如下通过嵌套调用PointerOf,也可以实现Pointer2Of:
template<typename T>
struct Pointer2Of
{
using Result = typename PointerOf<typename PointerOf<T>::Result>::Result;
};
上面我们通过嵌套调用两次PointerOf来完成Pointer2Of的实现。在Pointer2Of中我们每次使用PointerOf<...>::Result
时前面都用了typename关键字。原因是一旦PointerOf后面的尖括号中存在非具体类型的话,那么PointerOf的内部类型Result就是一个推导类型
。C++标准要求使用推导类型前面必须使用typename关键字显示指明这是一个类型。所以我们在Pointer2Of中使用PointerOf完整的方式是这样的:typename PointerOf<...>::Result
。
和Haskell相比,我们必须得承认C++的这种函数式编程的书写确实太繁琐了。为了简化对元函数的使用,我们可以用宏封装一下PointerOf:
#define __pointer(...) typename PointerOf<__VA_ARGS__>::Result
这样Pointer2Of的定义可以简化如下:
template<typename T>
struct Pointer2Of
{
using Result = __pointer(__pointer(T));
};
现在看起来好多了,__pointer(T)
的写法更像是在调用一个函数。
可以看到我们对类模板进行约束,固定用Result保存计算结果,且只返回单一结果,可以使我们将模板当做函数使用时的写法得到统一,这对于我们进行函数组合简直是必须的。
后续我们将一直把这种在编译期进行计算,靠Result返回计算结果的类模板看作是编译期的函数,它的目的是为了支持C++模板元编程。为了和C++运行时函数进行区分,后文中我们统一将其称作元函数。
如同函数是函数式编程的构成基础一样,元函数是C++模板元编程的构成基础。