Fork me on GitHub

读书笔记 effective c++ Item 48 了解模板元编程

1. TMP是什么?

模板元编程(template metaprogramming TMP)是实现基于模板的C++程序的过程,它能够在编译期执行。你可以想一想:一个模板元程序是用C++实现的并且可以在C++编译器内部运行的一个程序,它的输出——从模板中实例化出来的C++源码片段——会像往常一样被编译。

2. 使用TMP的优势

如果这没有冲击到你,是因为你没有足够尽力去想。

 

C++不是为了模板元编程而设计的,但是自从TMP早在1990年被发现之后,它就被证明是非常有用的,为了使TMP的使用更加容易,在C++语言和标准库中加入了一些扩展。是的,TMP是被发现的,而不是被发明。当模板被添加到C++中的时候TMP这个特性就被引入了。对于某些人来说所有需要做的就是关注如何以一种聪明的和意想不到的方式来使用它。

TMP有两种强大的力量。第一,它使得一些事情变得容易也即是说如果没有TMP,这些事情做起来很难或者不可能实现第二,因为模板元编程在C++编译期执行,它们可以将一些工作从运行时移动到编译期。一个结果就是一些原来通常在运行时能够被发现的错误,现在在编译期就能够被发现了。另外一个结果就是使用TMP的C++程序在基本上每个方面都更加高效:更小的执行体,更短的运行时间,更少的内存需求。(然而,将工作从运行时移到编译期的一个后果就是编译时间增加了。使用TMP的程序比没有使用TMP的程序可能消耗更长的时间来进行编译。)

3. 如何使用TMP?

3.1 再次分析Item 47中的实例

考虑在Item 47中为STL的advance写出来的伪代码。我已经为伪代码部分做了粗体:

 1 template<typename IterT, typename DistT>
 2 void advance(IterT& iter, DistT d)
 3 {
 4 if (iter is a random access iterator) {
 5 
 6 iter += d;                           // use iterator arithmetic
 7 
 8 }                                        // for random access iters
 9 
10 else {                                
11 
12 
13 if (d >= 0) { while (d--) ++iter; } // use iterative calls to
14 else { while (d++) --iter; } // ++ or -- for other
15 } // iterator categories
16 }

 

我们可以使用typeid替换伪代码,让程序能够执行。这就产生了一个“普通的”C++方法——也就是所有工作都在运行时开展的方法:

 1 template<typename IterT, typename DistT>
 2 void advance(IterT& iter, DistT d)
 3 {
 4 if ( typeid(typename std::iterator_traits<IterT>::iterator_category) ==
 5 typeid(std::random_access_iterator_tag)) {
 6 
 7 iter += d;                           // use iterator arithmetic
 8 
 9 }                                        // for random access iters
10 
11 else {                               
12 
13 
14 if (d >= 0) { while (d--) ++iter; } // use iterative calls to
15 else { while (d++) --iter; } // ++ or -- for other
16 } // iterator categories
17 }

 

Item 47指出这种基于typeid的方法比使用trait效率更低,因为通过使用这种方法,(1)类型测试发生在运行时而不是编译期(2)执行运行时类型测试的代码在运行的时候必须可见。事实上,这个例子也展示出了为什么TMP比一个“普通的”C++程序更加高效,因为traits方式属于TMP。记住,trait使得在类型上进行编译期if…else运算成为可能。

我已经在前面提到过一些东西说明其在TMP中比在“普通”C++中更加容易,Item 47中也提供了一个advance的例子。Item 47中提到了advance的基于typeid的实现会导致编译问题,看下面的例子:

1 std::list<int>::iterator iter;
2 ...
3 advance(iter, 10);               // move iter 10 elements forward;
4 // won’t compile with above impl.

 

考虑为上面调用所产生的advance的版本,将模板参数IterT和DistT替换为iter和10的类型之后,我们得到下面的代码:

 1 void advance(std::list<int>::iterator& iter, int d)
 2 {
 3 if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) ==
 4 typeid(std::random_access_iterator_tag)) {
 5 
 6 iter += d;
 7 
 8 // error! won’t compile
 9 
10 
11 }
12 else {
13 if (d >= 0) { while (d--) ++iter; }
14 else { while (d++) --iter; }
15 }
16 }

 

有问题的是高亮部分,就是使用+=的语句。在这个例子中,我们在list<int>::iterator上使用+=,但是list<int>::iterator是一个双向迭代器(见Item 47),所以它不支持+=。只有随机访问迭代器支持+=。现在,我们知道了+=这一行将永远不会被执行到,因为为list<int>::iteraotr执行的typeid测试永远都不会为真,但是编译器有责任确保所有的源码都是有效的,即使不被执行到,当iter不是随机访问迭代器“iter+=d”就是无效代码。将它同基于tratis的TMP解决方案进行比较,后者把为不同类型实现的代码分别放到了不同的函数中,每个函数中进行的操作只针对特定的类型。

3.2 TMP是图灵完全的

TMP已经被证明是图灵完全的(Turing-Complete),这也就意味着它足够强大到可以计算任何东西。使用TMP,你可以声明变量,执行循环,实现和调用函数等等。但是这些概念同“普通”C++相对应的部分看起来非常不同。例如,Item 47中if…else条件在TMP中是如何通过使用模板和模板特化来表现的。但这是程序级别(assembly-level)的TMP。TMP库(例如,Boost MPL,见Item 55)提供了更高级别的语法,这些语法不会让你误认为是“普通的”C++。

3.3 TMP中的循环通过递归来实现

再瞥一眼事情在TMP中是如何工作的,让我们看一下循环。TMP中没有真正的循环的概念,所以循环的效果是通过递归来完成的。(如果一提到递归你就不舒服,在进入TMP 冒险之前你就需要处理好它。TMP主要是一个函数式语言,递归对于函数式语言就如同电视对美国流行文化一样重要:它们是不可分割的。)即使是递归也不是普通的递归,因为TMP循环没有涉及到递归函数调用,所涉及到的是递归模板实例化(template instantiations)。

TMP的“hello world”程序是在编译期计算阶乘。它算不上是令人激动的程序,“hello world”也不是,但是这两个例子对于介绍语言都是有帮助的。TMP阶乘计算通过对模板实例进行递归来对循环进行示范。也同样示范了变量是如何在TMP中被创建和使用的,看下面的代码:

 1 template<unsigned n>          // general case: the value of
 2 
 3 struct Factorial {                   // Factorial<n> is n times the value
 4 
 5 
 6 // of Factorial<n-1>
 7 enum { value = n * Factorial<n-1>::value };
 8 };
 9 template<> // special case: the value of
10 struct Factorial<0> { // Factorial<0> is 1
11 enum { value = 1 };
12 };

 

考虑上面的模板元编程(真的仅仅是单一的元函数Factorial),你通过引用Factorial<n>::value来得到factorial(n)的值。

代码的循环部分发生在模板实例Factorial<n>引用模板实例Factorial<n-1>的时候。像所有递归一样,有一种特殊情况来让递归终止。在这里是模板特化Factorial<0>。

每个Factorial模板的实例都是一个结构体,每个结构体使用enum hack(Item 2)来声明一个叫做value的TMP变量。Value持有递归计算的当前值。如果TMP有一个真正的循环结构,value将会每次循环的时候进行更新。既然TMP使用递归模板实例来替换循环,每个实例会得到它自己的value的拷贝,每个拷贝都会有一个和“循环”中位置想对应的合适的值。

你可以像下面这样使用Facorial:

1 int main()
2 {
3 std::cout << Factorial<5>::value; // prints 120
4 
5 std::cout << Factorial<10>::value;       // prints 3628800
6 
7 }        

                                             

 如果你认为这比冰激凌更酷,你就已经获得模板元程序员需要的素材。如果模板和特化,递归实例和enum hacks,还有像Factorial<n-1>::value这样的输入使你毛骨悚然,你还是一个“普通的”C++程序员。

3.4 TMP还能够做什么?

当然,Factorial对TMP的功能进行了示范,如同“hello world”程序对任何传统编程语言的功能进行示范一样。为了让你明白为什么TMP是值得了解的,知道它能够做什么很重要,这里有三个例子:

  • 确保因次单位(dimensional unit)的正确性。在科学和工程应用中,把因次单位(例如,质量,距离和时间)正确的拼到一起是很必要的。将表示质量的变量赋值给表示速度的变量是错误的,但是用距离变量除以时间变量然后将结果赋值被速度变量就没有问题。通过使用TMP,确保(在编译期间)程序中的所有因次单元组合的正确性就是可能的,不管计算有多复杂。(这也是使用TMP来侦测早期错误的一个例子。)TMP这种用法的一个有趣的方面是它能够支持分数因次的指数。这需要在编译期间将分数简化,然后编译器才能够确认,例如,单元 time1/2同time4/8是相同的。
  • 优化矩阵操作。Item 21中解释了有一些函数(包括 operator*)必须返回新的对象,Item 44中引入了SquareMatrix类,考虑下面的代码:

    1 typedef SquareMatrix<double, 10000> BigMatrix; 2 BigMatrix m1, m2, m3, m4, m5; // create matrices and 3 ... // give them values 4 BigMatrix result = m1 * m2 * m3 * m4 * m5; // compute their product

     用“普通的”方式来计算result会有四次创建临时matrice对象的调用,每次调用都应用在对operator*调用的返回值上面。这些独立的乘法在矩阵元素上产生了四          次循环。使用TMP的高级模板技术——表达式模板(expression templates),来消除临时对象以及合并循环是有可能的,并且不用修改上面的客户端代码的语法。最   后的程序使用了更少的内存,而且运行速度会有很大的提升。

  • 产生个性化的设计模式实现。像策略模式,观察者模式,访问者模式等等这些设计模式能够以很多方式被实现。使用基于模板的技术被叫做policy-based设计,我们可以创建表示独立设计选择(choice或者叫”policies”)的 模板,这些模板可以以任意的方式进行组合来产生个性化的模式实现。例如,使用这种技术能够创建一些实现智能指针行为策略(policies)的模板,使用它能够产生(在编译期)上百种不同的智能指针类型。这项技术已经超越了编程工艺领域,如设计模式和智能指针,它成为了生殖编程(generative programming)的基础。

4. TMP现状分析

TMP并不是为每个人准备的。因为语法不直观,支持的相关工具也很弱。(像为模板元编程提供的调试器。)作为一个“突然性“的语言它只是最近才被发现的,TMP编程的一些约定正在实验阶段。然而通过将工作从运行时移到编译期所带来的效率提升带给人很深刻的印象,对一些行为表达的能力(很难或者不可能在运行时实现)也是很吸引人的。

对于TMP的支持正在上升期。很可能下个版本的C++就是显示的支持它。TR1中已经支持了(Item 54)。关于这个主题的书籍已经开始出来了,网上的一些关于TMP信息也越来越多。TMP可能永远不会成为主流,但是对于一些程序员来说——尤其是程序库的实现者——几乎必然会成为主要手段。

5. 总结

  • 模板元编程可以将工作从运行时移到编译期,这样可以更早的发现错误,并且提高运行时性能。
  • 基于策略选择(policy choices)的组合TMP能够被用来产生个性化的代码,也能够用来防止为特定类型生成不合适的代码。
posted @ 2017-04-15 12:54  HarlanC  阅读(852)  评论(1编辑  收藏  举报