20180408 C++ 认识模板(template)元编程
模板元编程(template metaprogram)是以C++写成,执行于C++编译器内的程序。
模板元编程(TMP)是被发现而不是被发明出来的。
TMP的特点:由于TMP执行于C++编译期,因此可将工作从运行期转移到编译期,这导致的结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找到。另外使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较小的内存需求,然而将工作从运行期转移到编译期的另一个结果是,编译时间变长了。
例如下面的STL 的advance伪码:
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让其中的伪码为真,取得C++对此问题的一个“正常”解决方案-所有工作都在运行期进行:
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-base解法的效率比traits解法低,因为在此方案中:
(1)类型测试发生于运行期而非编译期;
(2)“运行期类型测试”代码会出现在(或说被连接于)可执行文件中。
实际上这个例子正可彰显TMP如何能够比“正常的”C++程序更高效,因此traits解法就是TMP。traits的一大特点就是“引发 编译期发生于类型身上的if...else计算”。
advance的typeid-based实现方式可能导致编译期问题,eg:
std::list<int>::iterator iter;
...
advance(iter,10);//移动iter向前走10个元素,这样实现的话,无法通过编译
下面这一版的advance便是针对上述调用而产生的,将模板参数IterT和DistT分别替换为iter和10的类型之后,我们就会得到:
void advance(std::list<int>::iterator& iter,int d)
{
if(typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category)
== typeid(std::random_access_iterator_tag))
{
iter += d;//错误,因为在list<int>::iterator身上使用+=,但 //list<int>::iterator是bidirectional迭代器,并不支持+=
}
else
{
if(d >= 0){while(d--) ++iter;} //针对其他迭代器类型
else {while(d--) --iter;} //反复调用++或--
}
}
TMP已经被证明是“图灵完全(Turing-complete)”机器,意思是它的威力大到足以计算任何事物。
TMP对于循环的应用:
TMP并没有真正的循环构件,所以循环效果是借由递归完成,TMP主要是个“函数式语言”,TMP的递归甚至不是正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”。
TMP的起手程序是在编译期计算阶乘,eg:
template<unsigned n> //一般情况下:Factorial<n>的值是n乘以
struct Factorial //Factorial<n-1>的值
{
enum {value = n * Factorial<n-1>::value};
};
template<> //特殊情况
struct Factorial<0> //Factorial<0>的值是1
{
enum {value = 1};
};
你可以这样使用Factorial:
int main()
{
std::cout<<Factorial<5>::value;//打印出 120
std::cout<<Factorial<19>::value;//打印出 3628800
}
使用TMP可以达成的目标如下:
(1)确保量度单位正确:例如,将一个质量变量赋值给一个速度变量是错误的,但将一个距离变量除以一个时间变量并将结果赋值给一个速度变量则成立,若使用TMP,就可以确保(在编译期)程序中所有量度单位的组合都正确,不论其计算多复杂,这也就是为什么TMP可以被用来进行早期的错误侦测。
(2)优化矩阵运算:在某些函数中,包括operator*,必须返回新对象,而考虑下面的SquareMatrix类:
typedef SquareMatrix<double,10000> BigMatrix;
BigMatrix m1,m2; //创建矩阵并赋予他们数值
...
BigMatrix result = m1 * m2; //计算他们的乘积
以"正常的"函数函数调用动作来计算result,会创建4个暂时性矩阵,每一个用来存储对operator*的调用结果。若使用于TMP相关的模板技术,即expression template,就有可能消除那些临时对象并合并循环,这一切都无需改变客户端的用法,于是TMP软件使用更少 的内存,执行速度也更快。
(3)可以生成客户定制的设计模式(custom design pattern)实现品。设计模式如Strategy,Observer,Visitor等都可以通过多种方式实现出来,运用所谓的policy-base design的TMP-base技术,有可能产生一些templates用来表述独立的设计选项(所谓“policies”),然后我们可以任意结合它们,导致模式实现品带着客户定制的行为。
TMP语法不直观,其支持工具目前还不充分,由于TMP是一个在相对短时间之前才意外发现的语言,其编程方式开多少需要依赖经验。
TMP或许永远不会成为主流,但对于某些程序员-特别是程序开发人员-几乎确定会成为他们的主要粮食。
注意:
(1)模板元编程(Template metaprogramming,TMP)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
(2)TMP可被用来生成“基于政策选择组合(based on combinations of policy choices)”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。