最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
模板元编程(template metaprogramming,TMP)是编写 template-base C++ 程序并执行于编译期的过程。即用C++写的模板程序,编译器编译具现化的过程。一旦TMP程序结束执行,从 templates 具体化出来C++源码,也会一如往常地被编译。
TMP的好处:第一,它让某些事情更容易;第二,由于 模板元编程 执行于C++编译期,因此可将工作从运行期转到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译器找出来;另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。
我们再说一下条款47中伪代码的例子:
template<typename IterT, typename DistT>
void advance(IterI &iter, DistT d)
{
if (iter is a random access iterator)
{
iter += d; //针对随机访问迭代器使用迭代器算术运算
} else {
if (d >= 0) { //针对其它迭代器分类反复调用 ++ 或 --
while(d--) ++iter;
} else {
while (d++) --iter;
}
}
}
我们可以使用typeid让其中判断iter类型的伪码运行:
template<typename IterT, typename DistT>
void advance(IterI &iter, DistT d)
{
if(typeid(typename std::iterator_traits<IterT>::iterator_category)
==typeid(std::random_access_iterator_tag))
{
iter += d; //针对随机访问迭代器使用迭代器算术运算
} else {
if (d >= 0) { //针对其它迭代器分类反复调用 ++ 或 --
while(d--) ++iter;
} else {
while (d++) --iter;
}
}
}
上述的 typeid-based 解法效率比 traits 解法低,因为在此方案中:
- 类型测试发生在运行期而不是编译期
- “运行期类型测试”代码会出现在(或被连接于)可执行文件中
这个例子可以说明 TMP 比正常的C++程序更高效,因为 traits 解法就是 TMP。
某些东西在 TMP 比在 “正常的” C++容易,对此 advance 也提供了一个好例子。条款47曾提过 advance 的 typeid-based 解法可能导致编译期问题,下面就是个例子:
std::list<int>::iterator iter;
...
advance(iter,10);
下面的 advance 版本就是针对上述调用而产生的,将 template参数 给替换:
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;
}
}
问题出在上述代码中的第5行的+=
操作符,list<int>::iterator
是双向迭代器,并不支持 +=
(只有随机访问迭代器支持)。虽然我们知道那一行代码因为 list<int>::iterator
不会被执行,但编译器必须确保所有源码有效,即使是不会被执行的代码。traits-based TMP解法是针对不同类型执行不同的函数,每个函数所使用的操作都可施行于该函数所接受的类型。
TMP 并没有真正的循环构件,所以循环效果是由递归完成的。TMP 主要是个“函数式语言”,而递归对于这类语言来说,就像电视对于美国通俗文化一样地无法分割。TMP 递归甚至不是正常的递归,因为 TMP 递归不涉及递归函数调用,而是涉及 递归模板化具现化(recursive template instantiation)。
使用 TMP 的阶乘运算 示范如何通过递归模板化具现化实现循环,以及如何在 TMP 中创建和使用变量:
template<unsigned n>
struct Factorial{ //一般情况,Factorial<n>的值是 n * Factorial<n-1>
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0>{ //特殊情况,Factorial<0>的值是1
enum { value = 1 };
};
有了这个template metaprogram,只要指涉 Factorial<n>::value
就可以得到 n的阶乘值。
循环发生在模板具现体 Factorial<n>
内部指涉另一个模板具现体 Factorial<n-1>
之时。我们需要一个特殊情况的template特化版本 Factorial<0>
来结束递归。
每个Factorial
模板具现体都是一个 struct,每个 struct 都使用 enum hack(见条款2)声明一个名为 value 的 TMP 变量,value 用来保存当前计算所获得的阶乘值。TMP 以递归模板具现化取代循环,每个具现体有自己一份 value,每个 value 有其循环内的适当值。你可以这样使用Factorial
:
int main()
{
std::cout<< Factorial<5>::value; //输出120
std::cout<< Factorial<10>::value; //输出3628800
return 0;
}
TMP 能够达成的目标:
-
确保量度单位正确。举个例子,将一个质量变量赋值给一个速度变量是错误的,但将一个距离变量除以一个时间变量并将结果赋值给一个速度变量则成立。使用 TMP 就可以确保在编译期所有量度单位的组合都正确。
-
优化矩阵运算。 条款21曾经提到过某些函数包括
operator*
必须返回新对象,而条款 44中有一个SquareMatrix
类。如果这样使用:typedef SquareMatrix<double,1000> BigMatrix; BigMatrix m1,m2,m3,m4,m5; //创建矩阵 ... //赋予它们数值 BigMatrix result = m1 * m2 * m3 * m4 * m5; //计算它们的乘积
以“正常的”函数调用动作来计算 result,会创建四个临时性矩阵,每一个用来存储对
operator*
的调用结果。乘法还可能产生了4个作用在矩阵元素身上的循环。如果使用与 TMP 相关的模板(即expression templates),就有可能消除那些临时对象并合并循环。所以TMP使用较少内存,执行速度也有提升。 -
可以生成客户定制之设计模式(custom design pattern)实现品。使用 policy-based design 之 TMP-based 技术,有可能产生一些 templates 用来表述独立的设计项(所谓policies),然后可以任意结合它们,导致模式实现品带着客户定制的行为。
Note:
- TMP 可将工作由运行期移到编译期,因而得以实现早期错误侦测和更高的执行效率
- TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码