接下来一个主题就是元编程
元编程那个词叫metaprogramming
什么叫元编程呢
就是利用模板可以进行
编译期计算的特性
来进行程序设计
这个编译期的计算
可以是一个数值计算
也可以是一个型式计算
也可以是代码计算 相当复杂的特性
使用模板的元编程机制
可以编写一些非常有趣的程序代码
那么我们就要问
为什么可以进行元编程
首先同学们要知道
C++ 本身是两层语言
上面一层 下面一层
下面一层就是我们标准的语言实现逻辑
就是在执行期间运行我们的程序代码
这段代码 我们称它为动态代码
程序运行了 我就执行你的代码
然后我就得到结果 这叫动态代码
还有一层就是它的顶层
它执行的是静态代码
这些代码 编译你的程序的时候替你算
算完了以后得到这个结果 就这个意思
就是执行编译期计算的代码
我们称它为静态代码
执行运行期计算的代码
那就叫动态代码
而 C++ 它是一个两层语言
如果没有模板
我们只使用含参宏
也可以勉勉强强地认为 C++ 是两层语言
但是那个实在是有点名不副实
但是我们现在有了模板
那情况就不一样了
因为模板所能够完成的功能
要远远地大于含参宏
模板最主要地就是可以用于函数式编程
我们前面其实说过
对于程序设计而言
程序员的这个思维方式实际上是两种
一种是结构化的 一种就是函数式的
结构化的负责完成我们的底层代码
我们的任务的最底层的划分
它的底层代码的构建
每条指令该做什么 第一条指令做什么
第二条指令做什么 第三条指令做什么
怎么组装它们 这个构造就是结构化的
可是顶层的实现逻辑呢
我必须完成我的应用程序的模块化
完成库的架构 而这个架构
理论上我们所有的思考问题的出发点
和归宿都是一个又一个的函数
所以这个思维模式本身是函数式的
它的实现策略 函数之间的调用的关系
它的那个处理的方式和结构化的
那个方式是不一样的
当我们只有 C 语言的时候
我们只支持结构化的程序设计
那么连我们写函数写库的思维模式
都是结构化的
当我们有了面向对象架构以后
为了完成数据封装与信息隐藏
我们有了类 我们有了继承 有了多态
这样的架构依然是结构化的
面向对象的代码实现也依然是结构化的
同学们一定要清楚这一点
而只有模板的实现
它不是结构化的 它是函数式的
来了一个问题 你解决这个问题
解决这个问题的思考的出发点
它是一个归结为一个函数
接着一个函数的调用的那个动作
普遍使用递归来控制程序的流程
而不是使用循环
这个思维模式是有一个巨大的变化的
我们很少关注于细节
而关注一个整体的模块结构
这个就叫函数式程序设计
函数式设计是一个相当相当重要的一个策略
在 C++11 里面
不是提供了 λ 表达式吗
那个东西 它其实也是函数式设计
当然了 说到这里
我要特别指出一下 在此之前
我们从来没提过 λ 表达式吧
我讲了 C++11 的特性了吧
但是我并没有提 λ 表达式
为啥呀 因为 λ 表达式本身是非常强大的一个工具
但是和 C/C++ 代码的实际编写的那个流程、
底层的结构化的思维模式是格格不入的
把它添进去是四不像
它所带来的好处没有它带来的问题多
让程序员做思维转换
在这个上面会吃大苦头的
程序员很难在编程级别完成这个思维转换
除非使用的是一个完全不同的模板架构
可以做这个事儿
强烈支持模板的函数式程序设计
但是我强烈地反对在程序代码中
提供函数式的功能
如果我真地需要函数式编程
有的是函数式编程语言可以供你选择
不是使用 C++ 的 λ 表达式
已经证明 模板是图灵完备的
非常理论的一个结论
什么意思 就是说理论上
你可以使用 C++ 的模板解决任何问题
所有的程序代码都可以用模板来实现它 没问题
函数式语言能够解决所有的问题
就是这个意思
强大功能就体现在这个地方
那么我们为什么需要元编程呢
就是编译期间
你如果进行一些特定的计算
可以让我们的代码更通用 更易用
从而能够提升我们程序的运行效率
但是元编程确实有它自己的独有的缺点
因为 C++ 本身并不是为元编程所设计的
它只是提供模板以后产生的
一个附加的功能
甚至在 C++ 引入模板的时候
都没有想过 C++ 可以完成元编程
所以 它没有为元编程这个东西
做特定的优化和设计
所以用 C++ 写元编程的代码
相对结构化的编程
编程效率是低的 编译效率超低
编译所耗费的时间太长了 超低
代码写出来以后 丑陋不堪
难看 阅读难、调试难、维护难
非常容易导致代码膨胀
出了问题你还调试不出来
经常会出现这种情况
那么再讲一点好的 元编程可以做什么
我们刚才不讲了吗 它是图灵完备的
实际上什么东西都可以做
那么实际上它可以做 比如说数值计算
我们这里边有些例子
比如说素性判定、控制结构、循环展开、
型式判定、表达式编译期多态、
特性、策略、标签、元容器、
等等等等 都可以做
一堆呀 都可以做
不过对于我们系统编程来讲
元编程的意义不太大
一般用到的场合是非常非常受限的
我们看两个例子
第一个例子是斐波那契数列
定义一个类模板
template〈int i = 1〉 class Fibonacci
这个模板定义有点特殊
它的模板参数不是 typename T
而是固定成整型的 i
也就是一个值参数 而不是型式参数
缺省值是 1 就是这个意思
这个类里边 只有一个公有的枚举值 value
连这个枚举型的名字 我们都没写
也就是只有一个枚举值 value
value 被我们设定成
Fibonacci〈i-1〉::value + Fibonacci〈i-2〉::value
啥意思呀 就是你传来一个 i
我求这个 它对应的第 i 项斐波那契数列
它是几 那么它是几呀 我们就算
这不就是递归式子嘛
那我们就把这个 value 的值
用递归的方式设定两者之和
就是我们 i 这个体化版的
斐波那契数列值了 对吧
这就是一个递归的定义
写在类模板里
公开的一个枚举值 这样一个文字
外界就可以访问 value
那么你就可以直接取 value 的值
就可以了
当编程运行的时候
如果它发现了一个 Fibonacci
像这段代码 Fibonacci〈1〉、Fibonacci〈2〉、
Fibonacci〈3〉、Fibonacci〈4〉
我们看 Fibonacci〈3〉
如果想算 Fibonacci〈3〉
它就用 3 作为 i 的值
传给这个类模板去体化它
体化出来的结果就是
要获取这个 value 的值
具体值是多少
编译的时候它还要去算这个事情
想算 Fibonacci〈3〉的 value 值
那么它必须去算 Fibonacci〈2〉的 value 值
Fibonacci〈1〉的 value 值
把两者之和算出来
然后才能算出 value 来 它是这个模式
一旦它发现这里面有一个 Fibonacci〈2〉
这也是一个类模板的一个体化呀
它会继续体化它
同样的方式把 2 传进去体化它
它不就递归的体化这个类模板吗
一直到它能够得到明确的结论
这个体化是不会停的
当然不同的编译器对这个体化的深度
也就是所谓的模板体化深度
它的嵌套的深度有特定的要求
不同的编译器都不一样
一般 500 次、900 次、1000 次 这个意思
差不多像这样的一个深度
因为太深的话 编译时间受不了
就按照这个方式去算
就像我们写递归函数一样
你必须提供一个递归终止条件
递归终止条件 我们怎么做呢
我们就做类模板的特化
通过特化给出它的终止条件
template〈〉 class Fibonacci〈2〉｛ public：enum ｛ value = 1 ｝; ｝;
这是第二项的特化
同样地 第一项也要特化
除了 2 和 1 其他的都一模一样
完成这两个特化
就是斐波那契数列的终止条件
如果是 4 它就会体化4的版本
想算它的 value 值
它就必须去体化 3 的版本和 2 的版本
2 的版本直接有特化版
它就不再体化了
3 的版本它会进一步去体化
它的 2 的版本和 1 的版本
而 2 的版本和 1 的版本呢
它也找到了特化的版本
所以它也就不体化了
就算出来了第 4 项的斐波那契数列的值
你一输出 正确结果就会显示出来
这是第一个例子