C++模板元编程[metaprogram]
by Micolai Josuttis, David Vandevoorde
摘自C++ Templates: The Complete Guide一书
[译者注:翻译本文,全为引介一种(相对于译者的孤陋而言)全新的编程方法。版权所有于原著者,笔者不敢稍假借之。
原文笔误甚多,族繁不可计数。笔者水平有限,改之恐失信于原著,不改恐遗害于读者。对行文中的一些明显错漏,皆按自己的理解作了补正,并列原文于侧,以备查考。而文中的程序代码,未能一一核对,如有错误,还望谅解。
原文术语,笔者自行揣摩翻译,恐失其当,多在首次出现处加译注,以方括号佐之,如:元编程[metaprogramming];译注或行文补遗之处,皆循此例。盖凡方括号中之文字,皆笔者之言也;圆括号中者为原文自有。
本文摘自《C++ Templates: The Complete Guide》一书第17章,文中所指章节数及页数皆指原书而言。感兴趣的读者可以自行查阅原书内容。
]
17.1 元编程[metaprogram]的第一个例子
“元编程”指的是“编‘编程序的’程序”。换言之,我们只给出代码的布局,而编程系统则在运行时生成代码来实现我们所希望的功能。通常,“元编程”这个词意味着一种“加诸自身”的特性——元编程的组件最终会变成它的产品代码/程序中的一部分。
元编程有何吸引人之处?和其他大部分编程方法一样,元编程的目标也是为了以更少的努力换取尽可能丰富的功能——这里的“努力”同样可以用代码长度、维护成本,或者其他标准来衡量。而元编程的独特之处在于,有些用户自定义的计算工作可以在翻译期发生。[使用元编程]潜在的动机要么是为了效率(通常翻译期计算得到的东西可以被优化掉),要么是为了简化接口(元程序[metaprogram]一般都会比展开后的最终程序短小),或者二者得兼。
元编程经常依赖于第15章发展出来的traits和type functions的概念。因此,我们建议你,在研读本章之前,你应该对此前的章节了然于胸。
1 元编程的第一个例子
在1994年的C++标准化委员会开会期间,Erwin Unruh发现可以用模板[templates]在编译期执行一些计算。他[用这种方法]写了一个生成质数的程序。这个小小练习中最令人目眩神迷的部分是:质数的生成是在编译过程中由编译器完成的,而不是在运行期。特别地,编译器还为从2到某个特定值之间的每一个质数都生成了一系列的错误信息[error message]。尽管这个程序的移植性不是特别的强(因为错误信息没有被标准化),但是这个程序的确展示了模板具现化[template instantiation]机制可以作为一种初级的递归语言,用以在编译期实现一些较为复杂的计算工作的能力。这种通过模板具现化在编译期执行计算的技术通常就被称为“模板元编程[template metaprogramming]”。
为了一窥元编程的全豹,我们从一个简单的练习开始(Erwin的质数程序将在稍后的第318页展示给大家)。下面的程序展示了如何在编译期计算3的任意次方:
// meta/pow3.hpp
#ifndef POW3_HPP
#define POW3_HPP
//primary template to compute 3 to the N;
template<int N>
class Pow3 {
public:
enum { result = 3 * Pow3<N-1>::result };
};
//full specialization to end the recursion}
template<>
class Pow3<0> {
public:
enum { result = 1 };
};
#endif // POW3_HPP
模板元编程背后的驱动力是模板的递归具现化[recursive template instantiation]。在我们的程序中,为了计算3^N,我们利用下面两条规则来驱动模板的递归具现化:
1. 3^N = 3 * 3^(N-1)
2. 3^0 = 1
第一个模板实现了通常情况下的递归规则:
template<int N>
class Pow3 {
public:
enum { result = 3 * Pow3<N-1>::result };
};
当用一个正整数N来具现化这个模板的时候,模板Pow<3>必须首先计算它其中的枚举值result。这个枚举值又被定义成同一个模板被N-1具现之后的对应值。
第二个模板是一个特化版本,给出了递归的终点。它仅仅是给出了Pow3<0>时的result值:
template<>
class Pow3<0> {
public:
enum { result = 1 };
};
如果使用这个模板来计算3^7,只需具现化一个Pow3<7>即可。现在让我们来研究一下,当我们这样具现化这个模板的时候,具体都发生了哪些事情:
#include <iostream>
#include "pow3b.hpp"
int main()
{
std::cout << "Pow3<7>::result = " << Pow3<7>::result
<< '\n';
}
首先,编译器具现化Pow3<7>,它的result值是:
3 * Pow3<6>::result [此处原Pow3<5>疑为笔误,改之]
然后需要用6具现化同一个模板。依此类推,Pow3<6>将具现化Pow3<5>,后者又具现化Pow3<4>……当具现化到Pow3<0>的时候,result的值被定为1,至此递归结束。
Pow3<>这个模板(包括其特化)就被称作“模板元程序[template metaprotram]”。该程序描述了一些运算,这些运算将在翻译期随着模板具现化的进行而被执行。这个例子相对比较简单,而且也看不出对我们有多大帮助,但是到了这个地步,[元编程]这个工具已经是唾手可得的了。
17.2 枚举值[enumeration values]vs静态常量[Static Constants]
在旧式C++编译器里,要想在类声明中使用“真正的常量”(所谓的“常量表达式”[constant-expression]),枚举值是唯一的选择。但是,自从C++标准化之后,情况已经有所改变。C++标准提出了所谓“类内静态常量性初始化”[in-class static constant initializer]的概念。下面这个简单的例子介绍了这一机制的结构:
struct TrueConstants {
enum { Three = 3 };
static int const Four = 4;
};
在这个例子里,Four也是一个“真正的常量”——一如Three那样。
利用这个机制,我们的Pow3元程序可以像下面这样实现:
// meta/pow3b.hpp
#ifndef POW3_HPP
#define POW3_HPP
//primary template to compute 3 to the Nth
template<int N>
class Pow3 {
public:
static int const result = 3 * Pow3<N-1>::result;
};
// full specialization to end the recursion
template<>
class Pow3<0> {
public:
static int const result = 1;
};
#endif //POW3_HPP
这个版本和上个版本唯一的不同就是用类的静态常量成员代替了上个版本中的枚举值。然而,这个版本有一个缺点:静态常量成员是左值[lvalue]。因此,如果你声明这样一个函数:
void foo(int const&);
并传一个该元程序的具现结果给这个函数:
foo(Pow3<7>::result);
编译器必须把Pow3<7>::result的地址传给函数,这将强迫编译器为静态成员产生实体并分配空间。这样一来,这个计算工作的影响就不再单纯的限于“编译期”了。
枚举值不是左值(意即它没有实际的地址)。因此,即使你以“传址[by reference]”的方式调用它,也不会用到任何静态内存。传递枚举值的开销几乎完全相当于把计算的结果作为一个常数符号[literal]来传递一样。基于以上原因,在本书的所有元程序中,我们都是使用枚举值的。
17.3 另一个例子:计算平方根
让我们来看一个稍微复杂一点的例子:如何写一个元程序来计算给定值N的平方根。这个元程序应该是类似于这样的(其中用到的技术将在稍后解释):
// meta/sqrt1.hpp
#ifndef SQRT_HPP
#define SQRT_HPP
// primary template to compute sqrt(N)
template <int N, int LO=1, int HI=N>
class Sqrt {
public:
// compute the midpoint, rounded up
enum { mid = (LO+HI+1)/2 };
// search a not too large value in a halved interval
enum { result = (N<mid*mid) ? Sqrt<N,LO,mid-1>::result
: Sqrt<N,mid,HI>::result };
};
// partial specialization for the case when LO equals HI
template<int N, int M>
class Sqrt<N,M,M> {
public:
enum { result = M };
};
#endif // SQRT_HPP
第一个模板是通用的递归计算,它有一个模板参数N(被开平方根的值)和另外两个可选的参数。后一个模板代表了返回的平方根值的取值范围。如果仅使用一个模板参数去调用这个模板,正如我们所知,平方根最小是1,而最大不会超过被开方数本身。
我们的递归得益于二分查找技术[binary search technique](在这个上下文中,经常也被称为“二分法[method of bisection]”)。在模板内部,我们首先计算result是在LO到HI这个区间的前半部还是后半部,这一分支选择由?:运算符实现。如果mid^2比N大,我们将继续查找前半部;如果mid^2小于或等于N,我们将用同一个模板来查找后半部。
当LO和HI都等于同一个值M的时候,特化模板会终止这个递归,而这个M就是我们所求的result。
让我们再来看一个使用了这个元程序的小小的示例吧:
// meta/sqrt1.cpp
#include <iostream>
#include "sqrt1.hpp"
int main()
{
std::cout << "Sqrt<16>::result = " << Sqrt<16>::result
<< '\n';
std::cout << "Sqrt<25>::result = " << Sqrt<25>::result
<< '\n';
std::cout << "Sqrt<42>::result = " << Sqrt<42>::result
<< '\n';
std::cout << "Sqrt<1>::result = " << Sqrt<1>::result
<< '\n';
}
表达式
Sqrt<16>::result
将被扩展成
Sqrt<16, 1, 16>::result
在模板内部,元程序将像下面这样计算Sqrt<16, 1, 16>::result的值:
mid = (1+16+1)/2
= 9
result = (16<9*9) ? Sqrt<16,1,8>::result
: Sqrt<16,9,16>::result
= (16<81) ? Sqrt<16,1,8>::result
: Sqrt<16,9,16>::result
= Sqrt<16,1,8>::result
计算结果是:result等于Sqrt<16, 1, 8>::result,而后者将被展开为:
mid = (1+8+1)/2
= 5
result = (16<5*5) ? Sqrt<16,1,4>::result
: Sqrt<16,5,8>::result
= (16<25) ? Sqrt<16,1,4>::result
: Sqrt<16,5,8>::result
= Sqrt<16,1,4>::result
类似的,Sqrt<16, 1, 4>::result将被分解为:
mid = (1+4+1)/2
= 3
result = (16<3*3) ? Sqrt<16,1,2>::result
: Sqrt<16,3,4>::result
= (16<9) ? Sqrt<16,1,2>::result
: Sqrt<16,3,4>::result
= Sqrt<16,3,4>::result
最后,Sqrt<16, 3, 4>::result的计算结果是:
mid = (16+4+1)/2
= 4
result = (16<4*4) ? Sqrt<16,3,3>::result
: Sqrt<16,4,4>::result
= (16<16) ? Sqrt<16,3,3>::result
: Sqrt<16,4,4>::result
= Sqrt<16,4,4>::result
而Sqrt<16, 4, 4>::result将终止这次递归,因为这个模板(上限和下限相等)和那个显式特化的版本相匹配。最终的result就将是:
result = 4 [译注:原文是result = 5,疑为笔误]
透析全具现化[all instantiations]
================================
在上面那个例子中,实际上我们具现化了相当多的模板实例,比如在计算平方根的第一次迭代中:
(16<=8*8) ? Sqrt<16,1,8>::result
: Sqrt<16,9,16>::result
这个式子不仅具现化了条件语句的正分支[按:指Sqrt<16, 1, 8>],而且也具现化了负分支(Sqrt<16, 9, 16>)。更过分的是,由于该句代码试图通过::运算符访问一个类型别[class type]的成员,这个类型别的所有成员都要被具现化出来。这意味着,Sqrt<16, 9, 16>的全具现化结果将包括Sqrt<16, 9, 12>和Sqrt<16, 13, 16>各自的全具现化。如果认真的探究这整个过程的细节,我们会发现,最终有几十个模板实例被具现化出来。所有模板实例的数量几乎是N值的两倍。
这一点是让人非常不快的,因为对大多数编译器来说,模板具现化都是一种相当昂贵的操作,特别是对内存的消耗极大。幸运的是,有一种技术可以减少这种模板实例的急剧膨胀。我们可以通过特化[specialization]来选择所需的计算结构,而不是使用?:运算符来选择。为了演示这种技术,让我们重写一遍Sqrt的元程序:
// meta/sqrt2.hpp
#include "ifthenelse.hpp"
// primary template for main recursive step
template<int N, int LO=1, int HI=N>
class Sqrt {
public:
// compute the midpoint, rounded up
enum { mid = (LO+HI+1)/2 };
// search a not too large value in a halved interval
typedef typename IfThenElse<(N<mid*mid),
Sqrt<N,LO,mid-1>,
Sqrt<N,mid,HI> >::ResultT
SubT;
enum { result = SubT::result };
};
// partial specialization for end of recursion criterion
template<int N, int S>
class Sqrt<N, S, S> {
public:
enum { result = S };
};
这里的关键不同是使用了IfThenElse模板,对这个模板的介绍请参见第272页的第15.2.4节:
// meta/ifthenelse.hpp
#ifndef IFTHENELSE_HPP
#define IFTHENELSE_HPP
// primary template: yield second or third argument depending on first argument
template<bool C, typename TA, typename Tb>
class IfThenElse;
// partial specialization: true yields second argument
template<typename Ta, typename Tb>
class IfThenElse<true, Ta, Tb> {
public:
typedef Ta ResultT;
};
// partial specialization: flase yields third argument
template<typename Ta, typename Tb< {
public:
typedef Tb ResultT;
};
#endif //IFTHENELSE_HPP
请记住,IfThenElse模板是这样一种工具:它根据一个给定的布尔常量来选择两个型别中的一个。如果这个布尔常量是true,ResultT将被定义[typedef]为第一个型别;反之,ResultT则被定义为第二个型别。这里我要特别提醒的是:为一个类模板“起一个别名”[typedef]并不会导致C++编译器具现化该模板类实例的本体[body]。因此,当我们写下
typedef typename IfThenElse<(N<mid*mid),
Sqrt<N,LO,mid-1>,
Sqrt<N,mid,HI>>::ResultT
SubT;
的时候,Sqrt<N,LO,mid-1>和Sqrt<N,mid,HI> 都不会被完全具现化。只有到访问SubT::result的时候,这两个型别中的一个才会被完全的具现化出来,并成为SubT的等价物。和我们的第一种方法相比,这个策略最终导致的模板实例数量正比于log2(N):在N相当大的时候,这个结果要比第一种方法的结果小得多了。
17.4 使用归纳变量[induction variables]
你也许会抗辩说,前面几个例子里那种元编程的路数过于晦涩。你也许更希望知道,有没有什么方法,一旦你遇到需要用元编程解决的问题,能让你自救。哦,让我们看一个更“幼稚”但是也许更“迭代”的实现计算平方根的元程序。
这个“幼稚的迭代算法”是这样的:为了计算某个值N的平方根,我们只需写一个循环,循环变量I从1开始增加,直到I的平方大于等于N,循环才终止。此时的循环变量I的值就是N的平方根。如果用普通的C++来写这个算法,看上去应该像这样:
int I;
for(I = 1; I * I < N; ++I){
;
}
// I now contains the square root of N
但是,作为一个元程序,我们必须用递归的办法来构造这个循环,并且需要一个结束条件[end criterion]来终止递归。这个循环的元编程实现版本应该看上去类似这样:
// meta/sqrt3.hpp
#ifndef SQRT_HPP
#define SQRT_HPP
// primary template to compute sqrt(N) via iteration
template <int N, int I=1>
class Sqrt {
public:
enum { result = (I*I<N) ? Sqrt<N,I+1>::result
: I };
};
// partial specialization to end the iteration
template<int N>
class Sqrt<N,N> {
public:
enum { result = N };
};
#endif // SQRT_HPP
我们通过在模板Sqrt<N, I>中“迭代”I来实现循环。只要I * I < N 的结果为true,我们就把下一次迭代的结果Sqrt<N, I+1>::result作为result的值。否则,I就是我们所要的结果。
例如,如果你想计算Sqrt<16>,这个模板将被展开成Sqrt<16, 1>。而后,我们将开始一系列的迭代过程,其中用到了一个被称为“归纳变量”的变量I,它的值从1开始。这样,只要I^2比N小,我们就通过计算Sqrt<N, I+1>::result来进入下一次迭代。当I^2大于等于N的时候,I就等于result。
你也许还想知道为什么非要用一个模板的特化版本[template specialization]来结束递归不可,因为第一个模板或迟或早的总会走到以I作为结果的那一步,似乎递归就会终止于此了。这里要再强调一次,[之所以要引入模板的特化版本]是因为?:操作符两个分支都要展开会带来副作用,上一节中我们曾就此问题进行过讨论。因此,计算Sqrt<4>的时候,编译器具现化的过程是像下面这样的:
* step 1
result = (1*1<4 ? Sqrt<4,2>::result
: 1
* step 2
result = (1*1<4 ? (2*2<4) ? Sqrt<4,3>::result
: 2
: 1
* step 3
result = (1*1<4 ? (2*2<4) ? (3*3<4) ? Sqrt<4,4>::result
: 3
: 2
: 1
* step 4
result = (1*1<4 ? (2*2<4) ? (3*3<4) ? 4
: 3
: 2
: 1
尽管在第二步我们就得到了正确的结果,但编译器仍将实例化下去,直到发现一个特化版本来结束递归为止。如果没有这个特化版本,编译器只能一直[无限]实例化下去,直到编译器本身的能力极限。
再说一次,使用模板IfThenElse是为了解决下面的问题:
// meta/sqrt4.hpp
#ifndef SQRT_HPP
#define SQRT_HPP
#include "ifthenelse.hpp"
// template to yield template argument as result
template<int N>
class Value {
public:
enum { result = N };
};
// template to compute sqrt(N) via iteration
template <int N, int I=1>
class Sqrt {
public:
// instantiate next step or result type as branch
typedef typename IfThenElse<(I*I<N),
Sqrt<N,I+1>,
Value<I>
>::ResultT
SubT;
// use the result of branch type
enum { result = SubT::result };
};
#endif // SQRT_HPP
我们这里没有使用结束条件[end criterion],而是用了一个Value<>模板来把模板参数的值传给result。
使用IfThenElse<>模板之后,被具现化的模板数量正比于log2(N)而非N。就元编程的成本而言,这是一个非常显著的进步。由于编译器在模板具现化方面总是存在一定的能力极限,这一方法意味着你可以计算更大的数值的平方根。比如,如果你的编译器支持64层嵌套具现化的话,那么你最大可以计算4096的平方根(而不是64的平方根)。
这个“迭代的”Sqrt模板的输出是这个样子的:
Sqrt<16>::result = 4
Sqrt<25>::result = 5
Sqrt<42>::result = 7
Sqrt<1>::result = 1
请注意,为简单起见,在这个实现中,平方根都是向上取整的(也即:42的平方根[6.48...]是7而不是6)。
17.5 计算的完整性[computational completeness]
从Pow<>和Sqrt<>这两个例子中我们可以看到,模板元编程可以包括以下一些要素:
*状态变量[state variables]:模板参数可当此任
*循环结构[loop constructs]:通过递归来实现
*分支选择[path selection]: 利用条件表达式或特化来达成
*整型算术[integer arithmetic]
如果递归具现的层数不受限制,状态变量的个数不受约束的话,这一技术足以计算任何“可资计算的”[computable]物什。然而,用模板来做这样的计算工作并非易与。更有甚者,模板具现化对编译器的资源需索甚笃,急剧扩张的递归具现化进程会使编译器速度骤降,甚至耗尽所有的可用资源犹不能自解。虽然C++标准建议(但并未强制)至少应支持17层的递归具现,但复杂模板元程序可以轻而易举的突破这一限制。
因此,在实际应用中,还是应当检点的使用模板元程序。不过,在某些情况下,模板仍是一种不可或缺的工具。[模板元程序]的特别之处在于,对某些关键性的算法实现而言,它可以被隐藏到更“传统”的模板之内去,以之来尽可能的压榨出更多的效能。
17.6 具现化递归[recursive instantiation]vs模板参数递归[recursive template arguments]
考虑下面的递归模板:
template<typename T, typename U>
struct Doublify {};
template<int N>
struct Trouble {
typedef Doublify<typename Trouble<N-1>::LongType,
typename Trouble<N-1>::LongType,> LongType;
};
template<>
struct Trouble<0> {
typedef double LongType;
};
Trouble<10>::LongType ouch;
使用Trouble<10>::LongType不仅触发了对Trouble<9>、Trouble<8>、……Trouble<0>的递归具现,而且具现了Doublify这个型别,使之越来越复杂。表17.1显示了这种复杂性是怎样迅速膨胀的。
正如我们在表17.1中所见,对表达式Trouble<N>::LongType的型别描述的复杂性以N的指数级递增。通常,这样一个状况[指模板参数的递归]对C++编译器造成的压力,要比没有递归的模板参数的递归具现化造成的压力大得多。这里存在的一个问题是:编译器通常仅仅是为每个型别保存一个裁减[mangle]过的名字作为其代表。这种裁减过的名字一般是以某种方式编码模板特化的确切过程。早期的C++编译器采取的一种编码方式[使得内部的型别名称长度]大致和模板id的长度成比例。如果用这类编译器编译上面的Trouble<10>::LongType,大概需要超过10,000个字符才行。
新一点的C++实现考虑到了在现代C++程序设计中经常使用内嵌模板id的事实,因此使用了智能的压缩方法来极大的降低名称编码长度的增长(比如,可能仅用几百个字符就代表得了Trouble<10>::LongType)。尽管如此,其他方面的问题都是等价的,所以只要有可能,还是尽量组织好你的递归代码,不要引入递归内嵌的模板参数。
表17.1. Trouble<N>::LongType 的增长
Trouble<0>::LongType double
Trouble<1>::LongType Doublify<double,double>
Trouble<2>::LongType Doublify<Doubleify<double,double>,
Doubleify<double,double> >
Trouble<3>::LongType Doublify<Doubleify<Doubleify<double,double>,
Doubleify<double,double> >,
Doubleify<double,double>,
Doubleify<double,double> > >
17.7 利用元编程实现解循环[unroll loops]
元编程最早的实际应用之一就是数值运算中的解循环[按:指用非循环的方式改写循环代码以获取更高的效率],我把它展示于此,作为一个完整的示例。
数值应用中经常要处理N维数组或数学意义上的N维向量。一个典型的操作就是计算所谓的“点积[dot product]”。两个向量a和b的点积被定义为这两个向量对应分量的乘积之和。例如,如果两个向量都有三个分量,点积结果就为:
a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
一个数学库通常会提供一个函数来计算这样一个点积。先让我们考虑下面这个直白的[straightforward]实现方式:
// meta/Loop1.hpp
#ifndef LOOP1_HPP
#define LOOP1_HPP
template <typename T>
inline T dot_product (int dim, T* a, T* b)
{
T result = 0;
for (int i=0; i < dim; ++i)
{
result += a[i] * b[i];
}
return result;
}
#endif //LOOP1_HPP
[按:此处原文程序不全,按自己的理解补全之]
当我们这样调用这个函数的时候:
#include <iostream>
#include "loop1.hpp"
int main()
{
int a[3] = { 1, 2, 3};
int b[3] = { 5, 6, 7};
std::cout << "dot_product(3,a,b) = " << dot_product(3,a,b)
<< '\n';
std::cout << "dot_product(3,a,a) = " << dot_product(3,a,a)
<< '\n';
}
我们就会得到下面的结果:
dot_product(3,a,b) = 38
dot_product(3,a,a) = 14
这个结果是正确的,但是在要求高效率的应用中,它耗费了太多的时间。即使是把这个函数声明成inline的通常也获得不到优化的效能。
问题在于,编译器通常是把循环优化成若干次迭代,而这种优化在这个例子中只有反效果。简单的把循环展开成
a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
反而会好得多。
当然,如果我们只是偶尔的计算为数不多的几个点积,这种[未经优化的]表现问题不大。但是,如果我们使用这个库组件来执行上百万次点积计算,可就有天壤之别了。
当然,我们也可以不调用dot_product()函数,改以直接书写计算式,或者我们还可以使用特殊的函数来计算特殊维数的向量的点积。但这些方法都有够乏味冗长。模板元编程为我们解决了这个问题:我们“编程”来解开循环。这里是一个元程序:
// meta/Loop2.hpp
#ifndef LOOP2_HPP
#define LOOP2_HPP
// primary template
template <int DIM, typename T>
class DotProduct {
public:
static T result (T* a, T* b) {
return *a * *b + DotProduct<DIM-1,T>::result(a+1,b+1);
}
};
// partial specialization as end criteria
template <typename T>
class DotProduct<1,T> {
public:
static T result (T* a, T* b) {
return *a * *b;
}
};
// convenience function
template <int DIM, typename T>
inline T dot_product (T* a, T* b)
{
return DotProduct<DIM,T>::result(a,b);
}
#endif // LOOP2_HPP
现在,只要把你的应用程序稍加改动,你就可以得到同样的结果:
// meta/loop2.cpp
#include <iostream>
#include "loop2.hpp"
int main()
{
int a[3] = { 1, 2, 3};
int b[3] = { 5, 6, 7};
std::cout << "dot_product<3>(a,b) = " << dot_product<3>(a,b)
<< '\n';
std::cout << "dot_product<3>(a,a) = " << dot_product<3>(a,a)
<< '\n';
}
我们不再写
dot_product(3, a, b)
而是代以
dot_product<3>(a, b)
这个表达式很方便的具现了一个函数模板,把调用翻译成
DotProduct<3, int>::result(a, b)
这就是这个元程序的起点。
在元程序内部,result被定义为a和b的第一个分量之积,再加上a和b的剩余分量的点积的result:
template <int DIM, typename T>
class DotProduct {
public:
static T result (T* a, T* b) {
return *a * *b + DotProduct<DIM-1,T>::result(a+1,b+1);
}
};
结束条件是当a和b都是一维向量的时候:
template <typename T>
class DotProduct<1,T> {
public:
static T result (T* a, T* b) {
return *a * *b;
}
};
因此,对
dot_product<3>(a, b)
而言,具现化过程是这样进行计算的:
DotProduct<3,int>::result(a,b)
= *a * *b + DotProduct<2,int>::result(a+1,b+1)
= *a * *b + *(a+1) * *(b+1) + DotProduct<1,int>::result(a+2,b+2)
= *a * *b + *(a+1) * *(b+1) + *(a+2) * *(b+2)
注意:这种编程方法要求维数在编译器是已知的,这个条件通常(不过不是一定)是能满足的。
一些库,比如Blitz++(参见[Blitz++])、MTL库(参见[MTL])以及POOMA(参见[POOMA]),都是用这类元程序来提供快速的线性代数运算例程的。这种元程序要比优化器来得好,因为前者可以把较高层次的[higher-level]知识集成到运算当中去。事实上,实现一个工业级强度[industrial-strength]的库所要解决的各种细节问题远远超过我们所呈现在这里的这一点仅和模板有关的内容。不计后果的一味解循环固然并不总是能得到运行时的优化效果,但这已经属于额外的工程考量了,已经超出了本文所涉及的范围。
17.8 后记
前面提到过,史载最早的元程序是由Erwin Unruh写的,此后由Siemens在C++标准化委员会上推荐。他强调了模板具现化过程中的运算完整性,并通过开发第一个元程序演示了他的观点。他使用Metaware的编译器,并诱使编译器通过一系列的出错信息[error message]来给出一串质数。下面就是1994年C++委员会会议期间被传阅的代码(为了能在符合标准的编译器上编译,稍作了一些修改)。
// meta/unruh.cpp
// prime number computation by Erwin Unruh
template <int p, int i>
class is_prime {
public:
enum { prim = (p==2) || (p%i) && is_prime<(i>2?p:0),i-1>::prim
};
};
template<>
class is_prime<0,0> {
public:
enum {prim=1};
};
template<>
class is_prime<0,1> {
public:
enum {prim=1};
};
template <int i>
class D {
public:
D(void*);
};
template <int i>
class Prime_print { // primary template for loop to print prime numbers
public:
Prime_print<i-1> a;
enum { prim = is_prime<i,i-1>::prim
};
void f() {
D<i> d = prim ? 1 : 0;
a.f();
}
};
template<>
class Prime_print<1> { // full specialization to end the loop
public:
enum {prim=0};
void f() {
D<1> d = prim ? 1 : 0;
};
};
#ifndef LAST
#define LAST 18
#endif
int main()
{
Prime_print<LAST> a;
a.f();
}
如果你编译这段程序,只要初始化d失败,编译器就会在编译Prime_print::f()处打印出错信息。当初始值为1的时候,就会发生上述情况。因为[D型别]只有一个带void*参数的构造函数[constructor],而只有0才能被合法的转换[conversion]为void*。例如,在某种编译器上,我们(在其他信息中间)会得到如下这些出错信息:
unruh.cpp:36: conversion from &39;int&39; to non-scalar type &39;D<17>&39; requested
unruh.cpp:36: conversion from &39;int&39; to non-scalar type &39;D<13>&39; requested
unruh.cpp:36: conversion from &39;int&39; to non-scalar type &39;D<11>&39; requested
unruh.cpp:36: conversion from &39;int&39; to non-scalar type &39;D<7>&39; requested
unruh.cpp:36: conversion from &39;int&39; to non-scalar type &39;D<5>&39; requested
unruh.cpp:36: conversion from &39;int&39; to non-scalar type &39;D<3>&39; requested
unruh.cpp:36: conversion from &39;int&39; to non-scalar type &39;D<2>&39; requested
C++模板元程序作为一种严肃的编程工具,首次为大众所知(并获得某种程度的形式化[formalized])是在Todd Veldhuizen的论文Using C++ Template Metaprograms(参见[VeldhuizenMeta95])。同时Todd在Blitz++(一个C++的数值计算库,参见[Blitz++])中的努力也为元编程技术引入了许多精巧性与扩展性(这些努力也见诸于下一章所要介绍的表达式模板[expression template]技术)。