Item 48: 感受 template metaprogramming(模板元编程)

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

template metaprogramming (TMP)(模板元编程)是写 template-based(基于模板)的运行于编译期间的 C++ 程序的过程。考虑一下:一个 template metaprogram(模板元程序)是用 C++ 写的运行于 C++ 编译器中的程序。当一个 TMP 程序运行完成,它的输出——从 templates(模板)实例化出的 C++ 源代码片断——随后被正常编译。

如果你仅把它看作古怪的特性而没有打动你,那你就不会对它有足够的深入的思考。

C++ 并不是为 template metaprogramming(模板元编程)设计的,但是自从 TMP 在 1990 年代早期被发现以来,它已被证明非常有用,使 TMP 变容易的扩展很可能会被加入到语言和它的标准库之中。是的,TMP 是被发现,而不是被发明。TMP 所基于的特性在 templates(模板)被加入 C++ 的时候就已经被引进了。所需要的全部就是有人注意到它们能够以一种精巧的而且意想不到的方式被使用。

TMP 有两个强大的力量。首先,它使得用其它方法很难或不可能的一些事情变得容易。第二,因为 template metaprograms(模板元程序)在 C++ 编译期间执行,它们能将工作从运行时转移到编译时。一个结果就是通常在运行时才能被察觉的错误能够在编译期间被发现。另一个结果是使用了 TMP 的 C++ 程序在以下几乎每一个方面都可能更有效率:更小的可执行代码,更短的运行时间,更少的内存需求。(然而,将工作从运行时转移到编译时的一个结果就是编译过程变得更长。使用 TMP 的程序可能比它们的 non-TMP 对等物占用长得多的编译时间。)

考虑 228 页引入的 STL 的 advance 的伪代码。(在 Item 47。你现在可能需要读那个 Item,因为在本 Item 中,我假设你已经熟悉了那个 Item 的内容。)就像 228 页,我突出表示代码中的伪代码部分:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  if (iter is a random access iterator) {
    iter += d;                                     // use iterator arithmetic
  }                                                // for random access iters
  else {
    if (d >= 0) { while (d--) ++iter; }            // use iterative calls to
    else { while (d++) --iter; }                   // ++ or -- for other
  }                                                // iterator categories
}

我们可以用 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;                                     // use iterator arithmetic
  }                                                 // for random access iters
  else {
    if (d >= 0) { while (d--) ++iter; }             // use iterative calls to
    else { while (d++) --iter; }                    // ++ or -- for other
  }                                                 // iterator categories
}

Item 47 指出这个 typeid-based(基于 typeid)的方法比使用 traits 的方法效率低,因为这个方法,(1)类型检测发生在运行时而不是编译期,(2)用来做运行时类型检测的代码必须出现在可执行代码中。实际上,这个例子展示了 TMP 如何能比一个“常规”C++ 程序更高效,因为 traits 方法是 TMP。记住,traits 允许编译时在类型上的 if...else 计算。

我先前谈及一些事情在 TMP 中比在“常规”C++ 中更简单,而 advance 提供了这方面的一个例子。Item 47 提到 advance 的 typeid-based(基于 typeid)的实现可能会导致编译问题,而这就是一个产生问题的例子:

std::list<int>::iterator iter;

...

advance(iter, 10);                          // move iter 10 elements forward;
                                            // won't compile with above impl.

考虑 advance 为上面这个调用生成的版本。用 iter 和 10 的类型取代 template parameters(模板参数)IterT 和 DistT 之后,我们得到这个:

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;                                        // error!
  }
  else {
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
  }
}

问题在突出显示的行,使用了 += 的那行。在当前情况下,我们试图在一个 list<int>::iterator 上使用 +=,但是 list<int>::iterator 是一个 bidirectional iterator(双向迭代器)(参见 Item 47),所以它不支持 +=。只有 random access iterators(随机访问迭代器)才支持 +=。此时,我们知道我们永远也不会试图执行那个 += 行,因为那个 typeid 检测对于 list<int>::iterators 永远不成立,但是编译器被责成确保所有源代码是正确的,即使它不被执行,而当 iter 不是一个 random access iterator(随机访问迭代器)时 "iter += d" 是不正确的。traits-based(基于 traits)的 TMP 解决方案与此对比,那里针对不同类型的代码被分离到单独的函数中,其中每一个都只使用了可用于它所针对的类型的操作。

TMP 已经被证明是 Turing-complete(图灵完备)的,这意味着它强大得足以计算任何东西。使用 TMP,你可以声明变量,执行循环,编写和调用函数,等等。但是这些结构看起来与其在“常规”C++ 中的样子非常不同。例如,Item 47 展示了 if...else 条件在 TMP 中是如何通过 templates(模板)和 template specializations(模板特化)被表达的。但那是 assembly-level(汇编层次)的 TMP。针对 TMP 的库(例如 Boost 的 MPL ——参见 Item 55)提供了一种更高层次的语法,虽然还不至于让你把它误认为是“常规”C++。

为了一窥其它东西在 TMP 中如何工作,让我们来看看 loops(循环)。TMP 中没有真正的 looping construct(循环结构),因此 loops(循环)的效果是通过 recursion(递归)完成的。(如果你对 recursion(递归)感到不舒服,在你斗胆进入 TMP 之前一定要解决它。TMP 很大程度上是一个 functional language(函数性语言),而 recursion(递归)之于 functional language(函数性语言)就像电视之于美国流行文化:是密不可分的。)然而,甚至 recursion(递归)都不是常规样式的,因为 TMP loops 不涉及 recursive function calls(递归函数调用),它们涉及 recursive template instantiations(递归模板实例化)。

TMP 的 "hello world" 程序在编译期间计算一个阶乘。它不是一个很令人兴奋的程序,不过,即使不是 "hello world",也有助于语言入门。TMP 阶乘计算示范了通过 recursive template instantiation(递归模板实例化)实现循环。它也示范了在 TMP 中创建和使用变量的一种方法。看:

template<unsigned n>                 // general case: the value of
struct Factorial {                   // Factorial<n> is n times the value
                                     // of Factorial<n-1>

  enum { value = n * Factorial<n-1>::value };

};

template<>                           // special case: the value of
struct Factorial<0> {                // Factorial<0> is 1
  enum { value = 1 };

};

给出这个 template metaprogram(模板元程序)(实际上只是单独的 template metafunction(模板元函数)Factorial),你可以通过引用 Factorial<n>::value 得到 factorial(n) 的值。

代码的循环部分出现在 template instantiation(模板实例化)Factorial<n> 引用 template instantiation(模板实例化)Factorial<n-1> 的地方。就像所有正确的 recursion(递归)有一个导致递归结束的特殊情况。这里,它就是 template specialization(模板特化)Factorial<0>。

Factorial template 的每一个 instantiation(实例化)都是一个 struct,而每一个 struct 都使用 enum hack(参见 Item 2)声明了一个名为 value 的 TMP 变量。value 用于持有阶乘计算的当前值。如果 TMP 有一个真正的循环结构,value 会在每次循环时更新。因为 TMP 在循环的位置使用 recursive template instantiation(递归模板实例化),每一个 instantiation(实例化)得到它自己的 value 的拷贝,而每一个拷贝拥有适合于它在“循环”中所处的位置的值。

你可以像这样使用 Factorial:

int main()
{
  std::cout << Factorial<5>::value;            // prints 120
  std::cout << Factorial<10>::value;           // prints 3628800
}

如果你觉得这比吃了冰淇淋还凉快,你就具有了一个 template metaprogrammer(模板元程序员)应有的素质。如果 templates(模板)以及 specializations(特化)以及 recursive instantiations(递归实例化)以及 enum hacks 以及对类似 Factorial<n-1>::value 这样的类型的需要使你毛骨悚然,好吧,你是一个不错的常规 C++ 程序员。

当然,Factorial 示范的 TMP 的效用大约就像 "hello world" 示范的任何常规编程语言的效用一样。为了领会为什么 TMP 值得了解,更好地理解它能做什么是很重要的。这里是三个示例:

  • Ensuring dimensional unit correctness(确保计量单位正确性)。在科学和工程应用中,计量单位(例如,质量,距离,时间,等等)被正确组合是基础。例如,将一个代表质量的变量赋值给一个代表速度的变量是一个错误,但是用一个时间变量去除距离变量并将结果赋给一个速度变量就是正确的。使用 TMP,不论计算多么复杂,确保(在编译期间)一个程序中所有计量单位组合都是正确的是有可能的。(这是一个如何用 TMP 进行早期错误诊断的例子。)这个 TMP 的使用的一个有趣的方面是能够支持分数指数。这需要这个分数在编译期间被简化以便于编译器能够确认,例如,单位 time1/2 与单位 time4/8 是相同的。

  • Optimizing matrix operations(优化矩阵操作)。Item 21 阐释了一些函数,包括 operator*,必须返回新的 objects,而 Item 44 引入了 SquareMatrix class,所以考虑如下代码:

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

    用“常规”方法计算 result 需要四个临时矩阵的创建,用于每一次调用 operator* 的结果。此外,独立的乘法产生了一个四次循环遍历矩阵元素的序列。使用一种与 TMP 相关的被称为 expression templates(表达式模板)的高级模板技术,完全不改变上面的客户代码的语法,而消除临时对象以及合并循环是有可能的。最终的软件使用更少的内存而且运行速度戏剧性地更快。

  • Generating custom design pattern implementations(生成自定义的设计模式实现)。像 Strategy(参见 Item 35),Observer,Visitor 等设计模式能用很多方法实现。使用一种被称为 policy-based design(基于 policy 设计)的 TMP-based(基于 TMP)的技术,使得创建代表独立的设计选择的 templates ("policies") 成为可能,这种 templates 能以任意的方法组合以产生带有自定义行为的模式实现。例如,这种技术经常用于允许几个实现了 smart pointer behavioral(智能指针行为)的 policies 的 templates 生成(在编译期间)数百个不同的 smart pointer(智能指针)类型。将类似设计模式和智能指针这样的编程器件的范围大大地扩展,这项技术是通常所说的 generative programming(产生式编程)的基础。

TMP 并不适合于每一个人。它的语法是不符合直觉的,工具支持也很弱(template metaprograms 的调试器?哈!)作为一个相对晚近才发现的“附属”语言,TMP programming 的规则仍然带有试验性质。然而,通过将工作从运行时转移到编译时所提供的效率提升还是能给人留下深刻的印象,而表达在运行时很难或不可能实现的行为的能力也相当有吸引力。

TMP 的支持程度在不断提升。很可能在 C++ 的下一个版本中将对它提供直接的支持,而且 TR1 已经这样做了(参见 Item 54)。关于这一主题的书籍也即将开始出版(目前,C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond 已经出版——译者注),而 web 上的 TMP 信息也正在保持增长。TMP 也许永远不会成为主流,但是对于某些程序员——特别是库开发者——它几乎必然会成为主料。

Things to Remember

  • template metaprogramming(模板元编程)能将工作从运行时转移到编译时,这样就能够更早察觉错误并提高运行时性能。

  • TMP 能用于在 policy choices 的组合的基础上生成自定义代码,也能用于避免为特殊类型生成不适当的代码。