Imperfect C++之不完美(前三章)
最近看了本不错的书,Imperfect C++, Practical Solutions for Real-Life Programming by Matthew Wilson,人民邮电出版社,荣耀、刘未鹏译。但是只看了前三章,不打算继续看下去了,就写个不完美的总结在这里吧!注意,我的总结和体会,仅仅限制在前三章,我没看后面的内容。就是前三章,我看的也不仔细。
首先,这本书的名字揭示了它要介绍的,C++不足的地方。如果你想知道C++在哪些鸡毛蒜皮的地方有不足的,根本不用看这本超过五百页的书,就去任何一个编程论坛,找争论C++好还是Java好,或者去Linux的创始人与C++拥趸大战的地方,那里会列出成千上万C++做不到的地方,或者坏处的。比如说,“棉袄夏天穿了会很热,请楼上的说说看,你夏天怎么穿棉袄跑三千米?!你说棉袄好,你夏天穿了它跑个给我看看!”
在这本书里介绍的C++需要改进的,都是在编程实践中经常遇到的很根本的问题,而且这本书上还提出了可以改进的方法(有的不太好,还有的,说实话,小陈我还有点觉得不特别好)。可以说,如果一个程序员能够从思想上领会这本书上提到的大部分内容,那么他确实是一个……合格的程序员。这确实是一本好书,但是像它一样的好书不多但也不是说很难找到。它对C++语言有深入、独到的介绍,也有些理论上更深入的探讨,但是就我看来(从我读了前三章的经验)它仍然停留在对语言方面的解析。要么说它和设计模式是不同的类型,要么说,它还没上升到设计模式的高度。
一、尽早暴露错误
我自己的编程经验也不过两年多点。不过两年已经足够让我体会到,错误(bug)是个多么让软件人员恐怖的东西了!错误有运行时、链接时、编译时之分,后两种都很好对付。在目前的工作中,我从来不把对付后两种错误的时间算到日程中,往往十几分钟就解决了——当然,千万别以为这是因为我水平高,而是因为我见识太浅,这点自知之明,呵呵,还是有的。但是对于运行时错误,哦!即使是最低级的内存问题,只要它隐藏在两三万行代码里面,也可以让我追踪上一两天。不过我很喜欢对付运行时错误,对付运行时错误就好象在侦破一个没有任何蛛丝马迹的案子,很有意思!
不过老大可不这样想,为了一个小错误就赔上两三天时间,项目经理的噩梦无非如此了吧!要避免这样的问题,一个行之有效的方法,是让错误尽早的暴露。在我工作的第二年,我才有了这样的想法,我的做法,是使用断言,用断言来检查错误,但是总觉得还有一部分问题,要么不能用断言来检测,要么似乎,还可以再提前一点?断言,是在运行时检测错误,再提前,那可就到了编译时了哦!(不过不看这本书,我是绝对想不到约束的)
诚然,就是在编译的时候就报错(再早,就是编码时候报错了)。
C++里一提到编译期的问题,往模板上靠那是一抓一个准儿。在Imperfect C++中,针对这个问题提出的观点是“约束(constraints)”,具体的实现方式,当然是用摸板了。就我的理解,约束是在语言的语法之上,针对特定程序更加强的规则。如果说,语言的语法是在针对该语言所面向的所有领域上使用该语言的规则,那么约束就是在约束所依附的程序库所面向的所有使用场合上使用该程序库所附加个规则。因此,这个约束的使用范围当然是有狭窄很多,但是同时它更好的规定了程序库的使用条件,更早避免了出现部分错误。
对于约束,我产生的第一个疑惑就是,这东西有什么用?它在编译期间能避免什么错误?关于这一点,我觉得约束的作用应该是在程序库中的。比如我曾经负责完成一个模块,需要给另外模块提供功能,有个两个函数只能在一个特定的函数中调用。这样的规则,就可以通过约束来完成——当然了,这样的设计是相当糟糕的了。再比如,一个模板类,按照语法的约定,它可以接受任何类型的类,但是很有可能我提供的一个模板类,仅仅能接受一个类的子类,那么作者在第七页提到的约束must_be_subscriptable_as_decayable_pointer就很有用了。当然,这时候,直接传父类引用就可以了,似乎没必要用模板。这东西,应该是挺有用的,但是谁知道一个具体的应用?小陈我实在是才疏学浅。
不过,想来想去,总觉得出要么它能对付的问题太小,要么,似乎那样的问题总是要在不好的设计中才会出现。总感觉,约束面对的,是语法以外的规则,但是又觉得出在语法以外另立规则就是不好的设计。所以,就有种感觉是,如果一个设计中的约束太多,那么这个设计可以考虑改进一下。更可能的,呵呵,是有更多更好的应用,但是我不知道。
二、针对契约编程
编程的时候,我总有种如履薄冰的感觉——我不知道调用了这个函数,会不会造成崩溃,我不知道把一个接口提供给了别人以后,别人会不会用我没有预料到的方式调用,我也不会知道那样会造成什么后果——进一步的,别人也不会知道调用了我的函数,会不会造成崩溃。就这样,开始恶性循环了。
应该说,针对契约编程解决了这个问题。而且是从根本上解决的。在Imperfect C++的第二章,涉及到了一部分内容,但是,并不完全。如果让我来说的话,我不觉得C++不支持后条件是个缺憾,其实,抛异常就可以支持后条件了。但是,C++不支持针对契约编程,才是个缺陷。不过话说回来,如果 C++支持了针对契约编程,那就成了Eiffel了么。
在Imperfect C++中,对前条件说的不多,直接用assert就可以了。不过需要注意的是,书上说的是小写的那个assert,不是MFC中的大写的ASSERT。书上那样做,当然更合理,因为它针对的是C++,不是MFC。但是我觉得,判断前条件用ASSERT更好,也就是说,我认为,前条件的判断是应该在发布的时候消除掉的。关于前条件,我觉得有两个要求:第一,前条件是从逻辑上讲应该满足而且可以满足的;第二,在编程的时候,要假定前条件已经得到了满足。从第一条,我支持在发布软件的时候消除前条件判断;而第二条如果不满足,那么第一条也就不成立了。
对后条件,我的看法就和书上不太一样。我觉得,前条件满足而后条件不满足,意味着发生了异常。那么抛异常就可以了。异常是什么?异常上逻辑上讲应该满足而没有满足的,而且也不能通过程序自己确保满足。比如,我的程序要读注册表,那么“注册表存在”这就是个应该满足,但是不能通过自己的程序来确保满足的条件。如果注册表文件被删了,那么异常就发生了,因为这时候“注册表存在”这个条件应该满足,但是它没有满足。进而会导致后条件不满足。那么抛异常就可以了。
那么抛出异常以后怎么处理呢?我觉得在出现了异常的时候(逻辑上讲应该满足而没有满足的,而且也不能通过程序自己确保满足),就弹个对话框,然后退出整个程序或者相关的功能模块就可以了。比如说“注册表被破坏,无法完成指定功能。”这样做,会不会不友好呢?个人以为不会。如果你不抛异常,那一般会导致程序崩溃,用户则火冒三丈的大骂(这个用户脾气大)。有个对话框,至少用户知道该骂的是微软,而不是你。这样做,捕获所有的异常会多写不少代码,不过获得的是程序稳定性提高了,软件质量很高,而且,如果达到同样的质量,用防御式编程,大概会写不知道多多少的代码吧!异常和后条件,是针对那万分之一的错误情况的,如果你的软件要求没那么高,或者说你的用户都很好说话,其实是没必要采用的。
针对不变式,书上用一个函数来包括所有的类不变式,倒确实是个比较好的主意。还有就是书中提到的让断言提供更多信息的方法也确实挺有用的,不过我觉得,如果你认为断言是需要在发布版本删除的,那么没必要让断言提供更多信息,至少书上的方法不实用。直接在出发断言的地方加注释就可以了么!还有,就是“静态断言”,或者说是“编译期断言”,似乎和约束在抢市场。
三、RRID和RAII
第二章,全是在介绍基本常识,略过。唯一值得一提的是,书中介绍了用静态函数来为初始化列表中的成员进行初始化的方法。这真的是个很有用的好方法!真的很巧妙,很佩服作者!!!
第三章里介绍的RRID和RAII确实很好,可惜我对这两方面应用不多,呵呵。不过读了以后还是受益匪浅。不过我觉得,我们使用RAII就足够了,RRID,最好不要用它。RRID可以释放资源,或者说,它可以确保资源得到释放。但是问题在于,它根本不知道这个资源会不会在其他地方仍然在继续使用, 或者说已经被释放掉了。如果说,不是因为其他迫不得已的原因,我觉得RRID是不应该使用的,它的使用,说明对资源的管理有漏洞,存在着造成资源重复释放或者释放后使用的危险——当然这些危险比起资源泄露来说,要小得多。不过我的观点是,与其使用RRID来解决资源释放的问题,不如花点时间找个办法,能不能用其他办法来管理资源,从而避免使用RRID。比如,RAII?
很喜欢RAII的做法——它牺牲了灵活性,但是换来了安全。我觉得这是一笔合算的买卖。出于同样的原因,我喜欢引用,而讨厌指针。在书中对RAII的分类,有常型和变型:常型是在构造函数获得资源,在析构函数释放资源,而在此之间的任何操作,都不会让它失去资源;变型则是可以在中途设置接管其它资源,或者可以干脆完全没有任何资源。如果说你的程序不是很大规模或者很高水平的,我觉得直接只用RAII就足够了,如果你发现自己需要用RRID的时候,就留神好好改下设计,用回RAII,才是正道。如果你的程序是很大规模,很高水平……肯收我做个端茶到水扫地擦桌子的么?
在我的编程过程中有用过RAII类似的经验。是第一年的时候,我需要两个配对的操作,在进入函数的时候执行一个,退出的时候执行另外一个,有的时候,还要在进入大括号的时候执行一个,退出的时候执行另外一个。面对的问题并不是说资源的释放,而是我觉得这样配对的操作如果让程序员去写太没意思了,而且容易错,错了还不好查。最后采取的做法是定义了一个类,在构造函数里调用一个操作,在析构函数里调用对应的操作,那么程序员只需要在进入的时候,定义一个这个类的对象就可以了(临时对象不可以,那样会立即析构,而不是在退出作用域的时候析构)。实际上,为了进一步简化,我定义了一个宏,这个宏就会自己定义一个类的对象,程序员只需要写下这个宏就可以了。这个做法的效果很好,不过,这好象是面向方面编程里的东西吧!也许,不支持面向方面编程,也是C++的一个不完美的地方吧!
后记
C++不完美的地方太多了,比如它的数组只能从0开始,不能像Pascal一样从任意值开始,甚至使用枚举;它不支持属性,也不支持事件……太多了。但是我想,当你说“C++不支持……”的时候,可能最好改成“C++不直接支持……”,C++的语法是如此强大,以至于它完全可以自己实现任何语言的特性。Imperfect C++提出了它不足的地方,并不是为了批评它,而是在同时给出了如何规避这些不足之处,并且教给了我们怎么用C++来实现它没有的东西。“用C++实现它没有的东西”,我想这才是C++的魅力所在,也是使用它的一个境界吧!
不过对这本书,我还有两句微词。它讲的很细致,介绍了很多问题以及解决方案——这当然不是什么缺点,相反的这是个优点。不过我觉得要提高一个人的编程水平,依靠这样的书,和依靠《Delphi注册表操作一百例》那样的书提高自己面向对象编程水平一样,不可能。提高编程水平,更多的是要建立起来面向对象编程的思想,而不是一些从思想中衍生出来的技巧。另外,它针对的是编程,而不是设计。就我看来,保证软件质量而言,还是从设计方面保证更好,因为设计好可以确保整个使用这个设计的团队得到一个比较好的软件质量;而编程,总是个人的提高,它可以让一个人有非常好的代码质量,但是如果其他人质量没那么好,就会由于木桶的短板效应而失去意义。这是一本很好的教人格斗的书,却不是教人以战略的书——这已经很好了,我们很多人,连格斗都不会,只能比划呢!
虽然说了几句坏话,但是就好比王朔骂金庸一样,这本书岂是小陈我叽歪两句就能说倒的呢!最后感叹一下:好书啊!好书!
[原文在百度空间已经关闭]
标签集合/Tag clouds
C++
Symbain
轻松汇编
算法
论文学习
资治通鉴
Delphi
编程之美
Poco
MFC
Linux
IFC
知乎
汇编
数据分析
交叉编译
poco
j2me
android
XML
Java
DTD
飞信
零宽断言
诺基亚
联系人
编程
真值表
池西木
正则表达式
多线程
命令行
优化
stream
configure
cmake
VIM
UiAutomator
TDD
Symbian
Sqlite
SourceInsight
Python
MPAndroidChart
Kotlin
Flutter
Dokka
Chatgpt