C++语言的表达式模板:表达式模板的入门性介绍(二)
ic 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;
for(double i = from + step / 2; i < to; i += step)
sum += e.eva l(i);
return step * sum;
}
翻译已经过原作者许可,转载请先征求原作者的许可。图片均取自原文,如果有水印为CSDN所打和老子没关系。出于清晰起见,文章中所有模板中的class都被改为typename。
模板(template)最早是以将类型(type)参数化为目的引入C++语言的。(译注1)链表 (list)是一个典型的例子。实际编码的时候,人们并不希望为保存不同类型变量的链表 分别编码,而是希望在编写的时候能够使用一个占位符(placeholder)来代替具体的类型 (即是模板参数),而让编译器来生成不同的链表类(模板的实例化)。
时至今日,模板的使用已经远远超过C++模板的发明者所预期的范畴。模板的使用已经涵盖 了泛型编程,编译时求值,表达式模板库,模板元编程,产生式编程(generative programming)等诸多领域。在这篇文章中,我们仅限于探讨一些表达式模板的编程知识, 侧重于编写表达式模板程序库这个方面。
我们必须指出:表达式模板库是相当复杂的。出于这个原因,我们读到过的关于表达式模 板的介绍都不是很容易理解的。因此,本文的作者希望能够通过本文为表达式模板提供一 个通俗的介绍,同时又不失对具体实现细节的阐述,从而对读者阅读模板库的代码能够起 到帮助。作者希望提取出表达式模板编码的一些原则性知识。有关于此领域的更多细节可 以参考其他著作。
创世纪
时至今日,我仍然能清晰的记起我的同事Erwin Unruh在一次C++标准委员会会议时展 示的得意之作。这是一段并不能通过编译的代码,但是它却给出了质数数列。(参见:UNR )编译它的过程中产生的错误信息中依次包含了每一个给定范围内的质数。当然,不能够 通过编译的程序是毫无意义的,然而这段代码是有意这样的。它旨在指出,这段计算是在 编译时进行的,没有可执行文件,从而也就没有运行时。质数的计算只是编译时期的副产 物而已。
这段小小的程序引发了之后多年对所谓模板元编程的雪崩般的研究。本文将介绍从中得来 的一些编程技巧和技术。那么,模板元编程的工作原理是什么呢?
从本质上来看,无论是质数的计算,还是本文中所提及的其他技术,都是基于如下原理的 :模板的实例化是一个递归过程。当编译器实例化一个模板时,它可能会发现在此之前另 外的模板需要首先实例化;在实例化这些模板的时候,又会发现有更多的模板需要实例化 。许多模板元编程的技巧就是基于这个原理,来实现递归式的计算的。
阶乘——编译时计算的第一个例子
作为第一个例子,我们来在编译时对N的阶乘进行计算。N的阶乘(记作N!)定义为从1到N 所有整数的积。(译注2),作为一个特例,0的阶乘是1。通常对阶乘的递归计算可以采用 函数递归的方法,如下是一个运行时计算的例子:
[cpp]
int factorial(int n)
{
return (n == 0 1 : n * factorial(n - 1));
}
这个函数反复调用自身,直到参数n的值减少到0为止。使用的例子:
[cpp]
cout << factorial(4) << endl;
递归式的函数调用是昂贵的,特别是在编译器无法进行内联(inline)优化的时候——这样 函数调用的负担马上就凸显出来。我们可以用编译时计算来避免这一点。做法如下:用递 归式的模板实例化来代替递归式的函数调用。这样,名为factorial的函数将由名为 factorial的类代替: