模板元编程(TMP)

元编程是什么

元編程(英語:Metaprogramming),又称超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的资料,或者在运行时完成部分本应在编译时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。

泛泛来说,可扩展程序自身,这样的能力,为元编程。——比如,若编程甲可以输出 A - Z,那么写程序甲算「编程」;而程序乙可以生成程序甲(也许还会连带着运行它输出 A - Z),那么编写程序乙的活动,就可以算作 meta-programming,「元编程」。注意,程序甲和程序乙并不一定是同一种语言。

编写元程序的语言称之为元语言(程序甲)。被操纵的程序的语言称之为「目标语言」(程序乙)。一门编程语言同时也是自身的元语言的能力称之为「反射」或者「自反」。

元编程通常通过两种方式实现。一种是通过应用程序编程接口(APIs)将运行时引擎的内部信息暴露于编程代码。另一种是动态执行包含编程命令的字符串表达式。因此,“程序能够编写程序”。虽然两种方式都能用于同一种语言,但大多数语言趋向于偏向其中一种。

下面是一个简单的使用bash脚本编写的元程序示例,同时也是一个生成式编程的例子:

#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=99; I++)) do
    echo "echo $I" >>program
done
chmod +x program

输出结果: 

#!/bin/bash
echo 1
echo 2
...
echo 99

这个脚本(或程序)生成了一个新的99行的程序来打印输出数字1至99。这只是一个怎样用代码来编写更多代码的示例;但并不是打印一串数字最有效的方法。尽管如此,一个程序员可以在几分钟内编写和执行这个元程序,却生成了近100行的代码。

所以 metaprogramming 更狭义的意思应该是指「编写能改变语言语法特性或者运行时特性的程序」。换言之,一种语言本来做不到的事情,通过你编程来修改它,使得它可以做到了,这就是元编程。本版同文提及 method_missing,那么 Wat — Destroy All Software Talks 之中给出了运行时元编程的经典范例:

>> ruby has no bare words
NameError: undefined local variable or method `words' for main:Object
        from (irb) 1
>> def method_missing(*args); args.join(" "); end
=> nil
>> ruby has bare words
=> "ruby has bare words"
>> bare words can even have bangs!
=> "bare words can even have bangs!"

最常见的元编程工具是编译器,它可以将程序员使用高级语言编写的相对短小的程序转换为等价的汇编语言或者机器语言程序。这是最基础的编程工具,在大多数情况下,直接编写机器语言程序是不太现实的。

编译器能够将一种语言转换为另一种,而其它元编程系统则允许以编程方式操纵一种语言。宏系统即是这样一种简单的系统。在 Microsoft Office© 的程序中,宏可以记录一些特定的按键组合,并重新执行。另一方面,这些可执行代码可以通过点击宏选择界面的“编辑”按钮获得。

自产生程序是一种源代码等于输出的特殊的元程序,其指的是輸出結果為程序自身源码的程序。面向语言的程序设计是一种强烈关注元编程的编程风格,通过领域特定语言来实现。

 

模板元编程的背景

模板元编程(Template Metaprogramming,TMP)就是利用模板来编写那些在编译时运行的C++程序。 模板元程序(Template Metaprogram)是由C++写成的,运行在编译器中的程序。当程序运行结束后,它的输出仍然会正常地编译。

C++并不是为模板元编程设计的,但自90年代以来,模板元编程的用处逐渐地被世人所发现。

  • 模板编程提供的很多便利在面向对象编程中很难实现;
  • 程序的工作时间从运行期转移到编译期,可以更早发现错误,运行时更加高效。
  • 在设计模式上,可以基于不同的策略,自动组合而生成具体的设计模式实现。

C++ 模板元编程主要用来操纵类型,虽然从形式上说,值也是类型。

【类型和值是同一种东西】这句话怎么理解?我就这么说吧,如果一个语言有int、int64、float、double四种数据类型,然后你分别为他们实现一个求开方的函数sqrt,这样你就有4个sqrt了。很多语言到这一步都是这样的。(以下部分内容来源轮子哥-vczh)

好了,现在我们给它加上泛型,然后写出下面的一个函数:

void PrintLength<T>(T x, T y)
{
    var length = sqrt(x*x + y*y);
    Print(length);
}

怎么样,很自然吧。然后你把自己当成编译器,判断一下这个函数有没有写错。你怎么办?

遇到困难了吧。如果sqrt是独立的那四个,那我怎么保证T一定就是int、int64、float、double呢?

到目前为止,这只有两种方法。第一种是使用concept mapping。我可以给类型定义一个“CanSqrt”的concept:

concept<T> CanSqrt
{
    T sqrt(T t);
}

然后我分别实现他们:

instance CanSqrt<int>
{
    int Sqrt(int t) ...;
}
instance CanSqrt<int64> ...
instance CanSqrt<float> ...
instance CanSqrt<double> ...

然后把PrintLength改成这样,目标就是让编译器知道,凡是想使用PrintLength的人给的T必须是实现过CanSqrt的:

void PrintLength<T>( ... )
    where CanSqrt<T>
{
    ....
}

是不是为了代码可以检查(这是一个前提条件,所有静态类型语言都是这样的),就给加上了很多奇怪的概念了?其实这也很常见,Haskell是这样的,Rust也是这样的,C++即将也要这样了(不过个人觉得C++加上concept意义几乎没有)。

那C++是怎么做的呢?简单粗暴,就是你调用PrintLength的时候,它再根据你提供的类型分析一下函数体有没有写错……这基本上就是C++的template的想法。

那既然template可以用在函数上,那能不能用在类型上呢?这是必须的啊,不然vector<T>啊list<T>啊map<K, V>怎么来啊?

那既然类型可以有内部类型:

struct Fuck
{
    struct Shit{};
};

那我给Fuck加上了一个template<typename T>,能不能让不同的Fuck里面的Shit是不同的呢?C#说,不可以。C++说,可以。于是我们有了偏特化。

其实只要有偏特化,template就是图灵完备的了。虽然你看很多模板元编程都是告诉你如何在尖括号里面加入数字然后来操作它。但是你不要忘记,伟大的数学先驱Church告诉我们,这个世界上有Church Number,所以我们可以用Zero来代表0,Next<Zero>代表1,Next<Next<Zero>>代表2,Next<Next<Next<Zero>>>代表3,然后自己简单的做一个加减乘除,做出了完全一致的东西……就是写起来烦一点而已。

到了这里你应该可以体会到,其实图灵完备的门槛是很低的,你只需要自己

  • 设计个结构(对于C++来说就是“类型有名字”的这个事情
  • 随便加上一个pattern matching(对于C++模板就是偏特化
  • 然后允许这个结构递归的话(对于C++来说,就是说类型参数也可以是模板实例化后的结果,譬如说vector<shared_ptr<Fuck>>)

他就图灵完备了。

好了,我说了这么多废话,无非就是想告诉你:

1:如果C++的template不这么做的话,那他就要加上concept mapping,更复杂了。

2:C++的template选择了今天的做法,一不小心就图灵完备了。

 模板元编程意义

C++ 模板最初是为实现泛型编程设计的,但人们发现模板的能力远远不止于那些设计的功能。

模板的使命很简单:为自动代码生成提供方便。提高程序员生产率的一个非常有效的方法就是“代码复用”,而面向对象很重要的一个贡献就是通过内部紧耦合和外部松耦合将“思想”转化成一个一个容易复用的“概念”。但是面向对象提供的工具箱里面所包含的继承,组合与多态并不能完全满足实际编程中对于代码复用的全部要求,于是模板就应运而生了。

模板元编程其实就是复杂点儿的模板,简单的模板在特化时基本只包含类型的查找与替换,这种模板可以看作是“类型安全的宏”。而模板元编程就是将一些通常编程时才有的概念比如:递归,分支等加入到模板特化过程中的模板,但其实说白了还是模板,自动代码生成而已。C++ 模板元编程是“意外”功能,而不是设计的功能,这也是 C++ 模板元编程语法丑陋的根源。

C++ 模板是图灵完备的,这使得 C++ 成为两层次语言(two-level languages),其中,执行编译计算的代码称为静态代码(static code),执行运行期计算的代码称为动态代码(dynamic code),C++ 的静态代码由模板实现(预处理的宏也算是能进行部分静态计算吧,也就是能进行部分元编程,称为宏元编程,见 Boost 元编程库即 BCCL。

理论上说 C++ 模板可以执行任何计算任务,但实际上因为模板是编译期计算,其能力受到具体编译器实现的限制(如递归嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。

具体来说 C++ 模板可以做以下事情:编译期数值计算、类型计算、代码计算(如循环展开),其中数值计算实际不太有意义,而类型计算和代码计算可以使得代码更加通用,更加易用,性能更好(也更难阅读,更难调试,有时也会有代码膨胀问题)。

从编程范型(programming paradigm)上来说,C++ 模板是函数式编程(functional programming),它的主要特点是:函数调用不产生任何副作用(没有可变的存储),用递归形式实现循环结构的功能。C++ 模板的特例化提供了条件判断能力,而模板递归嵌套提供了循环的能力,这两点使得其具有和普通语言一样通用的能力(图灵完备性)。

编程形式来看,模板的“<>”中的模板参数相当于函数调用的输入参数,模板中的 typedef 或 static const 或 enum 定义函数返回值(类型或数值,数值仅支持整型,如果需要可以通过编码计算浮点数),代码计算是通过类型计算进而选择类型的函数实现的(C++ 属于静态类型语言,编译器对类型的操控能力很强)。代码示意如下:

#include <iostream>

template<typename T, int i=1>
class someComputing {
public:
    typedef volatile T* retType; // 类型计算
    enum { retValume = i + someComputing<T, i-1>::retValume }; // 数值计算,递归
    static void f() { std::cout << "someComputing: i=" << i << '\n'; }
};
template<typename T> // 模板特例,递归终止条件
class someComputing<T, 0> {
public:
    enum { retValume = 0 };
};

template<typename T>
class codeComputing {
public:
    static void f() { T::f(); } // 根据类型调用函数,代码计算
};

int main(){
    someComputing<int>::retType a=0;
    std::cout << sizeof(a) << '\n'; // 64-bit 程序指针
    // VS2013 默认最大递归深度500,GCC4.8 默认最大递归深度900(-ftemplate-depth=n)
    std::cout << someComputing<int, 500>::retValume << '\n'; // 1+2+...+500
    codeComputing<someComputing<int, 99>>::f();
    std::cin.get(); return 0;
}

从这个例子我们也可以窥探 C++ 模板元编程的函数式编程范型,对比结构化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改变存储(即变量 sum)的方式来对计算过程进行编程,模板元程序没有可变的存储(都是编译期常量,是不可变的变量),要表达求和过程就要用很多个常量:someComputing<int, 0>::retValume,someComputing<int, 1>::retValume,...,someComputing<int, 500>::retValume 。函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰(前提是熟悉这种方式),没有可变的变量就没有数据依赖,方便进行并行化。
 

TMP的用途

为了更好地理解TMP的重要性,我们来看看TMP能干什么:

  1. 确保量纲正确。在科学计算中,量纲的结合要始终保持正确。比如一定要单位为”m”的变量和单位为”s”的变量相除才能得到一个速度变量(其单位为”m/s”)。 使用TMP时,编译器可以保证这一点。因为不同的量纲在TMP中会被映射为不同的类型。
  2. 优化矩阵运算。比如矩阵连乘问题,TMP中有一项表达式模板(expression template)的技术,可以在编译期去除临时变量和合并循环。 可以做到更好的运行时效率。
  3. 自定义设计模式的实现。设计模式往往有多种实现方式,而一项叫基于策略设计(policy-based design)的TMP技术可以帮你创建独立的设计策略(design choices),而这些设计策略可以以任意方式组合。生成无数的设计模式实现方式。

参考文章:

维基百科-元编程:https://zh.wikipedia.org/wiki/%E5%85%83%E7%BC%96%E7%A8%8B

知乎-C++ 模板元编程的应用有哪些,意义是什么?:https://www.zhihu.com/question/21656266

Cnblog-C++模板元编程(C++ template metaprogramming):https://www.cnblogs.com/liangliangh/p/4219879.html

Harttle.land-Effective C++ 48:了解模板元编程 :https://harttle.land/2015/09/16/effective-cpp-48.html

Scott Meyers-感受 template metaprogramming(模板元编程):https://wizardforcel.gitbooks.io/effective-cpp/content/50.html

发布了119 篇原创文章 · 获赞 152 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/weixin_40539125/article/details/96773678
tmp