善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。

2017-12-27

Linux关机权限的特殊情况

很多Linux教程都说shutdown/reboot需要root权限。其实这不是完全正确的。

正常想来没错:shutdown/reboot如果可以不用root权限那还了得?然而其实至少在RHEL 7.2上并不绝对是这样子。当出现以下的特殊情况时:
1.是当前唯一登录的用户;
2.直接调用reboot,或shutdown带了now参数;
3.登录会话来自本地物理终端。
同时满足以上所有条件的话,就可以无需root权限以任何用户的身份关机或重启服务器。

我用RHEL 7.2默认安装的「基础设施服务器」进行的测试。Debian 9没有这个问题。我想这样的做法大概思路是:「如果你都摸到物理服务器边上,急着要马上关机,并且我认为这不会影响到其他人,那么当然可以让你关机,因为就算不让你关机你也可以拔电源线对吧?」

当然,这种想法应该没有考虑到服务器上运行爬虫之类的情况。我没有做更多测试,也许如果有一个别人的daemon进程就会使得结果不一样。不过我想这个事情可以提醒我们的是:Linux的事情不要想当然,也不要网上说什么都信。不要把一切都交给系统默认安全性设置,对于普通用户还是乖乖地把权限控制严格点比较好。

2017-06-22

防贼不编年史

Chapter One

很早的时候,赛博世界还只有病毒,没有贼。
那时候,有的病毒还可以和人类和睦相处,称之为「良性病毒」。
我有一个游戏,从别人那边复制过来的时候,就是带毒的。文件型病毒,文件体被加了密,还给搬到了隐藏扇区。不过游戏本身挺有意思,The Incridible Machine,第一代。于是我每次就把 BIOS 里的硬盘给 Disable 掉,然后再玩。慢虽然慢点,但只要记得玩过之后重启,就不会有什么问题。

Chapter Two

尽管兜里没什么钱,但 Internet 时代还是到来了。
网管差点把我从网吧赶了出去。因为他终于「逮到」我在用 SuperRabbit。他已经连续好几天都把时间花在恢复系统设置上了。我想日后 硬盘还原卡 卖得这么好,其中应该有我一份功劳。
「机器狗」真不是我放的。

Chapter Three

CIH 爆发了。
买了刷 ROM 机器的人都发了笔小财。
其他人:哇,没想到病毒也可以这么凶残!
我倒是没什么感觉,因为我那个时候还在用着一块 80486。

Chapter Four

IDT-C6 的发热好低,主频超到 100MHz 也不用风扇。
哇,有好多同学都安装了 冰河 哎!
咦,这位同学在玩美少女梦工场 3 呢。我只有 2。
「同学你好,你的游戏可以也 copy 给我一份吗?哎哎,不要关机!……」

Chapter Five

不知道从什么时候开始……
几乎所有的 IT 公司,都以在你的 IE 上装一个插件或工具栏为荣。
几乎所有的安装包,都会附带一两种「小东东」。
刚开始我还真没太放在心上,甚至对某些还持欢迎态度。然后,我的机器越来越慢了。
做毕业设计时,我接触到了 ActiveX。

Chapter Six

工作了。从 K6-2 一下子跳到 Pentium 4 的感觉真好。
「见鬼,这浏览器上什么时候多了这么些鬼东西?上网助手?中文实名?卸掉!就是它搞得我们的 OCX 不能用了。」
「你要记得安装 Windows 2000 SP4 补丁,不然我们的程序用不了。」
对了,有个新软件叫 VMware,真好玩!

Chapter Seven

我开始小心翼翼地上网浏览,对于弹出的 OCX 安装提示统统点「否」,后面视情况而定。因为我知道一旦安装了之后别人能做些什么事情。然而,有些人我是注定帮不了他们:
我:「你的 IE 上装这么多工具栏干什么?」
A:「不然我怎么上网?」
我:「Google 的网址是三达不溜点……」
A:「别说了我记不住。」
愿上帝保佑他们,阿门。

Chapter Eight

换工作了,当小头头了,总算有双核电脑用了。VMware 可以有自己单独的 CPU 了。
真不敢相信,以前在一台 768MB 的 P4 笔记本上跑了一个 Oracle8i + Lotus Domino R5 + 两个 Resin + Word + 若干 IE,去招标现场做演示的时候,机器到底是怎么撑下来的。

借着新工作接触到了 IceSword。妖魔鬼怪你们都现出原形罢!
测试人员:不好了,机房又爆发「震荡波」了。

Chapter Nine

我开始仔仔细细地打补丁,开启 Windows Update,总是保持自动更新。
而很多同事都是直接关掉了事:「工作到一半老是跳出来叫我更新,太烦!」
愿上帝保佑他们,阿门。

Chapter Ten

我发现同事们总算愿意给机器打补丁了。
同事:这个东西叫 360,打补丁蛮快的。
我:我也来试试。咦,这个 KB360018 怎么这么奇怪?

Chapter Eleven

不知道从什么时候开始,软件安装开始必须得小心了。
因为一不小心你就会安装上好几个不请自来的软件,电脑上会变成软件博物馆。送来给我「修一下」的电脑无一不是如此。连我自己有时候不小心也会中招。
有人建议我把家里电脑换成 Linux,不过我觉得对父母吩咐到位了,一般还是没事。毕竟我有 TeamViewer

Chapter Twelve

不知道从什么时候开始,国产软件慢慢地不能用了。
其实不是不能用,是不敢用了。因为开始流行一种东西叫做「全家桶」。腾讯、百度、迅雷、阿里、360、金山……。所有的这些曾经为我服务过的软件,仿佛都得了癌症。你不知道他们在背后干些什么,但是你的电脑的确越来越慢,行为越来越不正常了,而且你的 hosts 总是失效得很快。于是干脆就不用了。
不用了,然后也并没有什么事情发生。并没有像有的人以为的那样会社会大乱,民不聊生。人民照样活得很好。

Chapter Thirteen

一夜之间,我的密码就不再是「我」的密码了。
许多年前,我自己写的同学录,为了避免被 SQL injection,就把服务端存储的密码改成了 MD5 Hash。
后来,知道了世界上有种东西叫做「彩虹表」,于是我学会了 salt
再后来,我看到了 王小云教授 的论文,于是我 Hash 算法至少会是 SHA256。
然而,这么多年过去了,这帮狗日的居然还在服务器上存 明文密码。谁要硬说这里面没有阴谋,我只能说你的心挺大。

Chapter Fourteen

没想到,我的 QQ 也被盗了!
我已经很多年没用过 QQ 了。但是因为我太太的手机上有游戏用我的 QQ 账号登录着,所以我觉得企鹅还不敢回收我的账号。
突然间它就变了一个名字,列表里的好友也变成了一堆海南人。我还能再登进去,密码并没有被改。所以我一直到现在也没想通是怎么回事。
我把个人状态改成了「明文密码好」,然后就把这个 QQ 号扔那里了,就像它从未存在过。

Chapter Fifteen

那个周末,当我还在外地顶着紫外线用流量上网的时候,全世界有许多人已经  了。
贼要的是 Bitcoin
当第二天 IT 问我笔记本装没装补丁的时候,我微笑着告诉他「Linux」。
不过我马上又想起来 Heartbleed 的事情我还没处理,于是我又有点笑不出来了。
我也想要别人的 Bitcoin。

2017-06-13

银联钱包你真垃圾,我一点都不欢迎你

5月30日一下飞机,我就在虹桥T2的廊桥里面看到了银联云闪付62折的大幅营销海报。「去看看有没有便宜可占」,当时我这样对太太说。然后我们就各忙各的,这事几乎给忘了。

昨天太太跟我说,超市里面银联云闪付满79减30,所以她在自己的iPhone6+上装了个银联钱包。我想起了在机场看到的东西,就也去AppStore上搜了一下。有两个东西,一个叫「银联钱包」,一个叫「云闪付」,都是「中国银联」出品的,评分还真是一样低。我有点纳闷,好吧两个都下了,反正AppStore上至少没木马。

安装好之后把玩了一会儿,「云闪付」一来就叫我登录,而「银联钱包」至少让我看到里面有些什么功能了,里面也有「云闪付」的功能,那么好,「云闪付」你滚蛋吧。

其实我知道Apple Pay本来就是所谓的「银联云闪付」,我只是想知道银联这次在玩什么花样。结合新闻我有点弄明白了:大概闪付需要芯片,很多地方只肯买扫描枪,所以银联这次也搞了个跟支付宝有点像的扫二维码支付。这就必须要App支持,光靠Apple Pay还不行。

好吧,我觉得至少比起阿里粑粑这种流氓公司而言,银联我还可以尝试一下。于是我准备注册了……

我真的没想把本文写成吐槽文。不过正式开始用的第一眼我就被雷到了。拜托!中国银联!这是iOS,是哪个老师教你「自己开发密码键盘会更安全」的?你说出来我们来轮他。

先得注册。我在界面上看到了可以用「手机号」、「邮箱」或「用户名」来注册。我并不是一个暴露狂,所以我准备以用户名来注册。点开App上的注册,发现只能用手机号注册。我不死心,换到PC上在Web下注册。一开始Web页面看起来也只能用手机号注册,不过当我F12之后,就发现还有一个DIV被display:none了。
呵呵,我心想:「就这也能难住老子?」
看来还是绕不开。

然后还有这个令我一看到就恶心得头皮发麻的「点此安装」。
我用的是Chrome,要我换用Edge甚至是IE我都可以接受。不过要我安装OCX那就太过分了,。有的Web页面如果用手机浏览器打开,就不会提示安装控件了。我抱着姑且试一试的态度,用手机试了一下:
Chrome是这样,Safari也是这样。
好吧,反正就是必须得要手机号对吧。那我还是回手机App上去注册吧。

在App上输入手机号码,通过短信发了验证码给我,验证通过了,接下来让我输入密码。这明显的大爷作风嘛。我很想问银联你是不是并不在乎有没有用户来注册?反正老子是国企,用户多一个少一个无所谓?你们知道像美团之类的App在这一步是怎么做的吗?
我又要吐槽了。最低6位虽然太少,但我可以理解。最多不能超过16位是个什么意思?你们如果后台数据库里面保存的真的是密码的Hash值而不是原文,你管我密码最大有多长?你们知道Twitter允许的密码最大有多长吗?你们可以自己去试一下。
明文密码!我一直不厌其烦地在Twitter和Google+上强调这个事情:只要没有特别的理由就限制密码的最大长度,那后台保存明文密码的可能性就一下子变得高了起来。不要跟我讲什么16位现在还足够安全。关键是撞库!撞库!撞库!
我一直是坚持「为每个服务使用独立随机密码」这个原则的。我宁愿忘掉密码,宁愿冒本地密码本被人搞走的风险,我也不会把心放在Server管理人员身上。原因很简单:做过这行你就知道了。
所以,我开启了密码生成器,去掉「符号」的勾,选择了长度为「16」,然后得到了一串随机密码。知道我前面为什么吐槽自己做的密码键盘吗?你既然决定了自己做密码输入控件,就很可能不会支持复制粘贴。
在我比较辛苦地输入完密码之后,出来一个这个。
估计我以前在什么时候注册过银联的账号吧。有句妈卖批我不知当讲不当讲?!我输入手机号码的时候你不告诉我,我输入短信验证码的时候你不告诉我,我输入密码的时候你不告诉我,等我把这些都搞完了你就告诉我这个?!
抱着一丝希望,我点下了「是我的,立即登录」按钮,App跳回到了最开始的登录界面,输入我刚才生成的随机密码(是的我又辛苦了一遍),说我密码错。这次我真的骂人了。
好吧,看来我只能选择「忘记密码」了。
在又通过短信验证了一遍手机号之后,给我看的是这个界面。
我曾经在Blog中讲过,「密保问题」并不是一个好的设计。不幸的是,银联这里选的是所有密保问题中最糟糕的那几种之一。如果真的用家人生日做密保问题的答案,安全性极其脆弱,有等于无。如果用别的答案,又极容易忘记。
偏偏这里不回答还不行,又没有提供「忘记答案」的选项,实际上是把这条路给堵死了。其实手机短信验证的安全级别明显比密保问题要高。取信低安全等级的验证结果,而忽视高安全等级的验证结果,最后只能请求人工服务,这是极差的用户体验,也是极蠢的产品设计。

如果我记不起这个以前注册的账号的密码,我可能就只能打电话给银联了,而且可能这帮官腔佬最后还不肯替我解决。不过很幸运,我用一个旧密码最后成功登录了进去——看来我注册的时代还不太「古老」。
然后我又被雷到了。登录成功之后,App给我看了这个:
是的,没有「跳过」或者「稍后设置」的选择,这一步是必须的。
设置手势密码之后才可以设置TouchID。好在根据太太的经验,登录进去了之后是可以把手势密码功能给关掉。不过问题又来了,要进入「安全设置」你必须要:
到这一步,我彻底放弃了。
极其差劲的技术运用,极其糟糕的用户体验。难怪AppStore上绝大多数评价都只给了一星,而近期的五星好评全像是刷出来的。我估计如果AppStore不是规定最低是一星,很多人连这一颗星都不想给。
说实话,我不相信这种程度的技术能够保障我的资金和信息安全,给我天天打62折我也不敢用。算了。卸载。再见!


2017-05-31

淘宝天猫都碰不得啊

前言
六一儿童节快到了,给儿子买点什么礼物好呢?虽然前不久刚买了一架UH-60和一把Glock17,但我还是想买点对儿童稍微有点教育意义的东西。思来想去,觉得地球仪不错。正好暑假也准备带家人出国去转转。若是连自己去了哪里却一点概念都没有,那岂不是很遗憾?

平时工作也算得上忙,所以我就偷懒打算在网上购物。碰巧近来网购的体验还不错,正所谓好了伤疤忘了痛,我又开始在淘宝上逛了。

其实以前我是很吃过几次亏的,所以也曾经痛下决心告诉自己大部分东西都不能在淘宝上买了。但前不久那次买玩具的感受实在是不错:我擦!居然有金属的Dragunov?这次一翻地球仪,我更是震惊了:我擦!居然还有磁悬浮的?!

所以说啊,人必须时刻保持冷静,才能抵挡诱惑。头脑发热之下,我真的就下单买了个所谓的「磁悬浮地球仪」。带着残存的一丝丝理智,我选了一家天猫店下单。我觉得吧,C2C的淘宝不靠谱,B2C的天猫大概好一点,至少出了问题比较好解决。事后证明,我这想法也对也不对。

糟糕
等待收货的过程还是蛮引人遐想的。然而收到货一拆包装,首先心里凉了一半:包装倒没有什么特别的问题,不过产品外包装上连半个汉字都没见到,没有品名,没有厂家名称,没有商标,连个LOGO都没有。好在我是懂英文,加上有个图片,还能看出来没发错货。
打开盒子,里面除了地球仪和一些填充物,就只有一张非常简单的「说明书」。合格证、保修卡,什么都没有。这它妈的不就是传说中的“三无产品”吗?
好吧,网购的东西,要求别那么高,毕竟地球仪才是主要的。我对它有着不小的期待,因此心里倒也没怎么在意那些「细枝末节」的事情。看了看说明书,通上电源,开始折腾。折腾着折腾着,汗就下来了。
要完成所谓的「悬浮」,好困难啊!

虽然买之前看过一些评论,算是心里有所准备,但还是没想到这么麻烦。送的那个什么「悬浮棒」根本就不好使。还是有个评论里面说用中性笔的办法比较管用。好不容易能够比较顺利地「悬浮」上了,然而只要有一点点扰动,平衡立马就被打破,通常的结局就是地球仪被「铛」的一声吸到顶上。而且就算是很小心地退开,一般也坚持不了几分钟。我发现这大概跟加工精度有关系,因为顶上的磁体是斜的,而我无法去校正。
并且,产品说明里面提到的自动旋转,似乎也无法达到。球体也就是左右来回转,每次角度也就是几十度。我定睛看着它,发现角度在越来越大,正当我满心希望它最终能转一个整圈的时候。「铛」,它又被吸到顶上去了。

这它妈让人怎么用?我不可能每天都花一两个小时的时间在折腾这东西上。睡觉时还不能开着,不然保管神经衰弱。要是只能摆着或吸到顶上,那几十块的普通地球仪岂不是更好?我干嘛买个这么贵的银样蜡枪头?
不行,退货!

退货
在淘宝上买东西,顺利的话一切都好。然而一旦买到糟糕的玩意儿需要退货,那麻烦就来了。我以前的糟糕的购物经历无一不源自于此,这次当然也不例外。当我以「质量问题」为原因要求退货时,卖家拒绝了。我对此也并不意外——反正最糟糕也就是哪样,总不可能直接投降吧。

卖家拒绝的理由看起来很好笑——产品有正规工厂专利进货发票。这话好眼熟啊!对了,我以前在某个论坛上看卖家们交流心得时提到过,如果被投诉三无产品,就这样应对,只要你拿得出发票来,淘宝小二可不管它是不是真的……。呵呵,这样的发票当然可以买。有些买的发票连税务局那边都能过,何况淘宝。淘宝的处理方式很简单,C说你卖的是假货,你B有发票吗?噢,有,那C你去证明B卖的是假货……
我去你妈的。

这个时候我看到了一个「极速维权」的按钮。我点了一下,叫我提交证明。
我能怎么证明呢?我只能把上面那两张图片传了上去。产品不能正常使用,我能怎么证明?上传一段小电影吗?搞笑。我都能想得到接下来的过程,无非就是叫C去找质监局,出具鉴定证明。普通消费者常常到了这一环节就望而却步了。较真的可能会去工商局投诉,然而一般也是了无下文。

要说起来,这个环节是很给「天猫」加分的地方。我刚上传了照片不久,天猫说因为我是「信誉良好」的顾客,所以给我先行赔付了。的确也是马上就是进入到退货发物流的环节了。听明白了吧——东西真不真我不表态,但是我相信你!
很多时候,事情就这么摆平了。你要认真按「假一罚N」地去索赔,那可就没这么容易了。职业打假人毕竟是少数。这一招就叫做「分化瓦解」、「团结大多数,孤立一小撮」。

给东西重新装箱的时候,我又留意了一下这东西的做工,才发现其实粗糙得很,根本不值169元这个价钱。所谓专利号,搜了一下是宁波一家公司的,跟淘宝商家八竿子打不着。如果光看成本,20元都不一定有。尽管到淘宝上买东西没一个不是想贪便宜,但你一定要相信,卖家不会让你真正占到便宜的。

运费
我记得,我上次在Blog上吐槽淘宝的事情,最后问题也是出在退货的运费上。这次又是「也不例外」。可见,如果那里的确有一个坑,你再走几次也还是会掉进去的。

天猫很「贴心」地提供了「上门取件」服务,还可以约时间。说实话,当时我真的有点小「感动」,几乎就要拍手叫好了。

取件的人来的时候,我在上班,是我太太处理的。取完件我立马就收到了退款。然而收快递的人跟我太太说,运费本来是7元,但是要收11元,因为「你的东西超宽了」。
好吧。我心想,反正质量问题的退货运费是应该由卖家承担的。所以我也就没有坚持太多。收快递的人(我不想用「快递员」一词称呼他,因为我对他有意见,我觉得他不配)叫我支付宝直接付掉,我打开手机的淘宝App,的确有付款按钮。我点开看,7元。

怎么回事?也许是对方还没提交新的价格。我就又做了一些手头的工作。一个小时以后再去看,还是7元。
7元就7元,我心想,也许系统不觉得我的东西「超宽」。付完款没多久,太太说快递员打电话来,叫补4元运费,给了支付宝地址。
妈的,现在想起来补运费了?

结束
讲个笑话:太监下面有什么?
什么都没有了。

质量问题导致的退货,运费不是由卖家承担吗?如何承担?要我「垫付」已经是足够糟糕的体验了,然而现在是要我去找卖家聊IM来「讨」吗?
我最憎恨的就是这种「干点什么都要IM」的事情。而前几次还算「愉快」的购物体验,也正因为我「不用到IM上做任何事情」。

好吧,既然天猫觉得这个事情「到此为止」了,那我对你淘宝/天猫也就「到此为止」吧!
其实总结下来,写了这么多,无非是想让自己记清楚这些个教训。有钱留着去国外花,别它妈犯贱去交智商税。

2017-05-25

将C++11新特性用于代码优化

关于C++11的科普,在这里就不详细进行了,可以参考维基百科页面。即使是中文页面,我认为写得足够详细和系统了。

总之,C++11对原始的C/C++作出了在我看来是不算小的改动。有一些概念,放在以前的时代是绝对真理,在C++11推出之后,可能需要重新了解一下了。VS2013对C++11的支持并不算「完美」,不过大部分「有用」的特性还是到位了。这里就以它为例,来谈谈如何把C++11的新特性应用到你的软件开发工作中来提升性能和开发效率。

本文提到的C++11的这些新特性,我大致把它们分为两类:一类是可以直接提升代码的性能表现的,我列在「性能优化」部分;另一类虽然不能直接提升代码的性能,但可以提升开发效率,便于更快地开发出可维护性更好的代码,我列在「非性能优化部分」。

另外,受作者水平所限,本文并不是对C++11在这些方面的完整的参考内容,仅仅作为一个引导来阅读吧。


性能优化部分

右值引用和move语义
C++11引入了右值引用,支持了move语义。在我看来,这个变化的意义可能是C++11里面最大的一个。右值引用和move语义是什么,这里不展开。通俗一点地讲,这个特性使得程序员可以在必要的时候自行决定到底是深拷贝还是浅拷贝。对于大量的数据「搬运」操作,可以节省下不少时间。对于性能优化来说,意义重大。

其实就算没有右值引用,在C++11之前的时代也可以做类似的优化。C++程序员只要对于自己的资源管理类显式地提供深/浅拷贝版本的函数即可。不过这样一来代码工作量会比较大,程序会变得比较复杂,并且始终不是一个规范。现在这一切都不是问题了。

对于STL自己的类/容器,VS2013已经做了足够的优化。例如,你可以通过:
string strA = std::move(strB);
来把strB的字符串动态内存部分直接给到strA,速度比简单的赋值要快不少。当然,strB就不再具有有意义的值了(这里例子中会变成空字符串)。当你push_back或insert一个string到容器里面的时候,如果string其实是一个临时变量,那么用move语义你也可以得到相当明显的性能提升。

如果例子里面string换成一个map<string, string>,那么提升会更明显。总之,内存的分配和释放,以及memcpy操作统统被避免了。所以,理论上需要传递的东西越多,你得到的性能提升就会越显著。

就地部署(emplace)
C++11对于常见的STL容器,都提供了一种能提升性能的数据置入方法,称之为「就地部署」。通过用就地部署取代原来的push_back或insert之类的操作,不再需要先构造再传递,而是由容器直接调用目标对象的构造函数来完成数据填充。

在某些情况下(T提供了对应的构造函数时),这样可以避免一次拷贝构造的开销。而最差的情况(T没有提供对应的构造函数),也最多不过就是与push_back和insert效果一模一样而已。所以我建议所有能用上就地部署的地方,都统统用上,无需太多考虑。

并且,就地部署与move语义相互并不冲突,而且是互有补充。move语义解决深拷贝慢的问题,就地部署试图减少哪怕是浅拷贝的执行次数。两者配合起来效果更加完美。

散列表
在C++11里,不再需要通过第三方库来引入散列表(或者叫哈希表)了。STL正式支持了四种散列表的实现,全部都冠以「unordered_」的前缀,以便与一些第三方实现相区别。

对于大多数用map/set实现的代码,只要简单替换容器就可以得到性能上的提升。map/set基于红黑树(自平衡二叉树),时间复杂度至少是log(N)。散列表版本的map/set提供常数级的时间复杂度,随着数据量的增大,无论是写入还是读取的性能都超过了红黑树版本的map/set。

我个人的测试结论是:同是set<string>,即使是小数据量,散列表版读取代价也只是红黑树版的约60%;小数据量下,红黑树版写入略快,但在容器内数据量达到「万」级别的时候,散列表的写入速度也开始超越红黑树版(此为Release版测试结论,Debug版在「百」级别即发生超越现象)。

所以我认为,只有在数据量很小,并且写入与读取的概率大致相当时,使用红黑树版map/set才在性能上可能有明显收益。其余情况,都建议采用散列表版本map/set。当然,如果T是自定义类,并且你不愿意为它写散列函数,那就算了。


非性能优化部分

完美转发
C++11中所谓「完美转发」的特性,其实是配合右值引用来使用的。如果为了支持右值引用,而不得不让自己的代码量变大一倍,那有些人可能就要望而却步了。完美转发其实是借用了模板技术,使得你可以只写一份代码,就可以兼顾(常量)左值引用与右值引用的情况。工作量更少,代码更简洁,出错的概率也就更低。

不过,采用模板技术的缺点就是:编译期展开。这一方面降低了编译器的效率,另一方面会导致头文件的包含关系变得不太容易整理。除此之外,还有一种我称之为「不完美转发」的替代解决方案,本质上是在性能上作出一定程度上还算可以接受牺牲,来换取代码简洁性,取得一个还算OK的平衡。我会另外写一篇Blog来介绍一下它。

类型推导
「类型推导」也就是所谓的auto类型。这个东西使用起来基本没有门槛。很多人可能最开始接触C++11就是通过它了。

这个的确是一个好东西,用来写STL的iterator类型再合适不过了。因为我们本来也不怎么关心iterator的具体类型。不过,仍然不建议滥用。如果到处都是auto,阅读你代码的人会经常性地需要回顾才能知道一个变量的类型,特别是在你没有用匈牙利命名法的时候更是如此。

所以,我的建议是:当你觉得一个变量的类型写起来很麻烦,而你其实并不关心它的时候,放心地用auto。并且,auto变量的作用域不要太大,if/for/while循环内的局部变量用它是最合适的。

基于范围的for循环
很多语言早就可以这样写了。而C++11现在也可以这样写了:
for (auto& stk : stocklist)
相比起:
for (auto pIter = stocklist.begin(); pIter != stocklist.end(); ++pIter)
孰优孰劣一目了然。何况后者通常还需要跟一句:
auto stk = (*pIter);
不过,如果是一个map,你可能经常要取pIter->first/second之类。或者你打算在循环里面对pIter做erase操作,那还是用传统方式比较好。

空指针
用nullptr取代NULL。我觉得最大的好处就是nullptr的颜色没有NULL扎眼。不过,由于NULL也表示0,有的时候也表示无效句柄。我觉得对于所有指针类型的NULL,置换成nullptr可能会对阅读代码有一定帮助。

角括号
C++11的编译器现在可以识别>>到底是两个模板类的嵌套,还是>>运算符。因此写代码的时候就不特意空上一格,写多层模板类嵌套的时候就更美观一点。

不过,多层模板类嵌套,本来就不可能「美观」到哪里去。起码我是不建议太多此类的代码实践的。

初始化列表
vector可用这样的方式来进行初始化:
vector<int> vecX = { 1, 2, 3, 4 };
的确是比以前省事了。也就是说,C-Style数组的存在意义又少了一层。

统一初始化
struct可以被这样初始化:
struct C
{
    int a;
    int b;
    int c;
};
C c{1, 2, 3};
class的public成员也可以。
在某些喜欢使用各种结构体的代码中,这个特性可以让你少写一大堆构造函数。

通用智能指针
std::shared_ptr<T>,强在可以指向任意对象,缺点也由此而生:由于引用计数保存在shared_ptr中,因此对智能指针的赋值操作是线程不安全的。这个问题,有一篇Blog论述,我觉得写得不错,就直接引用不细讲了。从原理和测试数据来看,我认为这篇Blog是靠谱的。

所以,虽然shared_ptr很强大,但使用场合需要注意:单线程随便用。多线程下,赋值过程要注意。单对单没啥问题,最好不要出现左值右值交叉的情况(一个线程在A=B,另一个线程在B=C)。若因业务需求无法避免的话,要考虑当作临界资源加锁保护。实在不行,就写一个专用智能指针,把引用计数放在T里面,就不会有问题了。

正则表达式
与散列表类似,不再需要第三方实现,现在C++11也直接支持正则表达式了。我以前要找一个Unicode支持得好的Regex库真的是苦水一堆,现在有了官方支持真的是太好了。

2017-05-23

招行网银在Win10下糟糕的支付体验

我这人不用QQ,不用微信,微博借王维林也成功销号。然而,支付宝因为早年上淘宝购过物,搞过那个什么实名认证,我评估后认为不用比用下去可能更不安全,所以账号勉强留了下来。不过我也给自己定下了两条规矩:
  1. 不绑定银行卡的快捷支付。
  2. 不在里面保留超过200元人民币的余额(含余额宝)。
这两条规矩,使得我的支付宝平时基本上当作零钱包来使用,正式的支付只用在PC+网银上。这样,即使失窃,损失也很有限。再说,花钱方便是方便什么?方便你「败家」而已嘛。

因为我自己定的这些规矩违反某些方面的利益,所以经常给我形成一些耐性上的挑战。昨天我就又遇到了一次,来自招行网银+Win10。
没有足够的余额,又不开快捷支付的话,在支付宝上支付一笔淘宝订单就只能通过网银了。我以前一直用招行网银,虽然它对Chrome和x64很不友好,但我还是耐着性子开x86 IE来在这种j「特殊场合」将就一下。反正也就是最后一步嘛,登录Alipay集中支付一下就好。但昨天在Win10机器上我又发现了新的问题。

Edge不支持
Edge登录支付宝是挺顺利的,但招行网银的页面一打开,直接说这是「使用陈旧技术的页面」,估计是因为ActiveX。要想跨Browser提交表单估计还是做不到的,所以最后还是只有在Win10下打开Internet Explorer。

无管理员权限登录会失败
IE下几经折腾,可以打开招行的网银专业版了。然而插入UKey之后一登录就跳出Dialog让我输Key的密码。在确认不是钓鱼软件的情况下,连输4次后,专业版登录界面弹出提示说证书签名有问题云云。
我估计这问题就是因为权限不到位,果然给了管理员权限后就能正常登录了。然而专业版登录界面是从IE页面上Call出来的,这就意味着我得给IE管理员权限才行。这样的话风险就有点高,还好可以手动做单次授权。

以前我一直觉得,金融行业在IT技术应用上普遍保守,而在国内放眼看来,招行在技术方面还算是相对进取的。现在看来情况已经反转了。x64都普及多久了?IE份额离开绝对优势地位都多久了?Win10正式版都上市多久了?到现在还在用着ActiveX。招行里面搞技术的人这些年都不知道在干啥。曾经引以为傲的服务水平也许还没下滑,但也绝对不算突出了。我是不是该换一家主力银行了?

2017-05-22

VMware+Ubuntu声卡失效事件

在公司和在家里,都用VMware各自安装了一台Ubuntu 14.04 LTS来玩。在家里的一台用得没有什么问题。在公司的那一台,周末打算加班的时候装个网易云音乐来听歌的时候,发现没有声音,才注意到VMware上有一条报错提示:
使用的设备标识号已超出本地系统范围。 声音将中断。
公司电脑上没有接音箱,所以以前曾经禁用过宿主机的Windows Audio服务。我以为是这个原因,去看了一下,Windows Audio服务现在是启用中。把宿主机重启过,故障依旧。于是循例开始Google。

Google上搜到的中文内容,主要分为两派。一派说把pulseaudio卸载了就好了。我半信半疑地apt-get remove pulseaudio之后,嘿,还真的可以播放出声音了。不过更大的问题来了:系统设置丢了好多图标。回想起apt-get提醒我说要卸载掉的东西有一大堆,看来依赖于pulseaudio的东西不少。这条路应该不是什么正路。真是还好VMware有快照。

另一派说把宿主机上的立体声混音设备启用,故障就解决了。附和的人不少,看来有不少人都是这种办法解决的。具体页面有很多,随随便便就能搜到,我就不给出了。然而当我按照附带截图的操作指南去做的时候,问题又来了:我根本没有立体声混音设备。

这是怎么回事?我这人也不习惯卖关子。要说还是英文信息有用。英文页面上也有少数几个人抱怨遇到与我同样的问题(中文页面上我没有看到过)。最后还是VMware官方社区给出了有用的解答

要简单解释一下的话,其实就是这么一回事:我这个宿主机上的Win7当初装起来之后,偷懒没有安装Realtek官方的声卡驱动程序,而是直接Windows Update安装了微软给出的驱动。估计微软的这个驱动是个阉割版,缺一些东西,装了之后虽然使用起来没什么问题,但像我这次遇到的什么立体声混音设备,大概就是被阉割掉的内容之一。所以VMware找不到指定的设备,于是就没法让Ubuntu中的声音设备正常工作了。

总之,按照VMware官方解答的指引,我去Realtek官网上下载并安装了声卡驱动,现在VMware里面的Ubuntu可以欢快地播放音乐了。

2017-05-21

正确地获取 Windows 的版本号

以前,想要获取 Windows 的版本号很简单,有个 Win32 API 函数名字叫做 GetVersion,望文生义,接下来要做的事情就是去 MSDN 上查下用法就可以了。

现在,GetVersion 会被报告成「过期函数」了。也许还能用,但(据 MSDN 说)起码在 Win10 上是别指望得到预期的结果了。道理也很简单,Win10 都搞滚动升级了,版本号规则肯定也和之前不一样了,你还指望这么老的函数能兼容么?

别痴心妄想了,GetVersionEx 也一样过期。那么,眼下有什么好办法吗?

一般来说,拿 Windows 版本号可能有两种用途:

  • 我想看看你 Windows 版本达到我要求没。
  • 我就是想知道你 Windows 版本号是多少。

对于前者,微软现在在 MSDN 上是这样推荐的:它做了一组 Version Helper functions,你如果想知道当前 Windows 的版本是不是某个特定的发行版,调这组函数就可以。我们来看看这组函数中三个典型:

  • IsWindowsXPOrGreater
  • IsWindowsXPSP3OrGreater
  • IsWindowsServer

不需要更多说明,我们从名字中就可以看出,这组函数可以用于判断 Windows 的大版本,Service Pack 的版本(结合大版本),以及能知道是不是服务器版操作系统。通常情况下,这些函数大概是够了。

但是,有的时候我们并不关心版本号高低,我们只是想要一个版本号(例如记录日志时)而已。微软对此的建议是:用 GetFileVersionInfo 去获取一个系统 DLL(例如 Kernel32.dll)的文件版本号(原文看 这里)。

相关的代码虽然能找到,MSDN 上也有官方例子(有点小 Bug),但比起一行 GetVersion 来代码量实在是不能算很少。由此可见,处理「过期函数」真的没有想象中那么容易。最后我还是提供一下我从项目代码中挖出来的一个实现吧。别照抄,如果你不想引入 STL 的话:

#include <windows.h>
#include <Strsafe.h>

#pragma comment(lib, "Version.lib")

// 获取文件版本
std::wstring GetFileVersionString(const std::wstring& strFilePath, bool bStrVer = false) {
    DWORD dwVerInfoSize = GetFileVersionInfoSize(strFilePath.c_str(), nullptr);
    if (dwVerInfoSize) {
        std::vector<BYTE> vecVerData(dwVerInfoSize);
        if (GetFileVersionInfo(strFilePath.c_str(), NULL, dwVerInfoSize, &vecVerData[0])) {
            LPCVOID pBlock = &vecVerData[0];

            UINT cbTranslate;
            TCHAR SubBlock[MAX_PATH];
            struct LANGANDCODEPAGE {
                WORD wLanguage;
                WORD wCodePage;
            } *lpTranslate;

            // 阅读语言和代码页列表
            VerQueryValue(pBlock,
                L"\\VarFileInfo\\Translation",
                (LPVOID*)&lpTranslate,
                &cbTranslate);

            if (bStrVer && lpTranslate) {
                // 读取第一种语言和代码页的文件版本
                for (size_t i = 0; i < (cbTranslate / sizeof(struct LANGANDCODEPAGE)); ++i) {
                    StringCchPrintf(SubBlock, sizeof(SubBlock) / sizeof(TCHAR),
                        L"\\StringFileInfo\\%04x%04x\\ProductVersion",
                        lpTranslate[i].wLanguage,
                        lpTranslate[i].wCodePage);

                    LPVOID lpBuffer = nullptr;
                    UINT dwBytes;
                    if (VerQueryValue(pBlock, SubBlock, &lpBuffer, &dwBytes) && lpBuffer && dwBytes > 0) {
                        std::wstring strVersion(reinterpret_cast<TCHAR*>(lpBuffer));
                        return strVersion;
                    }
                }
            }

            // 未找到任何字符串版本
            VS_FIXEDFILEINFO* lpffi = nullptr;
            UINT uLen = 0;
            // 注意:这里的第二个参数 "\" 是固定写法,表示查询根块
            if (VerQueryValue(pBlock, L"\\", (LPVOID*)&lpffi, &uLen) && lpffi && uLen >= sizeof(VS_FIXEDFILEINFO)) {
                std::wstringstream wos;
                wos << HIWORD(lpffi->dwFileVersionMS) << L"." << LOWORD(lpffi->dwFileVersionMS) << L"."
                    << HIWORD(lpffi->dwFileVersionLS) << L"." << LOWORD(lpffi->dwFileVersionLS);
                return wos.str();
            }
        }
    }
    return L"";
}

// 获取 OS 版本信息
std::wstring GetOSVersion(const std::wstring& strWinSysDir, bool bStrVer) {
    std::wstring strWinSysFilePath = strWinSysDir;
    if (!strWinSysFilePath.empty() && strWinSysFilePath.back() != L'\\') {
        strWinSysFilePath += L'\\';
    }
    return GetFileVersionString(strWinSysFilePath + L"Kernel32.dll", bStrVer);
}

2017-05-20

SDL检查不报错事件调查报告

Visual Studio 2013 VC项目默认是启用SDL检查的。通常而言,这会使得一些“过期”函数在编译时被报告Error。比如strcpy和inet_addr之类都会遇到这个问题。

理论上讲,这些过期函数的确不安全,或者说容易被不安全地调用。微软也很“贴心”地在编译器的报错信息中给出了解决方案,比如用strcpy_s和InetPton来替换,都不用你去搜解决办法了。所以按照我的习惯,一般是就地解决掉这些问题再往下走。

不过呢,可能有的人性子比较急,也有的时候是从旧项目移植,想快点编译完先跑一下看看效果。改代码毕竟要时间,从strcpy改成strcpy_s可能还好,而从inet_addr改到InetPton就真的没有想象中那么轻松。所以编译器也给出了另一种建议,你可以设上几个Macro,SDL也是可以被忽略的。

到此为止都还比较和谐,大家都是有商有量地做事情。然而当我腾出时间准备把过期函数扫扫干净,把同事临时加的Macro去掉之后一编译,问题来了——SDL这次怎么不报错了?

再三确认过vcxproj中已经没有了相关的Macro,并且SDL的确是打开了。但这次编译就是不会报错,似乎对我面前的strcpy视而不见。为什么呢?

Google上搜了一大圈也没有方向。最后还是被我排除法硬试出来的——平台工具集如果选了v120_xp,那么SDL即使打开,有些过期函数也不会报错。是不是SDL就此失效,我不清楚,因为我没法去覆盖所有的过期函数。我估计,微软是这样想的:你既然打算让这个程序跑在过期的OS上,那函数过期不过期已经不重要了。

其实还有一个更重要的原因:strcpy_s还好,但要是把inet_addr真的换成了InetPton,你就会发现在WinXP下你的程序根本就跑不起来。实际上,WinXP就不支持InetPton。MSDN上的信息表明,最低也要Vista才可以。

我们的程序暂时还不能抛弃WinXP用户,但若要完全不知道用了哪些过期函数我又心有不甘,于是我打算做一个#if……#else……#endif来解决这个过期OS兼容的问题。搜了一下,正确的姿势应该是:
#if (_WIN32_WINNT >= 0x602)
#else
#endif
最后我是在Debug版本上用了平台工具集v120,在Release时还是用了v120_xp。过期函数只会局限在以上Macro范围内,有限度地使用。

另外,WinXP也不支持条件变量CONDITION_VARIABLE,所以这个服役期超长的操作系统是真的应该淘汰了。我不得不说一句:WannaCry,干得好!

2017-05-19

世界都在发展,而我们在翻墙

Google I/O 2017,没有熬夜看,只是大致了解了一下主要内容。
一转眼,Google+上线已经快六年,这六年中,热点变了好多次。SNS貌似早已过气,大家都在纷纷议论Google+到底什么时候被砍掉。曾经的热点Android现在已经变成波澜不惊的东西。Glass悄声无息已经很久。无人驾驶汽车现在好多家都在搞。Assistant,Allo,Google现在重点关注的是人工智能了。
也许SkyNet真的会出现,也许Terminator很快就会到来。我倒不太在意「人类被消灭」这种事情。如果能够见证这一时刻,也是一件幸事。

然而!眼看着世界都在发展,眼看着人类朝着技术奇点越走越近,我们在干什么?我们还在努力翻着墙,网络翻墙,肉体翻墙,精神翻墙,就在下下个周日。

美团那个HR,不是歧视,只是蠢

最近有个「美团招聘」事件在网上发酵。孤陋寡闻的我今天才知道。众说纷纭,我也来发表一下意见。

------------------------------------------分隔线------------------------------------------

这位据说来自美团的HR抛出了一个「五不要」的招聘原则,被人发在了网上:
  1. 不要简历丑的;
  2. 不要研究生博士生;
  3. 不要开大众的;
  4. 不要信中医的;
  5. 不要黄泛区及东北人士。
美团说已经把这个人辞退了。简单关注了一下,不少评论意见很简单:要不说这人说得有道理,要不说这人脑残搞歧视。互联网信息快餐时代,简单的意见的确比较容易表述,也比较容易挑口水,所以我觉得我还是写个Blog说一下自己的意见。

HR的招聘原则,脑子能想,但不能说或写出来。一旦别人知道了,那就是「歧视」。我觉得这个没啥大问题,不过我不关注这个层面。如果原则正确合理,即使是写了出来,我也觉得没啥问题。但是这「五不要」到底有没有道理,我觉得一定要分开来一条一条地看。如果你要么全盘接受,要么全盘否定,那么有可能脑子不太好使,请去医院挂号。

不要简历丑的
「丑」这个词,很主观。所以这个HR这样写出来,脑子不会太好使。主观的形容词你只能自己用,给别人讲了也无意义。你心中的「丑」跟别人心中的「丑」肯定不是同一回事。
那么,这一条有没有道理呢?当然有道理。招聘的考察因素当然可以包括被招聘者的审美和美术功底,包括排版能力。就算不是相关岗位要求的技能,但「审美观」作为对一个人综合素质的要求是并没有什么问题的。这一条,道理上没问题,只是不能写出来,因为写了也没意义。

不要研究生博士生
首先这一条写得有点问题,如果写成「不要研究生及其以上学历者」或「只要本科(含)以下学历者」可能更合适。不过我也不想抠这种文字上的细节。单看这一条表述的意思,一点问题也没有。招聘条件中对于学历的要求,这个能有什么问题?也许其他人觉得HR是歧视高学历者,但也可能是岗位不需要,或简单的「雇不起」。你要是看出来里面有「歧视」,那其实只说明你心里才有「歧视」。

不要开大众的
我初看到这一条时感觉简直是「这人神经病」。「开大众的」怎么着你了?撞你了还是蹭你车了?上网一搜才知道所谓「神车」是啥意思。
那么我们现在知道HR是什么意思了。不过,这一条问题很大。HR不想要具备某种价值观的人,这个很容易理解。但是开个大众车就一定具有某种价值观吗?可以举出反例的情况太多了。后面的分析会告诉我们,这个HR一定不是一个好的理科生,他的逻辑学连基础都没有。

不要信中医的
这一条没有问题。信中医的会有什么价值观,我们还是很清楚的。HR不想要有这种价值观的人,那是他的自由,一点问题也没有。

不要黄泛区及东北人士
看到这一条我又糊涂了。什么是「黄泛区」?黄皮肤泛滥?三藩还是雪梨?查了一下才知道说的是河南那片儿。不过总之「地图炮」仨字跑不掉。
这一题也跟第三条一样,问题很大。你一个HR招个白领而已,要考察的无非主要就是能力和性格。这两样哪个跟地区有必然的关系?

------------------------------------------分隔线------------------------------------------

好了,现在我们来看看这位HR犯了什么毛病?
他的问题不是「歧视」。他是脑残,是逻辑没学好。他不知道什么是充分条件,什么是必要条件。我估计他中学时平面几何的分数一定很低。

招人的时候,你若是HR,一定会提一堆的条件。这些条件里面,有一些是你要求应聘者必须满足的,不具备的话就别来了,来了也没用。这种叫做「必要条件」:若要应聘我司,必须如何如何。通常这类条件都会是「白名单」形式,符合这类条件,你只是有了资格,但并不保证一定会被聘用,也不保证不会被其它条件给刷掉。
还有一类条件,如果你符合了,你就别来了,不会考虑你的。排除性的,也就是上面提到的「XX不要」。这类条件,叫做「充分条件」:你只要如何如何,我就一定不要你。注意,这里一定是「黑名单」性质。除了骗子公司,没有HR会说「只要你符合XX条件,我就一定要你」。全世界都没有,不然这家公司早倒闭了。

黑名单有效的前提,是该条件可以充分覆盖HR希望避免的区域。换句话说,你排除掉的,一定是你不想要的。你的条件和你要的结果,中间要有逻辑关联性。举个例子:你不喜欢染头发的员工,你就列一条「不得染发」。你要列一条“必须是黑发”,那就有问题了。因为你这个条件涉及到了那些天然非黑发者,却并没有排除掉「染成黑发」者。结果就是,从正反两个方面,你想要的效果都没有达到。我们就会说你这个条件订得有点脑残。
美团这个HR的第三条和第五条就是如此。开大众的也许有价值观不符的,但也有跟价值观无关的。价值观不符合你要求的人,自然也有开其它品牌车的。「黄泛区」和东北人士可能有很多人你不喜欢,但这么多人你能确定所有人都不对你胃口?其它地区的人就都对你胃口?
所以啊,列出了这两条惹口水的条件,却既不能完全排除你不想要的人,还可能「误伤」本来符合你条件的人。站在公司立场看来,这种HR完全就是在瞎鸡巴搞,本职工作显然不合格,被辞退掉完全是活该。站在我们旁观者的立场,歧视归歧视,要是歧视得有道理,比如「不招共产党员」,我们都会点头称是。你要是说「不招关注过郭文贵者」,我们都会骂你脑残欠抽。

少年们,无论将来干什么工作,请先学好逻辑!

2017-05-18

当Win7 Windows Update 遭遇 0x80073712

Windows Update一直以来都以会遇到各种Error代码而闻名。今天又遇到一例,记录一下。

起因是WannaCry。我有一堆各种OS版本的虚拟机,其中一台Windows7 SP1 x86使用得很不频繁,昨天打开一看,上次Windows Update已经是2016年09月的事情了。虽然NAT挡在宿主机后面其实不会有啥问题,但是按照我的习惯,下班前还是让它去打了补丁。曾经在上一家公司的遭遇一直在提醒我:有人的虚拟机中了震荡波,然后不知情的时候被做了快照,于是每隔一段时间测试机房就会忙活一阵子(测试机为了测试程序的补丁管理功能是不打补丁的)。

今天早上一来,红色儿的,4个成功2个失败。我也没太放在心上,公司网络有时候会断,说不定是下载失败。再来了一次,在下载到11%的时候又失败了。我把VPN开起来(曾经有不开VPN打补丁会下载失败的经历),上了个厕所回来,然而这次还是失败,我看了下ErrorCode:80073712。每次都是这个。好吧,开始Google

官方网页推荐的做法大概是这样的:对于Win7而言,首先请先尝试用SFC修复一下。如果还不行,那么请下载工具System Update Readiness tool进行修复。

SFC这货其实没啥鸟用,反正我每次用都没啥好结果。这次也不例外,扫描到44%时告诉我:虽然我们发现有错,但是无法修复,你去看日志吧。

试了下再次Windows Update,还是报0x80073712。好吧,只好试试看那个修复工具了。下载下来两百多MB,安装了老半天。再次Windows Update,这回进度开始超过11%了,我长舒一口气。终于OK了。

顺便瞄了一眼同页面上对WinXP的问题处理建议,仅仅提到SFC。看来真的是该放弃这破烂了。

2017-03-29

对VS2013下C++11的精准转发与通用引用的一点研究

在C++11中,允许用以下方式编写模板函数:
#include <iostream>
#include <string>
class A
{
public:
    template <typename T>
    void foo(T&& t)
    {
        T _t = std::forward<T>(t);
    }
};
int main()
{
    std::string s1 = "test";
    A a;
    a.foo(s1);
    std::cout << "1:" << s1 << std::endl;
    a.foo(std::move(s1));
    std::cout << "2:" << s1 << std::endl;
    a.foo("ok");
}
以上代码在Visual Studio 2013上测试通过。输出是:
1:test
2:
可以看到,模板函数A.foo只有一个声明和实现,但既可以接受左值,也可以接受右值。并且当S1被当作右值引用传入的时候,其值是确确实实被“丢弃”了。这就是所谓的精准转发(把t的类型准确地传递到使用者),以及通用引用(用一个T&&就可以表示所有的情况)。对于要写库的程序员来说,可谓是一个福音了。
然而,对于模板类,下面的写法看上去很好,但是编译会报错的:
template <typename T>
class A
{
    T _t;
    public:
    void foo(T&& t)
    {
        _t = std::forward<T>(t);
    }
};
int main()
{
    std::string s1 = "test";
    A<std::string> a;
    a.foo(s1);
    std::cout << "1:" << s1 << std::endl;
    a.foo(std::move(s1));
    std::cout << "2:" << s1 << std::endl;
    a.foo("ok");
}
编译后报错:
error C2664: “void A<std::string>::foo(T&&)”: 无法将参数 1 从“std::string”转换为“std::string&&”
with
[
    T=std::string
]
无法将左值绑定到右值引用
这大概是因为,T的类型在A<std::string>的时候就确定了,因此编译器无法进行更多的类型推导。
那么怎么办呢?其实也不难,foo函数像下面这样写就可以了:
template <typename T>
class A
{
    T _t;
    public:
    template <typename X>
    void foo(X&& t)
    {
        _t = std::forward<X>(t);
    }
};
在函数模板中用一个新类型就可以了。如果T跟X不一致,那么编译器反正会检查出来的。不用担心。
还有一个问题:有的时候我们会把foo的实现写在class的外面。那这个时候怎么办呢?
我本来想抛题目给大家去做。不过都到最后了卖关子也没什么意思,还是直说吧:
template <typename T>
class A
{
    T _t;
    public:
    template <typename X>
    void foo(X&& t);
};
template <typename T>
template <typename X>
void A<T>::foo(X&& t)
{
    _t = std::forward<X>(t);
}
标红的两行,只能是这个顺序。类的模板定义在上面,函数的模板定义在下面。颠倒过来,报错。要写在一行也可以,先左后右就行。但是要想把尖括号打开强行并成一句,报错。

2017-03-06

作死的TerminateThread

有一天早上,我收到了一个来自同事的Dump文件。打开一调试,报错信息如下:
0x77B16BB9 (ntdll.dll) (XXX.dmp 中)处有未经处理的异常:  0xC0000005:  读取位置 0x1C7695E8 时发生访问冲突。
我一般首先会去看CallStack,因为PDB都有,所以得到的情况很清晰:
ntdll.dll!RtlpWakeByAddress()
ntdll.dll!RtlpUnWaitCriticalSection()
ntdll.dll!_RtlLeaveCriticalSection@4()
XXX.dll!RecMutex::unlock()
XXX.dll!LockT<RecMutex>::~LockT<RecMutex>()
……
这个调用栈看得我有点糊涂。这个RecMutex来自ICE的最新版本,我从1.3.0版就开始用ICE,用到现在的3.6.3。这种基础的代码我有信心不会有问题,也不会用出问题来。何况RecMutex只是对CRITICAL_SECTION的一个简单封装,代码并不多,也并不复杂。我这次在自己项目代码中用之前恰好看过一遍,也认为这里面不会有什么问题。
具体到RecMutex::unlock(),其实也就是干了这个事情:
void RecMutex::unlock() const
{
if (--_count == 0)
{
LeaveCriticalSection(&_mutex);
}
}
这个问题并不是每次都能重现。于是可以肯定跟「多线程」有关系。很显然,LeaveCriticalSection()这种级别的Win32 API调用绝对不会存在着会导致Crash的Bug,否则微软早就死翘翘了,所以问题肯定出在那个_mutex上。然而,既然if语句能进来,那么this指针一般来说是对的。因为是Release版本,优化导致了LocalVar的显示不是那么可靠,不过跟踪ESP的情况最终验证了我的猜测是对的——至少到这个时候,栈还没有坏掉。

这个_mutex就是一个CRITICAL_SECTION。那么我就很头大了。多线程出问题,一般是临界区被搞坏了。然而CRITICAL_SECTION本身就是用来保护临界区的。它什么情况下会被搞坏掉呢?

接下来的汇编代码,Debug起来就很有些难度了。我只能大致确认,在_RtlLeaveCriticalSection()中,_mutex还没有坏掉。但在RtlpWakeByAddress()中就肯定坏了。至于RtlpUnWaitCriticalSection()的情况就不是很清楚了。

到此时,三个小时已经过去了。正面Debug似乎走入了泥沼,现在只剩下「程序不会闹鬼,一定要查个水落石出」的精神还在支撑着我。起来上了个厕所,我换了条思路:问Google。
Google果然厉害,哪怕只是简单的关键词搜索,第一条搜素结果就直指要害

其实第二条搜素结果也很正确,不过Reddit毕竟还是没有StackOverflow专业。这个问题所描述的现象,跟我遇到的是一模一样。而且给我Dump文件的那个同事用的OS的确是Win10
。我最感激的是这哥们问题的开头一段几乎直接就回答了我的疑惑:TerminateThread。该死的!肯定有人在做这种事情。

我当然是知道的。TerminateThread会造成各种各样千奇百怪的问题。正常的应用层操作的确不太可能造成CRITICAL_SECTION本身被搞坏(除了把栈写坏),然而TerminateThread这种东西是一定会干得出来的。大多数程序员都知道TerminateThread不靠谱,所以都不会去用,这个已经几乎成为常识了。于是有一个坑爹的后果就是:一旦有人真的这样干了,正常人全部懵圈,因为没人想到会是这个原因。多谢这哥们把这种问题给问了出来,否则我可能还要花更多的时间在它上面。

接下来的事情就很简单,找出谁在用TerminateThread。其实也没那么简单,因为我找了一圈,都没发现相关的代码。我都有点开始怀疑自己是不是碰到了别的情况。这个时候我突然想到:会不会是别人的库里面有问题呢?

我当然用到了一些开源第三方库,不过我相信它们不会犯这种错误。它们就算要用到TerminateThread,也肯定是作为程序退出之前的强制性措施来使用,正常运行情况下是绝对不会这样用的。

相比较而言,我比较不信任其它组同事写出来的「中间件」。一问之下,那个写「中间件」的同事,也用了另外一个组提供给他的一些开发库。再追着问,果然,那个开发库里面对线程的封装,在退出「超时」的情况下调用了TerminateThread。

至此,水落石出了。这个Dump文件的确就是在调用了那个中间件的关闭接口之后不远的地方产生的,并且产生之前确实很明显地出现了「超时」的现象——界面卡住了约5秒没有动。可想而知,由于停线程超时,于是就去调用了TerminateThread。那个开发库的编写者大概以为,这样就可以让程序继续跑下去。然而这只是他的一厢情愿。用过TerminateThread之后,系统的行为就会是undefined的了。现在可能不会Hang,但是接下来会出现各种莫名其妙的错误,到那个时候必然将会更难调试。这种「保证线程可以被停掉」的代码我宁可不要。浪费时间,不就是在浪费生命么?

在最后,再次警告大家:珍爱生命,远离TerminateThread。

2017-02-03

折腾boost::python的一些收获

最近从事了一些通过boost在C++中调用Python脚本的工作。折腾下来有一些收获,记录一下,也许也可以帮到有些人。

一、关于万能变量类型


对于习惯了Python的C++程序员而言,boost::python::object这个东西是一个巨大的诱惑。它让你几乎可以像在Python里面那样使用弱类型的变量,同时还支持数组和字典之类的复杂变量类型,并且还支持嵌套。这简直就是一个万能变量类型,有了它,常见的需求几乎都可以满足了。

而且它还快,还容易用。它其实是PyObject*的一个封装,也就是说PyObject*其实功能也一样,但是没它容易使用。JsonCPP里面的Json::Value也可以「万能」,但性能与boost::python::object相差颇多。这真的是一个巨大的诱惑。

但是在这里,我要给大多数C++程序员泼一盆冷水。在单线程下,这个梦想可能真的是事实:但是在多线程下,boost::python::object就是一个

boost::python::object为什么快?因为它基于PyObject*,具有引用计数,所以赋值才飞快,浅拷贝嘛。但是引用计数的问题就是线程不安全。

当然,光是引用计数本身不会导致线程安全性问题,导致问题的是引用计数带来的临界区对象引用问题,而归根结底,是Python C API的「并发问题不归我管」的思路。boost::python的封装机制使得对象引用不好控制,也不太可见。想法是好的:使用者不需要关心这些。但现实就很无奈了。

所以,尽管很不甘心,关于万能变量类型的实现,我还是老老实实地用回了Json::Value。

二、类型转换与判断


要从boost::python::object转换成C/C++原生变量类型,一般用boost::python::extract<T>。转出来的对象调check()就可以确定是不是正确的类型。转boost::python::list或boost::python::dict也是一样。

有一点需要特别注意的是:用boost::python::extract<bool>的时候,很可能得不到你预想的结果。你会发现,int也被转成bool了(check()返回true),反过来也一样。这个不是Bug,是boost::python故意这样搞的。BOOST_PYTHON_BOOL_INT_STRICT宏可以解决这个问题(必须改动boost::python源代码并重新编译,因为相关代码在cpp里面不在头文件里面),它使得int和bool将被严格区分。

但是boost::python之所以把int和bool混起来用也不是没有道理的。这样使得MFC里面的那些BOOL(不是bool)类型的函数形参(和返回值)可以直通Python,封装起控件来尤其方便。如果你设了BOOST_PYTHON_BOOL_INT_STRICT,就不能这样干了,必须显式转换。左是一刀,右也是一刀。你们自己掂量吧。

反正我是没有去加BOOST_PYTHON_BOOL_INT_STRICT,而是自己通过_stricmp(value.ptr()->ob_type->tp_name, "bool")搞定的。

三、清空boost::python::list


用惯STL刚开始用boost::python的人可能会头大——怎么dict有clear()但list没有?
答案很简单,因为Python C API没提供,所以boost::python就没有。

说到底,boost::python就是对Python C API的一个封装而已。你如果去看boost::python的源代码,甚至会发现里面不少的「成员函数」其实是跑去执行了一句Python脚本。不了解细节的C++程序员很容易在这种地方栽跟头,所以我觉得boost::python绝对不会是一个终极形态。

要想清空数组,并不是完全没有办法。我找到的一个办法是boost::python::delitem(list, boost::python::slice())。并发调用时记得用本文最后一节的办法加锁。但是我是真的不建议在多线程环境下用boost::python,太多坑了。如果实在要用,在完成了Python脚本调用,把数据转换好之后,就赶紧离开boost::python区域吧。

四、boost::python::object深拷贝


boost::python::object默认的赋值操作都是作浅拷贝,极快。然而,有的时候需要深拷贝怎么办呢?

简单数据类型直接extract出来就是深拷贝(传值)了。会有问题的仅仅是复杂数据结构,具体地说,list和dict。

dict又是有提供copy(),理由还是——Python里面的dict有copy(),而list就没有。我尝试过最简单的办法,就是把一个list放到一个dict里面去copy()完再拿出来用。这样肯定可以达到目的。至于有没有更好的办法呢?我反正是自那之后就弃坑了,你们去研究吧。

五、多线程并发加锁


C++调Python的时候的加锁是一个不小的话题。不过这方面中文资料也算不少,有兴趣可以去搜来研究。我这里就简单地说一下跟boost::python相关的部分。

加锁是用PyGILState_STATE,像这样:
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
……    // 调用Python脚本
PyGILState_Release(gstate);
这种例子网上很多,就不细说了。
关键在于,我前面也提到了,boost::python里面很多看起来是object的成员函数的东西,其实都是对Python脚本的调用,所以都得加锁。

事实上,只要你用到了boost::python的地方,都得加锁。不管是module和call这种看起来就跟「调用」二字相关的,还是在对boost::python::object作数据处理,看起来人畜无害的,都得加锁,无论读/写,否则你就等着爆吧,特别是服务端程序。

这也就是我一再警告避免在多线程下使用boost::python的原因。当然,我这边是不得不用,只能小心翼翼,如履薄冰,然后尽量控制不要让菜鸟程序员去写这种代码。由此可见,隐藏了实现细节并非全是好事,也并非全是对新手有益。