C/C++编程:编程,元编程,模板元编程

野性酷女 2023-01-14 11:54 226阅读 0赞

问:付出更多的精力学习模板是否值得?

这个问题很功利,但是一针见血。因为技术的根本目的在于解决需求。那C++的模板能做什么?

一个高(树)大(新)上(风)的回答是,C++里面的模板,犹如C中的宏、C#和Java中的自省(restropection)和反射(reflection),是一个改变语言内涵,拓展语言外延的存在。

程序最根本的目的是什么?复现真实世界或人所构想的规律,减少重复工作的成本,或通过提升规模完成人所不能及之事。但是世间之事万千,有限的程序如何重现复杂的世界呢?

答案是“抽象”。论及具体手段,无外乎“求同”与“存异”:概括一般规律,处理特殊情况。这也是软件工程所追求的目标。一般规律概括的越好,我们所付出的劳动也就越少。
·

我们以数据结构举例。在程序里,你需要一些堆栈。这个堆栈的元素可能是整数、浮点或者别的什么类型。一份整型堆栈的代码可能是:

  1. class StackInt
  2. {
  3. public:
  4. void push(int v);
  5. int pop();
  6. int Find(int x)
  7. {
  8. for(int i = 0; i < size; ++i)
  9. {
  10. if(data[i] == x) {
  11. return i; }
  12. }
  13. }
  14. // ... 其他代码 ...
  15. };

如果你要支持浮点了,那么你只能将代码再次拷贝出来,并作如下修改:

  1. class StackFloat
  2. {
  3. public:
  4. void push(float v);
  5. float pop();
  6. int Find(float x)
  7. {
  8. for(int i = 0; i < size; ++i)
  9. {
  10. if(data[i] == x) {
  11. return i; }
  12. }
  13. }
  14. // ... 其他代码 ...
  15. };

当然也许你觉得这样做能充分体会代码行数增长的成就感。但是有一天,你突然发现:呀,Find 函数实现有问题了。怎么办?这个时候也许你只有两份这样的代码,那好说,一一去修正就好了。如果你有十个呢?二十个?五十个?

于是便诞生了新的技术,来消解我们的烦恼。

这个技术的名字,并不叫“模板”,而是叫“元编程”。

元(meta)无论在中文还是英文里,都是个很“抽象(abstract)”的词。因为它的本意就是“抽象”。元编程,也即是编程的抽象。用更好理解的说法就是,元编程意味着你写一段程序A,程序A会运行后生成另一个程序B,程序B才是真正实现功能的程序。那么这个时候程序A可以称作程序B的元程序,编写程序A的过程,就叫做元编程

回到我们的堆栈的例子。真正执行功能的,其实仍然是浮点的堆栈、整数的堆栈、各种你所需要的类型的堆栈。但是因为这些堆栈之间太相似了,仅仅有着些微的不同,我们为什么不能有一个将相似之处囊括起来,同时又能分别体现出不同之处的程序呢?很多语言都提供了这样的机会。C中的宏,C++中的模板,Python中的Duck Typing,广义上将都能够实现我们的思路。

我们的目的,是找出程序之间的相似性,进行“元编程”。而在C++中,元编程的手段,可以是宏,也可以是模板。

宏的例子姑且不论,我们来看一看模板:

  1. template <typename T>
  2. class Stack
  3. {
  4. public:
  5. void push(T v);
  6. T pop();
  7. int Find(T x)
  8. {
  9. for(int i = 0; i < size; ++i)
  10. {
  11. if(data[i] == x) {
  12. return i; }
  13. }
  14. }
  15. // ... 其他代码 ...
  16. };
  17. typedef Stack<int> StackInt;
  18. typedef Stack<float> StackFloat;

通过模板,我们可以将形形色色的堆栈代码分为两个部分,一个部分是不变的接口,以及近乎相同的实现;另外一部分是元素的类型,它们是需要变化的。因此同函数类似,需要变化的部分,由模板参数来反映;不变的部分,则是模板内的代码。可以看到,使用模板的代码,要比不使用模板的代码简洁许多。

如果元编程中所有变化的量(或者说元编程的参数),都是类型,那么这样的编程,叫做泛型

那么,模板的发明,仅仅是为了做和宏几乎一样的替换工作吗?可以说是,也可以说不是。一方面,很多时候模板就是为了替换类型,这个时候作用和宏其实没有什么区别。只是宏是基于文本的替换,被替换的文本本身没有任何语法。只有替换完成,编译器才能进行接下来的处理。而模板会在分析模板时以及实例化模板的时候都会进行检查,而且源代码中也能和调试符合意义对应,所以无论是编译时还是运行时,排错都相对简单

但是模板和宏也有很大的不同。模板最大的不同在于它是“可以运算”的。我们来举一个例子,不过可能有点牵强。考虑我们要写一个向量逐分量乘法。只不过这个向量,它非常的大。所以为了保证速度,我们需要使用SIMD指令进行加速。假设我们有以下指令可以使用:

  1. Int8,16: N/A
  2. Int32 : VInt32Mul(int32x4, int32x4)
  3. Int64 : VInt64Mul(int64x4, int64x4)
  4. Float : VInt64Mul(floatx2, floatx2)

所以对于Int8和Int16,我们需要提升到Int32,而Int32和Int64,各自使用自己的指令。所以我们需要实现下的逻辑:

  1. for(v4a, v4b : vectorsA, vectorsB)
  2. {
  3. if type is Int8, Int16
  4. VInt32Mul( ConvertToInt32(v4a), ConvertToInt32(v4b) )
  5. elif type is Int32
  6. VInt32Mul( v4a, v4b )
  7. elif type is Float
  8. ...
  9. }

这里的问题就在于,如何根据 type 分别提供我们需要的实现?这里有两个难点。首先, if(type == xxx) {} 是不存在于C++中的。第二,即便存在根据 type 的分配方法,我们也不希望它在运行时branch,这样会变得很慢。我们希望它能按照类型直接就把代码编译好,就跟直接写的一样。

嗯,聪明你果然想到了,重载也可以解决这个问题。

  1. GenericMul(int8x4, int8x4);
  2. GenericMul(int16x4, int16x4);
  3. GenericMul(int32x4, int32x4);
  4. GenericMul(int64x4, int64x4);
  5. // 其它 Generic Mul ...
  6. for(v4a, v4b : vectorsA, vectorsB)
  7. {
  8. GenericMul(v4a, v4b);
  9. }

这样不就可以了吗?

唔,你赢了,是这样没错。但是问题是,我这个平台是你可没见过,它叫 Deep Thought, 特别缺心眼儿,不光有 int8,还有更奇怪的 int9, int11,以及可以代表世间万物的 int42。你总不能为之提供所有的重载吧?这简直就像你枚举了所有程序的输入,并为之提供了对应的输出一样。

好吧,我承认这个例子还是太牵强了。不过相信我,在你阅读完第二章和第三章之后,你会将这些特性自如地运用到你的程序之中。你的程序将会变成体现模板“可运算”威力的最好例子。

发表评论

表情:
评论列表 (有 0 条评论,226人围观)

还没有评论,来说两句吧...

相关阅读

    相关 C/C++编程:hello 模板编程

    什么是模板元编程 模板元编程:编译系统将会执行我们所写的代码,来生成新的代码,而这些新代码才真正实现了我们所期望的功能。 通常, 模板元编程这个概念意味着一种反射的特性

    相关 C/C++编程:友模板

    友元声明的基本概念是很简单的:给予某个类或者函数访问友元声明所在的类的权利。然而,由于下面两个事实,这些简单概念却变得有些复杂: 友元声明可能是某个实体的唯一声明