c++ 预处理之正整型实现方法

所属分类: 软件编程 / C 语言 阅读数: 97
收藏 0 赞 0 分享

虽然通过一系列的奇技淫巧,让预处理也图灵完备一把,但是用预处理来做计算,真的很吃力不讨好。因为预处理一开始设计出来的目的,就没什么野心,原本就仅仅只是为了做简简单单的文本替换工作,并没有想过要成为正儿八经的编程语言,即便是最最缩水版脚本语言的功能要求都达不到。只是后来,实在是大量要求要批量自动生成代码,特别是c++11之前的版本玩什么模板元编程,铺天盖地的要有大量相似的代码。这些代码用其他工具来生成,当然形式会更加漂亮,但是始终还是用原生的预处理来做这种事情会更加的方便,否则每次修改,都要运行一遍外部工具,都麻烦啊!本人是倾向于用预处理来生成代码的。另外,c++11之后,的确原来很多需要宏来生成代码的场合已经不必要了,但是因为c++11的类型推导能力大大加强了之后,发现又有一大波地方可以用宏来生成代码了。并不是说C++中的宏是必不可少之物,但是用了宏,真的可以减少很多很多的重复代码,起码纸面上的代码清爽了很多。

预处理的原生数据类型就只有符号,然后符号只支持##的并接运算,同时,预处理也能识别并接后的结果(否则,并接运算就没意义了),如果是宏函数,就进行调用操作,如果是宏符号,就替换文本,如果什么都不是,就什么都不做,保留符号。但是这样的弱鸡类型,显然远远不能满足离经叛道的码猿需要。经过大量的宏编程的尝试之后,可以很肯定一点,预处理里面只能再模拟出来一种数据类型,那就是正整数,虽然通过补码运算来仿真负数,但是由于预处理里面的符号不能包含减号(-)字符,当然要花大力气捣鼓负整数也是可以的,只是使用上也不方便也不直观,性价比不高,基本上,必须用宏来生成代码的地方,都可以不需要负整数的。

另外,预处理也没有变量类型的概念,不要说强类型,就连弱类型也不是,完全就是无类型。正整数类型的概念全靠码猿人肉编译器来维护,一个循环的宏代码生成一般都是来来回回也不知道调用了多少层宏调用,任何一个地方出错,有时候是几吨密密麻麻的中间失败代码(编译器的预处理缓冲溢出,弃械投降),有时候就完全没有输出,没有任何一丁点的提示,简直是大海捞针的找问题。因此,在用宏循环生成代码时,必须小心翼翼,步步为营,不得不感慨,正儿八经语言里面的类型真是好东西啊。

其实,数据类型并不重要,重要的是数据上能够支持的运算集合以及这些运算能运用的场合。
好了,回到上文,我们用_ZPP_INC_N搞了10个数,通过复制粘贴,可以把N增加到255。实际运用中,完全足够用了。

#define _ZPP_INC_JOIN(_A, _B) _ZPP_INC_JOIN_IMP1(_A, _B)
#define _ZPP_INC_JOIN_IMP1(_A, _B) _ZPP_INC_JOIN_IMP2(~, _A##_B)
#define _ZPP_INC_JOIN_IMP2(p, res) res

#define PP_INC(x, ) _ZPP_INC_JOIN(_ZPP_INC_, x)
#define _ZPP_INC_0 1
#define _ZPP_INC_1 2
#define _ZPP_INC_2 3
#define _ZPP_INC_3 4
#define _ZPP_INC_4 5
#define _ZPP_INC_5 6
#define _ZPP_INC_6 7
#define _ZPP_INC_7 8
#define _ZPP_INC_8 9
#define _ZPP_INC_9 10
...
#define _ZPP_INC_255 256

同样的方式,再如法泡制PP_DEC,从256开始,一直递减到0为止。对于大于256的数,就不支持了,那就都是未定义操作。这样子,通过PP_INC(n),就得到n+1;而PP_DEC(n),则是n-1。比如PP_INC(PP_DEC(9)),其结果肯定是9了。很好,这样子,在预处理中就实现了自然数自增1和自减1的运算了。另外,对于大于256的数,比如512传递给PP_INC,就只得到一个_ZPP_INC_512的符号,完全没有任何意义。

然后,两个自然数是否相等的判断,也非常重要,必须支持。但是,在此之前,要实现一个宏函数PP_NOT,用来判断入参是否为0。为0的话,则函数返回1,否则,就返回0。也即是:
PP_NOT(0) == 1
PP_NOT(23) == 0,或者 PP_NOT(var) == 0。
记住,预处理提供给我们的原生类型就只有符号和##并接运算,除此之外,别无他物。好像工具太简陋,能完成目的吗?不得不佩服有些码猿的脑洞。以下代码是这样运作的,假设PP_NOT生成以下的调用形式,先不管PP_ARG1,至于符号~,是这样子的,可以看成普通的变量名字,它就是占位符,因为预处理只识别逗号(,),和小括号,至于其他符号,完全无视,那些是C/C++编译阶段才关心的符号。
PP_NOT(0) = PP_ARG1(~, 1, 0)
PP_NOT(n) = PP_ARG1(_ZPP_NOT_n, 0)
然后,让PP_ARG1取第二个参数(码猿的计数是从0开始的,也即是,0即是1,1即是2),就完成任务了。至于_ZPP_NOT_n是什么鬼,那个只是中间生成的临时符号,可以舍弃。我们只需对_ZPP_NOT_0做特别处理。因此,代码可以这样写了。PP_PROBE()用以生成两个入参
#define PP_PROBE() ~, 1
#define _ZPP_NOT_0 PP_PROBE()
#define PP_NOT(_X, ...) PP_IS(PP_JOIN(_ZPP_NOT_, _X))
# define PP_IS(...) PP_ARG1(__VA_ARGS__, 0)

这样子之后,显然PP_NOT(n)就可以变成PP_ARG1(_ZPP_NOT_n, 0)的形式了。PP_NOT不是只需一个入参吗?为何后面还要带省略号,纯粹是为了后面各种变态的运用,取悦编译器。已经用宏来写代码了,就不必再遵守什么清规戒律,只要能完成任务就行了。

至于PP_ARG1的实现,就很简单了,如下所示,
#define PP_ARG0(_0, ...) _0
#define PP_ARG1(_0, _1, ...) _1
#define PP_ARG2(_0, _1, _2, ...) _2

然后通过两次取反的函数,再补上函数PP_BOOL,如果入参>0,就返回1,否则返回0,类似于整型到bool的强制类型转换。
#define PP_BOOL(_X, ...) PP_NOT(PP_NOT(_X))

有了这些的铺垫之后,要比较两个自然数是否相等,就简单了。其实没什么神秘的,就是针对从0到255,重复256个以下形式的#define语句,
#define _ZPP_0_EQUALS_0 PP_PROBE()
#define _ZPP_1_EQUALS_1 PP_PROBE()
#define _ZPP_2_EQUALS_2 PP_PROBE()
...
#define PP_EQUALS(x, y) PP_IS(PP_CONCAT4(_ZPP_, x, _EQUALS_, y))
PP_EQUALS就是将入参并接成_ZPP_x_EQUALS_y的形式,只要x和y相同,也即是说,它们在上面的表格中,那么,道理就如同PP_NOT的实现那样,最后结果就是1了。其实,预处理中没有判断这种玩意,只有表格,只有并接,只有查表。所谓的图灵完备,说白了,没有玄虚的,就是建表,然后查表。对相等比较取反PP_NOT,自然就得到不相等的判断函数。
#define PP_UN_EQUALS(x, y) PP_NOT(PP_IS(PP_CONCAT4(_ZPP_, x, _EQUALS_, y)))
再次建表,就可以得到bool运算的函数,或与
#define PP_OR(a,b) PP_CONCAT3(_ZPP_OR_, a, b)
#define _ZPP_OR_00 0
#define _ZPP_OR_01 1
#define _ZPP_OR_10 1
#define _ZPP_OR_11 1

#define PP_AND(a,b) PP_CONCAT3(_ZPP_AND_, a, b)
#define _ZPP_AND_00 0
#define _ZPP_AND_01 0
#define _ZPP_AND_10 0
#define _ZPP_AND_11 1

再准备一张表格,将字节映射到8个二进制位。
#define _ZPP_BINARY_0 (0, 0, 0, 0, 0, 0, 0, 0)
#define _ZPP_BINARY_1 (0, 0, 0, 0, 0, 0, 0, 1)
#define _ZPP_BINARY_2 (0, 0, 0, 0, 0, 0, 1, 0)
#define _ZPP_BINARY_3 (0, 0, 0, 0, 0, 0, 1, 1)
#define _ZPP_BINARY_4 (0, 0, 0, 0, 0, 1, 0, 0)
...
然后通过模拟计算机组成原理里面的加减乘除的原理,就可以实现四则运算了。对了,整个预处理库的代码都在压缩包上,功能比boost的预处理库强多了,但是代码却少了很多,也容易理解多了,所有代码在vs下面正常运行,其他平台还没有测试。代码包:preprocessor.rar

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多