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

1059 篇文章 285 订阅

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

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

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

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

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

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

class StackInt
{
public:
    void push(int v);
    int pop();
    int Find(int x)
    {
        for(int i = 0; i < size; ++i)
        {
            if(data[i] == x) { return i; }
        }
    }
    // ... 其他代码 ...
};

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

class StackFloat
{
public:
    void push(float v);
    float pop();
    int Find(float x)
    {
        for(int i = 0; i < size; ++i)
        {
            if(data[i] == x) { return i; }
        }
    }
    // ... 其他代码 ...
};

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

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

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

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

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

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

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

template <typename T>
class Stack
{
public:
    void push(T v);
    T pop();
    int Find(T x)
    {
        for(int i = 0; i < size; ++i)
        {
            if(data[i] == x) { return i; }
        }
    }
    // ... 其他代码 ...
};

typedef Stack<int>   StackInt;
typedef Stack<float> StackFloat;

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

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

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

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

Int8,16: N/A
Int32  : VInt32Mul(int32x4, int32x4)
Int64  : VInt64Mul(int64x4, int64x4)
Float  : VInt64Mul(floatx2, floatx2)

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

for(v4a, v4b : vectorsA, vectorsB)
{
    if type is Int8, Int16
        VInt32Mul( ConvertToInt32(v4a), ConvertToInt32(v4b) )
    elif type is Int32
        VInt32Mul( v4a, v4b )
    elif type is Float
        ...
}

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

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

GenericMul(int8x4,  int8x4);
GenericMul(int16x4, int16x4);
GenericMul(int32x4, int32x4);
GenericMul(int64x4, int64x4);
// 其它 Generic Mul ...

for(v4a, v4b : vectorsA, vectorsB)
{
    GenericMul(v4a, v4b);
}

这样不就可以了吗?

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

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值