[cpp]
template
struct factorial
{
enum { ret = factorial::ret * n };
};
这个模板类既没有数据也没有成员函数,而仅仅是定义了有唯一enum值的匿名enum类型。 (之后便可以看到,factorial::ret起到了函数返回值的作用。)为了计算这个值,编译 器必须反复实例化以n-1为模板参数的新的模板类,这就是递归的第一推动力。
值得注意的是,factorial模板类的参数并不常见:它并不是一个类型的占位符,而是一个 int类型的常量。常见的模板参数都和下面的例子类似:
[cpp]
template class X {...};
其中T是一个实际类型的占位符。在编译器遇到形如X的代码时,T将被具体的类型( int)所取代。在我们的例子中,实际类型int成为了模板的参数,也就是说,在编译时将 被具体的int类型的值所取代。调用此模板类的代码可以是这样的:
[cpp]
cout << factorial<4>::ret <
编译器将依次实例化factorial<4>,factorial<3>……我们注意到,这个递归是没有终点的 。这样可不行。所以,需要利用模板的特化来提供这样一个终点。在我们的这个例子里:
[cpp]
template <>
struct factorial<0>
{
enum { ret = 1 };
};
从上述代码片段中可以看到递归将止步于此。因为此时enum的值已经不再依赖实例化其他 模板类了。
倘若你不熟悉模板的特化,在这里只需要记住对于特定的模板参数,可以提供一个特殊的 类模板即可。在我们的例子中我们提供了一个特殊的,参数为0的阶乘模板类。在这里编译 器可以不再通过递归来计算需要的值。
那么我们这么计算阶乘,好处是什么呢?表达式factorial<4>::ret在编译时将会被其具体 数值,也就是24所取代。运行时无需对此进行计算。在可执行文件里,是找不到计算的痕迹的。
开平方根——编译时计算的又一个例子
让我们试试另外一个编译时计算的例子。这一次,我们试图计算N的平方根的近似值——更准 确的说,我们希望能找到一个整数,使得它的平方是最小的比N的平方大的完全平方数。例 如:10的平方根大约是3.1622776601,所以哦我们希望能通过编程得到4这个比 3.1622776601大的最小的整数。如果采用运行时计算的方法的话,我们就需要调用C的标准 库:ceil(sqrt(N))。但是如果需要用这个值来定义数组的大小的话,那么我们就倒了大霉 :
[cpp]
int array[ceil(sqrt(N))];
不能通过编译,因为数组的大小在编译时必须是一个常数。(译注3)因此,我们有理由在 编译时进行计算。
回忆一下我们在第一个例子中所做的:我们利用了模板实例化是通过递归进行这一特性。 在这里我们再次通过引发递归式的模板实例化来近似获取相应的值。这里我们定义一个有 一个给定类型int的模板参数N的类模板,并使用一个内部保存的值来返回结果。如果我们 将这个类命名为Root,那么它的一个用例如下:
[cpp]
int array[Root<10>::ret];
Root的代码如下:
[cpp]
template struct Root
{
static const size_t ret = Root
(down mean : Upp)>::ret;
static const size_t mean = (Low + Upp) / 2;
static const bool down = ((mean * mean) >= N);
};
在此我们不拘泥于细节,仅仅给出一些注解。(译注4)模板类有三个参数,其中两个有默 认值。在这三个参数中:
需要开方的数
预期平方根的上界和下界。默认值是1和N。平方根必然是介于1和N之间的某个数。
在这个例子中,返回值ret不是一个enum的值,而是一个静态常数成员,用于引发递归实例 化。余下的静态数据成员mean和down仅仅作为辅助,以简化递归实例化的编码。
在什么时候递归才能停止呢?递归的停止取决于一个特化的,不需要进一步进行模板实例 化的模板。如下是所需的偏特化的Root类:
[cpp]
template
struct Root
{
static const size_t ret = Mid;
};
这里偏特化只有两个模板参数。这是因为在递归结束的时候,上界和下界均已收敛到结果上了。
在我们的递归例子中,会产生如下实例化的模板:
[cpp]
Root<10, 1, 10>;
Root<10, 1, 5>;
Root<10, 4, 5>;
Root<10, 4, 4>;
之后得到了4这个预期的结果。
从上述两个例子可以看出,编译时计算通常是通过递归实例化模板这一途径进行的。递归 的函数为类模板所取代。函数的参数为已知类型的常数模板参数代替,而返回值则由类内 保存的常数来表示。递归的终止通常由模板的特化来实现。有了上述的知识,Erwin Unruh 的质数计算程序将不再神秘,因为它无非是使用了与上述两个例子相同的原理而已。
表达式模板
到此为止,我们已经能够在编译时进行数值计算(译注5),然而这还不是本文的主题。下 面我们进行一项更宏伟的计划:在编译时进行更加复杂的表达式计算。首先我们来实现一 个编译时计算向量点乘的功能。点乘定义为两个向量对应元素的积的和。例如:两个三维 向量(1, 2, 3)和(4, 5, 6)的点乘等于1 * 4 + 2 * 5 + 3 * 6,也就是32。我们的目标是 使用表达式模板来计算任意维度向量的点乘,如下所示:
[cpp]
int a[4] = {1, 100, 0, -1};
int b[4] = {2, 2, 2, 2};
cout << dot<4>(a, b) <
点乘是表达式模板的一个最简单的例子,不过在这里使用的技术可以被扩展到高阶矩阵的 数值计算上。对于矩阵来说,编译时求值的技巧可以带来比向量计算更加好的性能提升。
反复用不同的参数代入相同函数求值的情况下,表达式模板可以起到有力的辅助作用。如 果使用这种技术,我们不再需要在运行时损失调用函数的时间,而是可以直接将函数在编 译时嵌入到调用之中。例如在计算积分
的时候。我们知道积分x / (1 + x)可以通过在积分区间中取n个等距离的点(这里是 [1.0, 5.0])来近似计算。(译注6)如果我们使用表达式模板来实现一个近似求解任意函 数积分的程序,那么它的一个可能的样子如下:
[cpp]
template
double integrate(ExprT e, double from, double to, size_t n)
{
double sum = 0;
double step = (to - from) / n