c++ 预处理的图灵完备之引言

我们还是来讨论c++吧,这几年在c++里面玩代码自动生成技术,而预处理是不可避免,也是不可或缺的重要工具。虽然boost pp预处理库在宏的运用上很是完善,但是代码也太多了,而且代码很不好理解,对此,不免让人疑惑,有必要搞得那么复杂,搞那么多代码吗?并且,看了boostpp的使用接口后,感觉写得很不干净,也不好组合。因此,重新做了一套预处理的轮子。以下的代码,假设在msvc2013以上的版本运行,反正很多人用MSVC的,装逼的自当别论,造出来的轮子,倾向于先支持msvc。

首先,我们定义一个宏,用来给把入参变成字符串,咦,这个事情也太easy了,但是,在此,感觉,还是有必要废话多解释一下。以下代码惯例都是,所有可用的宏函数都是以PP开头全部大写,而以_ZPP开头的全部都是内部实现,其实还可以做得更难看一点。因为宏函数是全局的,没有作用域的概念,并且只是单纯的文本替换,死的时候,还不知道怎么死,所以,必须谨慎对待。像是windows.h头文件那样,直接用min,max作为宏的名字,虽然用起来很方便,但也不知道制造了多少麻烦,所以,很多时候,包含windows.h时,第一件事情就是undef min和max。

以下的代码,可以随便在某个工程下,随便建立一个cpp后缀名的源文件,然后按CTRL+F7编译,不需要F5,就可以看到运行的效果,如果编译通过,就说明宏基本上正确,测试代码越多,准确性就越高。当然,你们也可以通过设置源文件的属性,让msvc生成预处理后的文件,然后用记事本打开那个文件观看。

#define PP_TEXT(str) _ZPP_TEXT(str)
#define _ZPP_TEXT(str) #str

在c++预处理宏中,操作符#是将后面跟随的表达式加上两个双引号,也就是字符串。PP_TEXT(str)不是直接定义成#str,而是通过调用_ZPP_TEXT(str),然后在那里才将入参变成字符串,显得有点辗转,有点多此一举,但,其实是为了支持宏的全方位展开,也就是入参str本身也存在宏调用的时候,纯属无奈。比如,如果这样实现

#define PP_TEXT(str) #str

那么,对于下面的情况,

#define AAA aaa

PP_TEXT(AAA),结果将是"AAA",而不是"aaa"。因为宏操作符直接是将入参变成字符串,没有让入参有一点点回旋的空间,所以只好引入间接层,让入参有机会宏展开。后面,很多宏函数都是这样实现,不得不间接调用,以便让宏全面展开。而msvc的宏展开机制更加奇葩,更加不人性化,其间接调用的形式也更丑陋。这都是没办法的事情。
然后,为了调试宏,或者测试宏,当然,很多时候,调试宏,还是要打开预处理的文件来对比分析。我们对 static_assert作一点点包装,因为static_assert需要两个参数,c++11后面的c++版本中,static_assert好像只需要一个入参,那时就不需要这个包装了。

#define PP_ASSERT() static_assert((__VA_ARGS__), PP_TEXT(__VA_ARGS__));

PP_ASSERT(...)里面的三个点,是不定参数的宏,而__VA_ARGS__就代表了...所匹配的所有参数,这条语法很重要,要熟练。这里,就不详细解释其用法了,后面会有大把大把的宏函数用到__VA_ARGS__。
好了,我们可以开始用PP_ASSERT(...)做测试了。
PP_ASSERT(2+3==5)
如果,然后编译这个文件,发现编译通过了,比如
PP_ASSERT(2+3==4)
编译的时候,就会报错误信息,error C2338: 2+3==4
好了,测试准备建立起来,就可以开始肆无忌惮的写代码了。一步一步地构建c预处理宏的图灵完备。
显然,当务之急,最根本的宏就是将两个宏参数的并接,也即是##运算符,显然好比#运算那样子,必须给里面参数有宏展开的机会,因此要间接调用,下面是其实现

#define PP_JOIN(_A, _B) _ZPP_JOIN_I(_A, _B)
#define _ZPP_JOIN_I(_A, _B) _ZPP_JOIN_II(~, _A##_B)
#define _ZPP_JOIN_II(p, res) res

竟然不止一层间接,而是两层,又多此一举,是因为发现在做宏递归的时候,一层间接调用还不能让宏充分地展开,所以只好又加间接层,也不明白是何原因,也懒得追究了。现在,接下来,当然是测试PP_JOIN了。各位同学,可以新建立一个测试文件,那个文件include我们的这个宏函数。当然,也可以在同一个文件里面写测试代码,注意分成两段代码,上一段写宏函数,下一段写测试代码,目前来看,都可以的,后面再整理。

PP_ASSERT(PP_JOIN(1+2, == 3))
#define A 20
#define B 10
PP_ASSERT(PP_JOIN(A + B, == 30))

有了PP_JOIN,就可以开始做点其他事情了。比如,计数器,

#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

这里,我们重新又实现了一遍PP_JOIN,这也是没办法的事情,后面在重重嵌套的时候,会出现PP_JOIN里面又包含PP_JOIN的情况,这样会导致宏停止展开了,所以,只好对于每一个要用到JOIN之处,都用自己版本的JOIN。
这是宏函数的实现方式,通过并接,文本替换,一一枚举,才达到这样的效果,也就是说,我们通过JOIN函数,在宏里面构造了一个计数器的数据类型。如果每个宏函数都这样写,岂不是很累。好消息是,只需用这种苦逼方式实现几个最基本的函数,然后通过宏的递归引擎,其他的宏函数就不需这样子一个一个苦逼的并接替换了。
PP_ASSERT(PP_INC(9)==10)
PP_ASSERT(PP_INC(PP_INC(9)) == 11)
写测试代码习惯了,写起来就很有意思了,测试通过,也是最激动人心的时刻。
接下来,要处理msvc里面宏的恶心行为,然后就结束本引言。

#define PAIR_SECOND(x, y) y
PP_ASSERT(PAIR_SECOND(10, 20) == 20)

这样子,还不错,下面,再define一个宏函数,让其返回一个pair,也就是两个值

#define MAKE_PAIR(x, y) x, y

然后,这样调用,
PAIR_SECOND(MAKE_PAIR(10, 20))
编译器马上就不高兴了,warning C4003: “PAIR_SECOND”宏的实参不足
好像是编译器没有先展开MAKE_PAIR(10, 20),然后再调用PAIR_SECOND,而是直接把MAKE_PAIR(10, 20)整个当成一个函数传给PAIR_SECOND,然后,PAIR_SECOND就提示实参不足,然后,硬要测试,
PP_ASSERT(PAIR_SECOND(MAKE_PAIR(10, 20)) == 20)
显然,无论如何,编译器势必就龙颜大怒了。对此,我们只好再引入间接层,想办法让MAKE_PAIR(10, 20)先展开,然后再传给PAIR_SECOND。这样,就不能直接用这样的形式了,PAIR_SECOND(MAKE_PAIR(10, 20)) 。只好改成这样,下面的几行代码,很有点惊天地泣鬼神的味道。

#define _ZPP_INVOKE_JOIN(_A, _B) _ZPP_IMP_INVOKE_JOIN_I(_A, _B)
#define _ZPP_IMP_INVOKE_JOIN_I(_A, _B) _ZPP_IMP_INVOKE_JOIN_II(~, _A##_B)
#define _ZPP_IMP_INVOKE_JOIN_II(p, res) res

#define PP_INVOKE(m, args, ) _ZPP_INVOKE_JOIN(m, args)

前面几行代码都是PP_INVOKE的JOIN函数实现,可以直接当它们是JOIN函数,关键是PP_INVOKE(m, args, ...)这里,第一个参数m是宏函数,第二个是args,是要传给第一个参数m的参数列表,用括号括起来,至于后面的省略号,是有些时候为了取悦编译器而添加的,也不知道是什么原因,反正这样子就可以了,懒得追究。垃圾宏,垃圾预处理,只要能完成功能就行了,c++中,代码生成代码,重头戏在tmp那里,宏只是小小必要的辅助工具而已。然后,这样调用,
PP_ASSERT(PP_INVOKE(PAIR_SECOND, (MAKE_PAIR(10, 20))) == 20)
编译通过了,好不容易啊!

(0)

相关推荐

  • c++ 预处理的图灵完备之引言

    我们还是来讨论c++吧,这几年在c++里面玩代码自动生成技术,而预处理是不可避免,也是不可或缺的重要工具.虽然boost pp预处理库在宏的运用上很是完善,但是代码也太多了,而且代码很不好理解,对此,不免让人疑惑,有必要搞得那么复杂,搞那么多代码吗?并且,看了boostpp的使用接口后,感觉写得很不干净,也不好组合.因此,重新做了一套预处理的轮子.以下的代码,假设在msvc2013以上的版本运行,反正很多人用MSVC的,装逼的自当别论,造出来的轮子,倾向于先支持msvc. 首先,我们定义一个宏,

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

    虽然通过一系列的奇技淫巧,让预处理也图灵完备一把,但是用预处理来做计算,真的很吃力不讨好.因为预处理一开始设计出来的目的,就没什么野心,原本就仅仅只是为了做简简单单的文本替换工作,并没有想过要成为正儿八经的编程语言,即便是最最缩水版脚本语言的功能要求都达不到.只是后来,实在是大量要求要批量自动生成代码,特别是c++11之前的版本玩什么模板元编程,铺天盖地的要有大量相似的代码.这些代码用其他工具来生成,当然形式会更加漂亮,但是始终还是用原生的预处理来做这种事情会更加的方便,否则每次修改,都要运行一

  • less简单入门(CSS 预处理语言)

    Less 是一门 CSS 预处理语言,它扩充了 CSS 语言,增加了诸如变量.混合(mixin).函数等功能,让 CSS 更易维护.方便制作主题.扩充. Less 可以运行在 Node.浏览器和 Rhino 平台上.网上有很多第三方工具帮助你编译 Less 源码. 官网地址 http://lesscss.org/ less手册 www.lesscss.net/ bootstrap官网less介绍 http://less.bootcss.com/ 一.浏览器端环境搭建 github下载地址:htt

  • java使用JDBC动态创建数据表及SQL预处理的方法

    本文实例讲述了java使用JDBC动态创建数据表及SQL预处理的方法.分享给大家供大家参考,具体如下: 这两天由于公司的需求,客户需要自定义数据表的字段,导致每张表的字段都不是固定的而且很难有一个通用的模板去维护,所以就使用JDBC动态去创建数据表,然后通过表的字段动态添加数据,数据的来源主要是用户提供的Excel直接导入到数据库中. 如果考虑到字段的类型,可以通过反射的机制去获取,现在主要用户需求就是将数据导入到数据库提供查询功能,不能修改,所以就直接都使用String类型来处理数据更加便捷.

  • IOS开发之路--C语言预处理

    概述 大家都知道一个C程序的运行包括编译和链接两个阶段,其实在编译之前预处理器首先要进行预处理操作,将处理完产生的一个新的源文件进行编译.由于预处理指令是在编译之前就进行了,因此很多时候它要比在程序运行时进行操作效率高.在C语言中包括三类预处理指令,今天将一一介绍: 宏定义 条件编译 文件包含 宏定义 对于程序中经常用到的一些常量或者简短的函数我们通常使用宏定义来处理,这样做的好处是对于程序中所有的配置我们可以统一在宏定义中进行管理,而且由于宏定义是在程序编译之前进行替换相比定义成全局变量或函数

  • ThinkPHP3.1新特性之查询条件预处理简介

    以往的ThinkPHP3.0版本对数组方式的查询条件会进行安全过滤(这是由于3.0强制使用了字段类型检测,所以数组方式的查询条件会强制转换为字段的设定类型),但是3.0版本并不支持字符串条件的安全过滤.而ThinkPHP3.1版本则增加了对条件字符串进行预处理的支持,让ORM的安全性更加得以保证. 一.使用where方法 Model类的where方法支持字符串条件预处理,使用方式: $Model->where("id=%d and username='%s' and xx='%f'&quo

  • php+mysqli使用预处理技术进行数据库查询的方法

    本文实例讲述了php+mysqli使用预处理技术进行数据库查询的方法.分享给大家供大家参考.具体如下: 代码有些难度,需要基础知识比较扎实才能好理解,代码先放上来: 这里实现查询所有 id>5 的 id,title,contents值: 复制代码 代码如下: <?php $mysqli = new MySQLi("localhost","root","123456","liuyan"); if(!$mysqli){

  • php+mysqli预处理技术实现添加、修改及删除多条数据的方法

    本文实例讲述了php+mysqli预处理技术实现添加.修改及删除多条数据的方法.分享给大家供大家参考.具体分析如下: 首先来说说为什么要有预处理(预编译)技术?举个例子:假设要向数据库添加100个用户,按常规思路,就是向数据库发送100个执行请求,此时,按照 mysql 数据库的工作原理,它需要对每一条执行语句进行编译(这里就有100次).所以,这里的效率是非常低的. 预处理(预编译)技术的作用,就是减少编译的次数和时间,以提高效果.通过一个案例来说明,预处理(预编译)技术是如何做到的(好吧,先

  • PHP mysqli扩展库 预处理技术的使用分析

    1.使用mysqli扩展库 预处理技术 mysqli stmt 向数据库添加3个用户 复制代码 代码如下: <?php //mysqli扩展库 预处理技术 mysqli stmt 向数据库添加3个用户    //1.创建mysqli对象    $mysqli = new MySQLi("localhost","root","root","test");    if($mysqli->connect_error){ 

  • MySQL中预处理语句prepare、execute与deallocate的使用教程

    前言 MySQL官方将prepare.execute.deallocate统称为PREPARE STATEMENT,我习惯称其为[预处理语句],其用法十分简单,下面话不多说,来一起看看详细的介绍吧. 示例代码 PREPARE stmt_name FROM preparable_stmt EXECUTE stmt_name [USING @var_name [, @var_name] ...] - {DEALLOCATE | DROP} PREPARE stmt_name 举个栗子: mysql>

随机推荐