template metaprogramming(TMP,模板元编程)是编写template-based C++程序并执行于编译期的过程。
所谓template metaprogram(模板元程序)是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从template具现出的若干C++源码,便会一如以往地被编译。
TMP有两个伟大的效力。
第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的。
第二,由于模板元程序执行于C++编译期,因此可将工作从运行期转移到编译期。
这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期转移至编译期的另一个结果是,编译时间变长了。程序若使用TMP,其编译时间可能远长于不适用TMP的对应版本。
考虑在文章47的一段伪码:
template<typename IterT,typename DistT>
void advance(IterT& iter, DistT d)
{
if (iter is a random access iterator)
iter += d;//针对random access迭代器使用迭代器算术运算
else
{
//针对其他迭代器分类反复调用++或--
if (d >= 0)
{
while (--d) ++iter;
}
else
{
while (++d) --iter;
}
}
}
可以使用typeid让其中的伪码成真:
template<typename IterT,typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterT)::iterator_category)
==typeid(std::random_access_iterator_tag)
iter += d;//针对random access迭代器使用迭代器算术运算
else
{
//针对其他迭代器分类反复调用++或--
if (d >= 0)
{
while (--d) ++iter;
}
else
{
while (++d) --iter;
}
}
}
这个typeid-based解法的效率比traits解法低,因为在此方案中,1.类型测试发生于运行期而非编译期,2.“运行期类型测试”代码会出现在(或者说被连接于)可执行文件中。
advance的typeid-based实现方式可能导致编译期问题。
例如:
std::list<int>::iterator iter;
//...
//移动iter向前走10个元素,无法通过编译
advance(iter, 10);
下面的一段代码便是针对上述调用而产生的。将template参数IterT和DistT分别替换为iter和10的类型之后,得到这些:
void advance(std::list<int>::iterator& iter, int d)
{
if (typeid(typename std::iterator_traits
<std::list<int>::iterator>::iterator_category)
== typeid(std::random_access_iterator_tag)
iter += d;//错误
else
{
//针对其他迭代器分类反复调用++或--
if (d >= 0)
{
while (--d) ++iter;
}
else
{
while (++d) --iter;
}
}
}
问题出在使用了+=操作符,因为list<int>::iterator是bidirectional迭代器,并不支持+=。只有random access迭代器才支持+=。
此刻我们知道绝不会执行起+=那一行,因为测试typeid的那一行总是会因为list<int>::iterator而失败,但编译器必须确保所有源码都有效,纵使是不会执行起来的代码。而当iter不是random access迭代器时,"iter+=d"无效。与此对比的是traits-based TMP解法,其针对不同类型而进行的代码,被拆分为不同的函数,每个函数所使用的操作(操作符)都可施行于该函数所对付的类型。
TMP的起手程序是在编译器计算阶乘。TMP的阶乘运算示范如何通过“递归模板具现化”实现循环,以及如何在TMP中创建和使用变量:
//一般情况,Factorial<n>的值是n乘以Factorial<n-1>的值
template<unsigned n>
struct Factorial {
enum{value=n*Factorial<n-1>::value};
};
//特殊情况,Factorial<0>的值是1
template<>
struct Factorial<0> {
enum{value=1};
};
有了这个模板元程序,只要你指涉Factorial<n>::value就可以得到n阶乘值。
循环发生在template具现体Factorial<n>内部指涉另一个template具现体Factorial<n-1>时。和所有良好递归一样,我们需要一个特殊情况造成递归结束。这里的特殊情况是template特化体Factorial<0>。
每个Factorial template具现体都是一个struct,每个struct都使用enum hack声明一个名为value的TMP变量,value用来保存当前计算所得的阶乘值。若TMP拥有真正的循环构件,value应该在每次循环内获得更新。但由于TMP以“递归模板具现化”取代循环,每个具现体有自己的一份value,而每个value有其循环内的适当值。
可以这样使用Factorial:
int main()
{
std::cout << Factorial<5>::value;//印出120
std::cout << Factorial<10>::value;//印出3628800
return 0;
}
TMP的应用:
1.确保量度单位正确。
若使用TMP,可以确保(在编译期)程序中所有量度单位的组合都正确,不论其计算多么复杂。
2.优化矩阵运算。
考虑以下代码:
typedef SquareMatrix<double, 10000> BigMatrix;
BigMatrix m1, m2, m3, m4, m5;//创建矩阵
//...//赋予它们数值
BigMatrix result = m1 * m2 * m3 * m4*m5;//计算它们的乘积
以“正常的”函数调用动作来计算result,会创建4个暂时性矩阵,每一个用来存储对operator*的调用结果。各自独立的乘法产生了4个作用于矩阵元素身上的循环。若使用高级、与TMP相关的template技术,即所谓expression template,就有可能消除那些临时对象并合并循环,这一切都无需改变客户端的用法。于是TMP软件使用较少的内存,执行速度又有了戏剧性的提升。
3.可以生成客户定制的设计模式实现品。
设计模式如Strategy,Observer,Visitor等等都可以多种方式实现出来。运用所谓policy-based design的TMP-based技术,有可能产生一些template用来表述独立的设计选项,然后可以任意结合它们,导致模式实现品带着客户定制的行为。
总结
1.模板元编程可将工作由运行期移至编译期,因而得以实现早期错误侦测和更高的执行效率。
2.TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。