游戏中的物品交换逻辑抽象
如果总结一下游戏的内部逻辑,或许会让游戏变得很无趣,不过程序员大概会很高兴,比如说我。
如果把用户的各种信息都抽象成“物品”、“个数”,那么所有的游戏逻辑都是用户之间的物品交换。
为了方便描述,我来自定义一套语法表示这种交换。这里用的是类BNF语法,如果没耐心可跳过。
物品交换 := 物品描述 “=>” 物品描述
物品描述 := 物品描述 “+” 单个物品描述 | 单个物品描述
单个物品描述 := 用户 “(” 物品 “,” 数量 附加属性 “)”
物品 := 物品名称 | 物品函数
物品函数 := rand “(” 物品列表 “)”
物品列表 := 物品列表 “,” 物品 | 物品
附加属性 :=”,” “{” 附件属性列表 “}” | “”
附加属性列表 := 附加属性列表 “,” 属性 | 属性
属性 := 属性名 “:” 属性值
数量 := 数字 | 数字函数
数字 := [...]
Firefox插件(plugins)开发实用指南
Firefox插件可实现强大功能,但其中麻烦事情不少。写这个实用指南首先是为了方便自己记忆,免得以后再次栽倒一些坑里面,如果能帮助其他人,则是更好。这个指南不是为了手把手教读者开发插件,而是作为一个FAQ,解决各种诡异问题。
Firefox拥有众多的扩展(Extension),开发扩展也非常容易,不过有一些事情还是无法用扩展解决,需要访问操作系统的底层功能,这就需要写插件(plugins)。例如flash就是一个插件而不是扩展。
Mozilla提供了一系列的教程和文档,虽然很不详尽,众多重要的API语焉不详,但至少是一个好的开始。
最需要阅读的是plugins API和使用入门。这是一个相当长的文档,如果看完所有的内容会花费大量的时间而且还会很晕,这里列一些重点供参考。
plugins基础概念
写第一个插件(只需要关注Writing Plug-ins这一节所谈到的内容)
获得一份firefox的源码,比如firefox 3.6。plugins的例子可以在源码里找到(modules/plugin/sdk/samples),如果出了问题还可以自己编译一个debug版的firefox来调试。
了解浏览器能提供什么功能
制作插件的安装程序,推荐用扩展的方式安装插件,有无数的好处
完成以上这些内容以后差不多就已经可以实现自己的插件了,一般而言,参照着例子来做开发不会有什么问题,只是有不少细节需要留意。
Firefox plugins开发的众多奇怪的约定(假设plugins已经被正确安装)
有些约定非常奇怪,不要问我为什么,天晓得开发firefox的牛人们怎么想的。
在Windows下,plugins必须满足以下条件才能被firefox检测到:
插件的名字必须是np*.dll,也就是必须以np开头,.dll结尾
插件dll资源的语言必须为LANG_ENGLISH,code page必须为1252。在rc文件里是这么写的:
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
插件dll的VERSION_INFO里面必须包含以下值:
VALUE “MIMEType”, “application/x-your-mimetype”
这个MIME就是<object>标签引用插件的唯一凭证。
在Linux下,plugins必须满足以下条件才能被检测到:
插件的名字必须是lib*plugin.so,即以lib开头,plugin.so结尾
插件必须实现NP_GetMIMEDescription和NP_GetPluginVersion,并返回合适MIME字符串。注意,这个字符串并不是普通的MIME,是有特殊规则的,详见前面这个链接的内容。
插件so不要静态链接gtk、opensll、pthread、z等系统库,这会在不同linux平台上因为符号表的问题遇到各种运行时错误
特别需要说明的是,NP_GetPluginVersion、NP_GetEntryPoints等关键函数没有任何官方文档介绍它们,只能根据例子来猜,反正没事就别改它们的实现,copy例子中的代码就好。
firefox插件开发注意事项
写firefox插件的一个基本习惯是,经常编译代码并运行它,保证你的插件还能工作。只要firefox无法加载dll/so,或者加载出现任何错误,都会悄无声息的忽略这个插件。时常关注一下about:plugins,看看插件是不是还在这个列表里。
firefox插件从窗口模式上可分为windowless和windowed两种。其中,windowless模式的文档较多较全,是firefox比较推荐的模式,坑比较少,这里就不多说了。windowed模式则相反,需要好好说说。
无论在Windows还是Linux上,windowed的插件都拥有独立于浏览器页面的窗口。firefox会通过插件的NPP_SetWindow来告诉插件当前窗口的情况。
关于windowed插件有两个诡异问题需要注意:
Windows平台下,插件窗口默认会响应WM_CTLCOLOREDIT、WM_CTLCOLORLISTBOX、WM_CTLCOLORBTN、WM_CTLCOLORSTATIC消息,并设置一个默认的背景色。这本来没问题,但在Windows XP下,这个颜色居然永远是黑色,而不是默认系统背景色(通常是白色)。最好subclass这个窗口并且拦截这些消息,不要让firefox去处理它们。对于插件来说,firefox处理这些消息只是帮倒忙而已。至于firefox还帮了哪些倒忙,可以去源码widget/src/windows/nsWindows.cpp的nsWindow::ProcessMessage()去围观。
Linux平台下,NPP_SetWindow传入的NPWindow指针中虽然有一个ws_info成员,这个成员里面也确实有一个display变量指向X Window的Display结构,但绝对不要真正使用它,否则可能会导致firefox直接退出,据说这可能是firefox的一个bug。
测试firefox插件小技巧,测试方面的高手可以无视
测试插件前建议先在firefox里面创建一个新的profile(帐号)。这样可以创造一个最干净的开发环境,避免被其他扩展/插件干扰。
默认的profile名叫default,在命令行里输入firefox -p default就可以使用这个profile。如果只是输入firefox -p,会弹出一个对话框用于选择profile。这个命令在Windows和Linux下都可使用。
无论是哪个平台,调试插件的方法都很类似。
Windows下可以用VC以调试方式启动firefox,载入插件时调试器会自动载入对应的符号,捕捉发生的异常或者设断点都很方便。
Linux下直接用gdb就好,细节应该不用多说。有一点需要注意,系统默认安装的firefox命令(默认放在/usr/bin/firefox)是一个shell脚本,真正的可执行文件名字需要打开这个脚本自行查找。
实现firefox插件的基本功能
firefox为插件提供的接口十分原始,很多功能默认没有实现,下面提供了一些思路和方法。
让插件接受焦点:默认情况下,<object>标签不能获得焦点,必须指定tabindex。
在插件中使用tab键跳到下一个element:没有好办法,必须自己手动将焦点还给浏览器窗口(Linux下不必如此),然后自己用NPN_*系列函数找到应该获得focus的DOM element,然后调用这个element的focus()方法。
隐藏和显示插件:直接设置<object>标签的style.display = “none”即可,但这里有个严重的副作用,firefox会调用插件的NS_PluginShutdown,销毁这个插件。如果不期望造成这种效果,要么别用这种方式隐藏插件,要么把插件状态保存在js里,再次显示的时候把状态设回去。
触发DOM事件:firefox没有提供任何便利的方法触发DOM事件,要在插件中做到这点,必须自己模拟js触发DOM事件的过程。例如,对于HTML事件,假设self是DOM element,js会这么做。
evt = document.createEvent(“KeyboardEvent”);
evt.initKeyEvent(
“blur”, // in DOMString typeArg,
false, // in boolean canBubbleArg,
false); // in boolean cancelableArg,
self.dispatchEvent(evt);
对应的C代码就是
void FireHTMLEvent(NPP npp, [...]
我这程序员的一年
2008年12月30日,正是我在微软发farewell letter的日子。我当时已经拿到百度的offer,正在准备把自己的角色从微软的项目经理转换成百度的技术研发。角色的转变背后往往藏着各种故事,我当然也不例外。
从微软到百度,我只觉得这是我的幸运,并没什么值得夸耀的地方。我在微软的一年半时间里,技术上逐渐荒废,连自己也觉得堕落不已,有劲使不出。离开微软并非自我选择,但尝试走进百度,则是当时一个勇敢的决定,我重新抱起书本,打开已经陌生的Visual Studio,从头开始准备。
我参加的第一个百度面试是在普天大厦,部门是NS,当时有两个面试官同时面我。我用刻意的沉稳与简洁来掩盖自己的不安,整个过程好似梦魇,令我疲惫不堪。郁闷的是,也许因为HR之间没有沟通好,前一轮面试结束1小时后,我还要赶到信威通信,参加百度第二个面试,电子商务部的面试。本来在上一个面试中我已经把斗志与自信消磨的差不多了,我只好用自己的本色来面对这第二个面试。很奇怪,第二个面试反而比第一个轻松,我发现我的思维开始活跃起来,沉睡已久的程序员的细胞开始复苏,讲起各种技术竟也能变得流畅而不刻板了。
再之后,我觉得自己像撞了大运一般,两边的面试竟都走到了最后一轮,而且都还通过了,这个面试的经历为我今年的程序员之路开了个好头。
值得一提的是,在电子商务部最后一轮面试的时候,老大问我对未来的规划时,我犹豫了。我曾想,做项目经理、做管理似乎是一个程序员的必然发展道路,但对于我真的适合么?我已经厌烦那种push团队前进、营造团队氛围、制定远景方向这种事情,我更想这几年踏踏实实的做事,完善自己的知识体系。但当时我还无法打破自己的思维惯性,还是支支吾吾的说希望成为研发经理云云。直到加入百度半年后,我才坚定自己的想法,做一个简单可依赖的程序员,先从技术做起。
2009年1月,我加入了百度,这程序员的一年开始了,然后很快,这一年结束了。
我从没想过时间会过的这么快,这么紧张有趣。我从对Linux一窍不通,到现在都开始习惯完全用vi编码、在命令行中调试、负责服务器程序的优化,这种变化我自己都感到惊讶。
粗略统计了一下,这一年大约写了10K的代码,这个数字比一年前0行代码比起来当然是无穷大,但和原来本科七年写100K代码比起来,似乎也不算那么多。我当年做简历的时候就很惊讶,自己参与过的各种项目、自己写的各种小玩意,居然有那么多行代码,到今天我终于明白,其实这并不难,如果不是今年我把很多时间用在摸索上面,恐怕代码行数还得翻番。
我今年最大的收获是激活了程序员的基因,手指终于开始适应写代码啦,这是一个很好的开始。
本文写到这里似乎还没开始说这一年到底发生了什么,但确实已经要结束了。无论是接手康神留下的系统,还是从信威搬到百度大厦,这些都是外在的一些挑战与变化,相比自己重新选择未来道路这件事情来说,真的是微不足道。我这程序员的一年,恰好就是选择结果的体现,到现在我已经可以说,这个选择没有错,至少我比原来快乐。
架构成长之路
产生软件架构设计的动机是代码复用,当一段重复的代码在多个不同的模块之中重复多次、每个人都为这段代码写的吐血的时候,大家自然而然就想到了——我们要有一个“架构”,或者说准确的说是一个框架。
最原始框架一定是最基本的。从广义上来说,操作系统就是一个框架,它封装了每一个程序需要完成的基本任务,管理内存、磁盘IO、中断、输入输出等。不过操作系统能封装的东西有限,最关键的是,封装总是略显简单。比如,需要写一系列服务器程序,每个程序都要访问网络、保持长连接、管理连接池等,这些机制并不是几行代码就可以实现,需要一个稍微高一点层次的框架来解决这个问题。于是很自然的,层次的概念就出来了。
当框架有了层次概念之后,大家就可以各司其职,为完善各自的框架而奋斗,直到最终框架已经极为好用,每写一个程序就像冲杯奶茶一样容易。大家都很高兴。
到现在为止,软件架构似乎还非常的简单直白,但随着时间发展,复杂的需求涌现出来了,架构也就突破原来软件框架的范畴,变成更复杂的东西。
还是拿那一系列服务器程序为例,假设它们是一个在线交易平台的后台服务。某个程序员完成了个人转账业务的代码,另一个程序员完成了商户转账业务的代码,互相一比对发现,嘿,大家怎么写的大同小异,应该优化。不过这样的优化存在着风险,因为谁都不能完全确定,未来这两个业务会怎么发展。
保守的程序员会让这两份代码独立,静观其变,反正框架已经足够好,重写一份这种业务也花不了多少时间。这并没有错,不过会错失让架构突破框架限制的机会。
喜欢挑战的程序员会仔细思考这两个业务逻辑,从业务的层面来分析其中的异同以及未来变化的趋势。
比如转账这件事,无论是个人业务还是商户业务,创建交易、记账这些事情总少不了吧,这或许可以抽象出来,但是检查账户权限、验证密码这些事情肯定会不一样,这些东西保持尽可能的可变。如此这般,业务层面的封装逐渐成形,所谓的架构终于有了架构的意思。
在引入业务之前,前面提到的“架构”只是一个放之四海皆可用的程序员的玩具;引入业务之后,架构终于得以走向成熟,摆脱程序员的束缚,反过来影响程序员的思维。
真正让架构腾飞的是引入领域知识(domain knowledge)的概念。作为一个具有业务功能的系统,要维护业务的纯正性不容易。还是拿转账来说事,如果提供了创建交易和记账的业务抽象,再假设转账是一气呵成的,从代码上来说,先创建交易还是先记账其实没有太大区别。但这事搁在包含了领域知识的架构里面可不行,因为账务要求,必须先有单据再有流水。
领域知识把程序员变成业务专家,渐渐的程序员就更能明白什么是业务、什么算框架,也可以开始用业务接口包装子系统,理解子系统之间的逻辑,让架构日臻完善。
P.S. 上面这些文字并没有涉及具体的设计原则、方法。
P.S.S. 再完美的架构也需要持续改进,况且这世界上恐怕还不存在所谓“完美”的架构。
体验为先
一般的想法似乎是:产品部负责体验,技术部负责实现细节,这两者虽相辅相成,但终究关注点不同。不过我觉得其实技术部也应该关注体验,这种体验是开发体验、运维体验和用户体验,至于实现细节,反倒是最后需要关注的。这样提的原因是,确实有一些技术的人一见到好的新奇的技术就像应用到项目中去(本身没什么不好),却没把体验这件事情做好(这个有点不好)。
现在来说说我对体验的理解。作为开发者,最大的目标绝不只是开发符合需求的产品。产品的开发、实施、上线、维护等过程都涉及大量不同的人,每一个相关的人对这个产品都有自己的体验,都会对这个产品在某一方面上是否好用有自己的看法。就像体验不好的产品会被用户弃用一样,过程体验不好的产品也会被开发者抛弃,只是迟早的事。
开发体验好也就是代码具有很好的观赏性、架构容易懂、代码质量高、具有可伸缩性等,它是开发过程中最需要注意的事情。
代码是开发者之间的交互工具,一个开发者写出来的代码其实就是其他人未来将用到的“界面”,它的体验直接影响到它的用户(所有与它相关的开发者,包括作者)对它的看法。开发体验不好的代码,在完成自己的当前使命之后很可能会很快被开发者厌倦,甚至废弃,如果一个组织不断在废弃旧代码,做重复造轮子的事情,这种开发成本当然很大,其中明显有问题。
这里有一个很有趣的问题,如果牛人写出来的精致无比的代码是否就一定是具有好的开发体验的代码?这还真不一定。如果确实周围的人一个都看不懂这代码,要么就不要把代码写那么好,浅显一点会更好,要么就多找一点能看懂这种代码的牛人。过于美妙而难以理解的代码是定时炸弹,一旦牛人离开就会面临难以维护或者必须重写的风险,这对于公司而言是不可接受的。说到底,体验是人的主观感受,不同的受众就有不同的体验,好的体验的定义也不同。
做好了开发体验,其次要做的是运维体验。运维体验主要包括更新服务、排错、监控的复杂程度等,主要的用户是运维人员。现在搭建一个大容量的网络应用至少需要几十台机器,机器之间的互相依赖如果管理不好就会变成网状,有点牵一发动全身的味道。有些时候开发者相比运维体验,更关心功能是否能实现,这是有问题的。对于互联网企业,不变的是变化,任何一个服务上线之后都会有用户不满意的地方,如果升级的运维成本比较大,开发者/产品负责人就会惧怕改变,这将直接影响服务质量和用户体验,最终还是害了产品。
此外,作为开发者应该时常思考如何量化服务的质量。是不是满足了一定的服务质量指标就好了?还有没有其他指标?在不同阶段这些问题的答案都不一样,比如已有服务基本稳定之后,开发者可能会从最初的服务质量转而关注如何发现服务性能瓶颈上,这时,难以运维(或者说“监控”)的服务就会显示出弊端,甚至变得没法满足运维体验方面的需求,导致重新开发。
所以在代码设计完了之后就开始考虑如何运维它是一个很值得建议的做法。
最后,好用的产品要落实到用户身上,他们的用户体验是绝对产品成败的关键。在这个时候,开发者可能有的误区是追求完美,希望在项目结束的时候就解决所有已经发现的问题,这其实完全没必要。很多大软件(例如微软的各种软件)遗留一堆bug就敢发布,为什么,就是因为开发者应该追求的是用户体验,而不是那些完全不影响用户体验、只有geek可以发现的微小缺陷。
另外,有这种追求完美思想的另一个原因是惧怕变化,希望一次性做完之后再也不去碰这份完美的代码,实际上这也是不切实际的。保持重构才是我们应该追求的目标,无论是针对代码本身,还是各种体验。
我们在某些时候或许可以追求快而放弃一个或两个体验而直接追求用户体验,但最后还是要补课,欠下的总要还。同时,不要一次性就把三种体验追求到极致,它们应该是迭代完成的。
要想一开始就能做到较好的开发体验、运维体验直到用户体验其实并不难,难就难在有没有这样去理解这些事情。说白了,就是有没有在各个环节为他人着想、为未来打基础,这才是一种做产品的开发者。
设计哲学:职责单一还是接口单一
今天跟康神讨论设计,他提到一个设计哲学(这个词就很让人崇拜),很是有道理。大概意思是说,在设计中,有人喜欢职责单一,所有东西分的很细,有人喜欢接口单一,保证接口稳定,这是个设计哲学问题,各有优缺。
我自己是一个极度喜欢保持职责单一的人,面对康神设计的接口单一但内部逻辑较复杂的模块,多少有点想重构的冲动。其实这两种设计哲学都没太大问题,确实得看需求来确定到底用什么。
接口单一的设计,最典型例子的就是大家都知道的printf,这个东西博大精深,可以组合打印任意格式的文字,如果C标准委员会乐意,还可以通过扩展格式字符串来实现更多的打印选项,同时保证100%向下兼容。还有很多接口,它们通过传递一个结构来传递参数,这个结构拥有众多成员,每次调用接口时只用其中一部分。这些接口也能像printf一样易扩展,康神设计的就是后面这种接口。
这样设计的好处是保持整个系统接口的统一和稳定,这对于一个大型工业系统来说至关重要,因为牵一发动全身,改变任何一个通用接口都会付出很大代价。但它的缺点也十分明显,主要体现是效率不高和模块职责不明。
可以想象,printf的运行效率并不高,对于%以及后面参数的解析是在运行时完成的,每次运行都会在解析格式字符串上花时间。传结构的缺点是内存使用效率不高,并且随着业务逻辑不断变复杂,结构会大到不能接受的地步,最终肯定在结构中设置一个类似于格式字符串或者表达式的东西,逐渐转变成printf的设计模式。
同时,由于接口过于强大,在接口背后隐藏的逻辑也必定复杂无比。想想解析格式字符串或解析结构时该需要多少烦人的if…else,就知道要实现这种设计将会写出多少逻辑复杂的代码,会产生多少的bad smell,给后来维护的人造成不少困扰,甚至会引入不少潜在的错误。
反观职责单一的设计,没有接口单一设计的缺点,但劣势也很明显,就是没有统一的接口,使得整个系统模块之间的调用方式始终难以稳定。如果整个系统由一个人开发还好,如果多人协作,则会大大降低开发效率,会将大量时间用在沟通接口和解决联调问题上面。公平的说,职责单一的设计也可以用facade模式来提供统一接口,但是facade模式的实现代码又会变得和接口统一时一样,没有根本改变,只是一个折中和权衡。
那是否说明“完美”的职责单一的设计不适合大型开发呢?也不一定,因为我们有C++和无比变态的模板技巧。
比如我们可以设计出一个这样的print,功能类似于printf。注意,这个函数设计成只在print的时候才输出,expression类不会造成任何输出。
pattern p = builder() << “uid: ” << uid << ” uname: ” << name;
uid = 1000;
name = “realdodo”;
print(p); // 打印出”uid: 1000 uname: realdodo”
如果熟悉boost.spirit的朋友肯定觉得这个代码很眼熟,没办法,谁要我是这个库的忠实粉丝呢。在这里,pattern_builder类只是一个模板生成器,而不进行任何计算(不同于sstream这样的类),它用<<运算符针对各种类型生成各种特化的模板类,类似于一个嵌套的pair<>。这些模板类层层嵌套,但是pattern类可以轻松搞定,方法就是设计一个concrete_pattern类。
struct concrete_pattern_base
{
concrete_pattern_base() {}
virtual ~concrete_pattern_base() {}
virtual string to_string() const = 0;
};
tempate <typename builder>
struct concrete_pattern : public concrete_pattern_base
{
builder builder_;
concrete_pattern(const builder & b)
: builder_(b)
{}
virtual string to_string() const
{
// 用模板特化的方法实现to_string()方法,这里略去
}
};
这样,pattern的实现就非常简单。
class pattern
{
concrete_pattern_base * pattern_;
public:
template <typename builder>
pattern(const builder [...]
什么是“Nabialek trick”?
现在是时候了,经过这些天的学习和尝试,我终于知道boost.spirit里谈到的Nabialek trick是什么东西了,当然应该大方的与大家分享。
这个Nabialek trick其实并不是编译期间的技巧,而是利用查表的方式将一些静态的类型推导变成简单的运行时调用,用性能损失换取编译期间的复杂,是一种相反的过程。可以说,Nabialek trick给人的感觉是完完全全的没有意义,要知道C++的template就是为了提升运行时性能而增加编译期间运算,这个反其道而行意义何在?其实说白了,意义就在于:消除模板技巧带来的疯狂的自动类型推导,以及解除对某些类型的静态绑定。前者意义在于可以提升编译速度,而且也可以避免达到模板嵌套上限(比如MSVC 7.1,最多只能嵌套256层,这已经算不错了),后者意义在于可以让复杂的表达式变得更容易匹配、更快的执行和更加灵活(所谓的将“线性非决定式”转化为“决定式”,将“或”逻辑从表达式中剔除,避免不必要回溯以加快速度),但是由于创建查找表的过程涉及到内存分配,所以后者并不能总是达到很好效果。
我们再从正面来想想Expression Templates给编程带来的困难,请看下面的示例代码(来自boost.spirit的帮助/libs/spirit/doc/techniques.html,我做了一些简化和修改):
r =
“//” >> *(anychar_p – ‘n’) >> ‘n’
| “/*” >> *(anychar_p – “*/”) >> “*/”
;
在这里,r的类型就是:
alternative<sequence<sequence<strlit<const char*>, kleene_star<difference<anychar_parser, chlit<char> > > >, chlit<char> >, sequence<sequence<
strlit<const char*>, kleene_star<difference<anychar_parser, strlit<const char*> > > >, strlit<const char*> > >
是的,我没指望任何人能够看懂……不过假如做一下的赋值:
line1 = “//” >> *(anychar_p – ‘n’) >> ‘n’;
line2 = “/*” >> *(anychar_p – “*/”) >> “*/”;
r = line1 | line2;
这样的代码应该好理解一些,但是没有本质不同。再来改改:
line1 [...]
boost中如何面对疯狂的类型推导?
这些天,我一直在说一个名词:Expression Templates。这个东西很有用,我也会在未来把这篇英文文档用易懂的中文讲解一遍(当然,各位已经看过的高手可以无视我未来的这篇文章)。
可以很肯定地说,Expression Templates一定是未来C++各种类库设计的标准思路,现在boost.spirit、boost.lambda、boost.uBLAS、boost.xpressive等库都是用这种思路设计,而且介绍这种方法的论文也明确指出,合理应用这种方法可以显著的提高程序性能,能让C++程序库的效率高于相同功能的C程序库(例如std::sort和qsort的比较)。但是它的一个致命缺点是:令人发狂的类型推导。
看看下面这个例子吧,它是boost.spirit的一个例子(boost 1.34.0的libs/spirit/example/techniques/no_rules/no_rule2.cpp),用于说明如果没有自动类型推导或者类似功能的机制,类型定义会变得多么的疯狂:
程序代码
struct skip_grammar : grammar<skip_grammar>
{
template <typename ScannerT>
struct definition
{
definition(skip_grammar const& /*self*/)
: skip
( space_p
| ”//” >> *(anychar_p - ’\n’) >> ’\n’
| ”/*” >> *(anychar_p - ”*/”) >> ”*/”
)
{
}
typedef
alternative<alternative<space_parser, sequence<sequence<
strlit<const char*>, kleene_star<difference<anychar_parser,
chlit<char> > > >, chlit<char> > >, sequence<sequence<
strlit<const char*>, kleene_star<difference<anychar_parser,
strlit<const char*> > > >, strlit<const char*> > >
skip_t;
skip_t skip;
skip_t const& start() const { return skip; }
};
};
请注意中间的那个skip_t的类型定义,我相信一定没有人相信是人手写出来的……本来应该编译器做的事情换做人来做,太疯狂了!特别是还要自己弄明白所有的operator的返回类型,更加的困难……
boost.spirit的作者Joel de Guzman给出了一个比较猥琐的解决方案:将skip_t先定义成随便一个什么类型,比如int,然后编译,这样便一起就会提示说:“Cannot convert boost::spirit::alternative<… …………> to int”,把这个错误拷贝出来,就可以看到实际的类型了——天哪,足够疯狂了!这还没完,如果遇到更复杂的情况怎么办,看看boost 1.34.0的libs/spirit/example/techniques/no_rules/no_rule3.cpp吧,头开始疼了……
所以,Joel de Guzman在给我留下深刻印象之后开始说明他的方案。
方案一:期待C++ 0X早日成为事实的标准,让那个非常有用的auto关键字成为到处可用的东西。
方案二:使用部分编译器已经实现的typeof关键字,然后写一个宏:
程序代码
#define RULE(name, definition) typeof(definition) name = definition
这样,就可以简单的使用宏来简化各种类型定义,不过可怜的MSVC并没有实现这种功能。
方案三:使用“Nabialek trick”。看到“Nabialek trick”这个陌生的名词,我的第一个反应就是“去google/baidu”,不过很可惜,google上的所有搜索结果都直接或间接的来自我正在看的boost.spirit文档,而baidu更是告诉我找不到。OK,我们还是回到这个名词本身吧。这个trick是因为其发明者,Sam Nabialek,而得名,其作用是将“线性非决定式”(linear non-deterministic)转换成“决定式”(deterministic)。如果要说的更具体一些就不行了,其实我还不太明白,我正在阅读boost.spirit的部分源码,希望能够有些收获。无论如何,boost.spirit最终用极为优雅的方式将类型推导的难题再次交给了编译器,整个代码非常的简洁明了,可谓是最佳的实现方式。
就我个人来说,应该还可以提出方案四:使用boost.typeof库的基础设施,不过为了能使用这个东西,依然必须手动的“注册”各种类型,并依赖于大量的模板和宏技巧,比较烦,所以并不算一个很好的方式,但至少还可以接受。
综合来说,正是因为Expression Templates的广泛使用,类型推导成了一个疯狂的怪兽,而C++ 0X中的auto关键字成了绝对的救星,我们还是来呼唤新标准早日降临吧!
使用boost.spirit制作一个简单的四则计算器
从传统意义上来说,boost.spirit库是一个类似于yacc的库,主要业务是做词法解析,然后提供各种读取数据的接口,但是由于这是一个用C++实现、并且大量运Expression Templates技巧的库,所以各种功能可以用非常快捷的方式实现,非常的好用,几乎把C++的各种优良特性都充分发挥出来。
在此,我就从boost.spirit库中间的一个例子出发,经过简单的修改就变成一个极为健壮的四则计算器。我所参考的例子是boost 1.34.0的libs/spirit/example/fundamental/calc_plain.cpp,我因为是在这上面直接修改而来,所以源码中带有原作者的版权声明。
关于boost.spirit的用法,在这里我先不说,以后有时间我来慢慢的把它用中国话讲解一遍。这个程序的核心实际上是一个EBNF的表达式,也就是如何用EBNF语法来表示四则运算。
在这里,我就直接给出答案(EBNF的知识请暂时自行看编译原理的教材):
程序代码
expression ::= term ( (‘+’ term) | (‘-’ term) )*
term ::= factor ( (‘*’ factor) | (‘/’ factor) )*
factor ::= REAL | ’(‘ ex ’)’ | (‘-’ factor) | (‘+’ factor)
其中,expression就是我们需要的表达式pattern。注意这里,正是由于expression首先去匹配term,而term首先去匹配factor,最后factor是以“纯数字”、“括号”、“正负符号”的顺序匹配,term则是以factor、“乘法”、“除法”的顺序匹配,而expression是以term、“加法”、“减法”的顺序匹配,所以就保证了整个表达式匹配过程是按照四则运算的先后顺序进行的。在匹配的过程中只要安插各种“监视”的函数(boost.spirit里面的术语叫做“actor”),就可以轻松实现四则运算。
把EBNF对应到boost.spirit里,具体的grammer类实现如下:
程序代码
struct calculator : public grammar<calculator>
{
template <typename ScannerT>
struct definition
{
definition(calculator const& /*self*/)
{
expression
= term
>> *( (‘+’ >> term[do_calc<do_add>()])
| (‘-’ >> term[do_calc<do_substract>()])
)
;
term
= factor
>> *( (‘*’ >> factor[do_calc<do_multiply>()])
| (‘/’ >> factor[do_calc<do_divide>()])
)
;
factor
= real_p[&push_real]
| ’(‘ >> expression >> ’)’
| (‘-’ >> factor[&do_neg])
| (‘+’ >> factor)
;
}
rule<ScannerT> expression, term, factor;
rule<ScannerT> const&
start() const { return expression; }
};
};
请注意,calculator从grammer派生,但是除了在内部定义了一个嵌套class以外,并没有做更多的事情。
至于这里面用到的do_calc<>、push_real等functor和函数,则是一些极为简单的东西,实现如下:
程序代码
namespace
{
stack<double> calc_stack;
struct do_add
{
double operator () (double lhs, double rhs) const
{
return lhs + rhs;
}
};
struct do_substract
{
double operator () (double lhs, double rhs) const
{
return lhs - rhs;
}
};
struct do_multiply
{
double operator () (double lhs, double rhs) const
{
return lhs * rhs;
}
};
struct do_divide
{
double operator () (double lhs, double rhs) const
{
return lhs / rhs;
}
};
template <typename op>
struct do_calc
{
void operator () (const char *, const char *) const
{
double result = calc_stack.top();
calc_stack.pop();
result = op()(calc_stack.top(), result);
calc_stack.pop();
calc_stack.push(result);
}
};
void push_real(double d)
{
calc_stack.push(d);
}
void do_neg(char const*, char const*)
{
cout << ”NEGATE\n”;
double result = calc_stack.top();
calc_stack.pop();
calc_stack.push(-result);
}
double show_result()
{
return calc_stack.top();
}
}
可以从源码中清晰看到,这些函数就是简单的做了些出栈/入栈以及运算的工作,每一个函数功能极为单纯,可认为就是一些状态及处理函数而已,而状态机逻辑则由grammer搞定了。
令人害怕的C++错误输出
这几天详细阅读了boost不少库的文档,特别详细研究了一下boost.spirit库,一个非常令人惊叹的LL词法分析库。关于这个库本身,我以后有时间再慢慢介绍,真的非常让人惊叹,不过今天我要惊叹的是使用它时得到的错误信息……
我用VS2005编译,错误信息如下:
程序代码
d:\boost\boost_1_34_0\boost\spirit\core\scanner\scanner.hpp(130) : error C2198: ’void (__cdecl *const )(const char *,const char *)’ : too few arguments for call
d:\boost\boost_1_34_0\boost\spirit\core\scanner\scanner.hpp(161) : see reference to function template instantiation ’void boost::spirit::attributed_action_policy<AttrT>::call<void(__cdecl *)(const char *,const char *),IteratorT>(const ActorT &,const double &,const IteratorT &,const IteratorT &)’ being compiled
with
[
AttrT=const double,
IteratorT=iterator_t,
ActorT=void (__cdecl *)(const char *,const char *)
]
d:\boost\boost_1_34_0\boost\spirit\core\composite\actions.hpp(109) : see reference to function template instantiation ’void boost::spirit::action_policy::do_action<void(__cdecl *)(const char *,const char *),const T,iterator_t>(const ActorT &,AttrT &,const IteratorT &,const IteratorT &) const’ being compiled
with
[
T=double,
ActorT=void (__cdecl *)(const char *,const char *),
AttrT=double,
IteratorT=iterator_t
]
d:\boost\boost_1_34_0\boost\spirit\core\composite\alternative.hpp(60) : see reference to function template instantiation ’boost::spirit::match<T> boost::spirit::action<ParserT,ActionT>::parse<ScannerT>(const ScannerT &) const’ being compiled
with
[
T=double,
ParserT=boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,
ActionT=void (__cdecl *)(const char *,const char *),
ScannerT=boost::spirit::scanner<const char *,boost::spirit::scanner_policies_t>
]
d:\boost\boost_1_34_0\boost\spirit\core\composite\alternative.hpp(60) : see reference to function template instantiation ’boost::spirit::match<boost::spirit::nil_t> boost::spirit::alternative<A,B>::parse<ScannerT>(const ScannerT &) const’ being compiled
with
[
A=boost::spirit::action<boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,void (__cdecl *)(const char *,const char *)>,
B=boost::spirit::sequence<boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,boost::spirit::chlit<char>>,
ScannerT=boost::spirit::scanner<const char *,boost::spirit::scanner_policies_t>
]
d:\boost\boost_1_34_0\boost\spirit\core\composite\alternative.hpp(60) : see reference to function template instantiation ’boost::spirit::match<boost::spirit::nil_t> boost::spirit::alternative<A,B>::parse<ScannerT>(const ScannerT &) const’ being compiled
with
[
A=boost::spirit::alternative<boost::spirit::action<boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,void (__cdecl *)(const char *,const char *)>,boost::spirit::sequence<boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,boost::spirit::chlit<char>>>,
B=boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::action<boost::spirit::rule<scanner_t>,void (__cdecl *)(const char *,const char *)>>,
ScannerT=boost::spirit::scanner<const char *,boost::spirit::scanner_policies_t>
]
d:\boost\boost_1_34_0\boost\spirit\core\non_terminal\impl\rule.ipp(233) : see reference to function template instantiation ’boost::spirit::match<boost::spirit::nil_t> boost::spirit::alternative<A,B>::parse<ScannerT>(const ScannerT &) const’ being compiled
with
[
A=boost::spirit::alternative<boost::spirit::alternative<boost::spirit::action<boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,void (__cdecl *)(const char *,const char *)>,boost::spirit::sequence<boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,boost::spirit::chlit<char>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::action<boost::spirit::rule<scanner_t>,void (__cdecl *)(const char *,const char *)>>>,
B=boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,
ScannerT=boost::spirit::scanner<const char *,boost::spirit::scanner_policies_t>
]
d:\boost\boost_1_34_0\boost\spirit\core\non_terminal\impl\rule.ipp(232) : while compiling class template member function ’boost::spirit::match<boost::spirit::nil_t> boost::spirit::impl::concrete_parser<ParserT,ScannerT,AttrT>::do_parse_virtual(const ScannerT &) const’
with
[
ParserT=boost::spirit::alternative<boost::spirit::alternative<boost::spirit::alternative<boost::spirit::action<boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,void (__cdecl *)(const char *,const char *)>,boost::spirit::sequence<boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,boost::spirit::chlit<char>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::action<boost::spirit::rule<scanner_t>,void (__cdecl *)(const char *,const char *)>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>>,
ScannerT=boost::spirit::scanner<const char *,boost::spirit::scanner_policies_t>,
AttrT=boost::spirit::nil_t
]
d:\boost\boost_1_34_0\boost\spirit\core\non_terminal\rule.hpp(135) : see reference to class template instantiation ’boost::spirit::impl::concrete_parser<ParserT,ScannerT,AttrT>’ being compiled
with
[
ParserT=boost::spirit::alternative<boost::spirit::alternative<boost::spirit::alternative<boost::spirit::action<boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,void (__cdecl *)(const char *,const char *)>,boost::spirit::sequence<boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,boost::spirit::chlit<char>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::action<boost::spirit::rule<scanner_t>,void (__cdecl *)(const char *,const char *)>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>>,
ScannerT=boost::spirit::scanner<const char *,boost::spirit::scanner_policies_t>,
AttrT=boost::spirit::nil_t
]
e:\realdodo\my project\testboostwavequickstart\cacl_plain.cpp(130) : see reference to function template instantiation ’boost::spirit::rule<T0> &boost::spirit::rule<T0>::operator =<boost::spirit::alternative<A,B>>(const ParserT &)’ being compiled
with
[
T0=scanner_t,
A=boost::spirit::alternative<boost::spirit::alternative<boost::spirit::action<boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,void (__cdecl *)(const char *,const char *)>,boost::spirit::sequence<boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,boost::spirit::chlit<char>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::action<boost::spirit::rule<scanner_t>,void (__cdecl *)(const char *,const char *)>>>,
B=boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,
ParserT=boost::spirit::alternative<boost::spirit::alternative<boost::spirit::alternative<boost::spirit::action<boost::spirit::real_parser<double,boost::spirit::real_parser_policies<double>>,void (__cdecl *)(const char *,const char *)>,boost::spirit::sequence<boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>,boost::spirit::chlit<char>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::action<boost::spirit::rule<scanner_t>,void (__cdecl *)(const char *,const char *)>>>,boost::spirit::sequence<boost::spirit::chlit<char>,boost::spirit::rule<scanner_t>>>
]
e:\realdodo\my project\testboostwavequickstart\cacl_plain.cpp(109) : while compiling class template member function ’calculator::definition<ScannerT>::definition(const calculator &)’
with
[
ScannerT=scanner_t
]
d:\boost\boost_1_34_0\boost\spirit\core\non_terminal\impl\grammar.ipp(262) : see reference to class template instantiation ’calculator::definition<ScannerT>’ being compiled
with
[
ScannerT=scanner_t
]
d:\boost\boost_1_34_0\boost\spirit\core\non_terminal\impl\grammar.ipp(281) : see reference to function template instantiation ’void boost::spirit::impl::call_helper<0>::do_<result_t,definition_t,ScannerT>(RT &,DefinitionT &,const ScannerT &)’ being compiled
with
[
ScannerT=scanner_t,
RT=result_t,
DefinitionT=definition_t
]
d:\boost\boost_1_34_0\boost\spirit\core\non_terminal\grammar.hpp(53) : see reference to function template instantiation ’boost::spirit::match<boost::spirit::nil_t> boost::spirit::impl::grammar_parser_parse<0,calculator,boost::spirit::parser_context<>,ScannerT>(const boost::spirit::grammar<DerivedT> *,const ScannerT &)’ being compiled
with
[
ScannerT=scanner_t,
DerivedT=calculator
]
d:\boost\boost_1_34_0\boost\spirit\core\non_terminal\grammar.hpp(63) : see reference to function template instantiation ’boost::spirit::match<boost::spirit::nil_t> boost::spirit::grammar<DerivedT>::parse_main<ScannerT>(const ScannerT &) const’ being compiled
with
[
DerivedT=calculator,
ScannerT=scanner_t
]
d:\boost\boost_1_34_0\boost\spirit\core\scanner\impl\skipper.ipp(131) : see reference to function template instantiation ’boost::spirit::match<boost::spirit::nil_t> boost::spirit::grammar<DerivedT>::parse<scanner_t>(const ScannerT &) const’ being compiled
with
[
DerivedT=calculator,
ScannerT=scanner_t
]
d:\boost\boost_1_34_0\boost\spirit\core\scanner\impl\skipper.ipp(153) : see reference to function template instantiation ’boost::spirit::parse_info<> boost::spirit::impl::phrase_parser<boost::spirit::space_parser>::parse<IteratorT,DerivedT>(const IteratorT &,const IteratorT &,const ParserT &,const boost::spirit::space_parser &)’ being compiled
with
[
IteratorT=const char *,
DerivedT=calculator,
ParserT=calculator
]
d:\boost\boost_1_34_0\boost\spirit\core\scanner\impl\skipper.ipp(171) : see reference to function template instantiation ’boost::spirit::parse_info<> boost::spirit::parse<const CharT*,DerivedT,boost::spirit::space_parser>(const IteratorT &,const IteratorT &,const boost::spirit::parser<DerivedT> &,const boost::spirit::parser<boost::spirit::space_parser> &)’ being compiled
with
[
CharT=char,
DerivedT=calculator,
IteratorT=const char *
]
e:\realdodo\my project\testboostwavequickstart\cacl_plain.cpp(160) : see reference to function template instantiation ’boost::spirit::parse_info<> boost::spirit::parse<_Elem,DerivedT,boost::spirit::space_parser>(const CharT *,const boost::spirit::parser<DerivedT> &,const boost::spirit::parser<boost::spirit::space_parser> &)’ being compiled
with
[
_Elem=char,
DerivedT=calculator,
CharT=char
]
我数了一下,这个错误信息总共有11355个字符,并且完全没有告诉我究竟应该如何排除这个错误……不过幸好在错误信息的最后几行中指出了真正出错的地方(真的是这样,前面的输出完全是没有用的烟雾弹……),然后利用这一点点信息,结合boost.spirit的文档,我终于知道错误的原因:我把一个函数的输入参数类型和个数写错了。
OK,看来调试这种基于Expression Templates的库真的需要耐心和勇气。
