《C++覆辙录》——1.10:无视(久经考验的)习惯用法

本节书摘来自异步社区出版社《C++覆辙录》一书中的第1章,第1.10节,作者: 【美】Stephen C. Dewhurst(史蒂芬 C. 杜赫斯特),更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.10:无视(久经考验的)习惯用法

“很早就有人发现,最杰出的作家有时对修辞学的条条框框置若罔顾。不过,每当他们这么做的时候,读者总能在这样的语句中找到一些补偿的闪光点,以抵消违反规则带来的代价。也只有他们确信有这样的效果存在他们才这么做,否则他们会尽一切可能去因循那些既成的规则”(Strunk和White,The Elements of Style)39。

以上这条被经常引用的对于英语散文写作的经典导引,却常常在指导软件开发中的文本书写风格方面时也屡试不爽。我对本条金科玉律以及背后的深邃思想心悦诚服。不过,我对它还有不甚满意的方面,那就是它在脱离了上下文的前提下,并未揭示为何通常情况下因循修辞学的既成规则是事半功倍的,也未有阐明这些既成规则究竟是怎么来的。相比Strunk高高在上的圣断,我倒更对White的朴实无华的“牛道”之比喻情有独衷。

仍在使用中的语言就像是奶牛群行经的道路:它的缔造者即奶牛群本身40,而这些缔造道路的奶牛们在踏出这条道路以后,或是一时兴起,或是实际有需,有时继续沿着它走,有时则大大偏离。日复一日,这道路也历经沧桑变迁。对于一头特定的奶牛而言,它没有义务非得沿着它亲身参与缔造的羊肠小道框出的轮廓行走不可。不过它如果因循而行,常常会因此得益,而若非如此,则不免会因为不知身处何处和不知行往何方而给自己平添障碍(E. B. White,The New Yorker刊载文章)。

软件开发的程序语言并不像自然语言那般复杂,因而我们“撰写清晰代码”的这一目标肯定比“书写漂亮的自然语言的句子”要容易企及。当然,像C++那样的语言的复杂性已经使得其开发软件时的效能与一套标准用法和习惯用法紧密相依。C++语言的设计大可不拘小节,它给予软件工程师足够的弹性。但是,那些久经考验的习惯用法为开发的效率和清晰的交流开启了方便之门。对于这些习惯用法的无意忽视甚至有意违背,无异是对误解和误用的开门揖盗。

很多本书中的建议都包括了发掘和运用C++编码和设计中的习惯用法。很多这里列举的常见错误都可以直接视作对某个C++习惯用法的背离。而针对它们提出的正解,则又经常可以看成是向相应习惯用法的归顺。出现这种情况有个好理由:有关C++编码和设计中的习惯用法乃是C++软件工程师社群的所有人一起总结并不断地加以完善的。那些不管用的或是已经过气的方法会逐渐失宠直至被抛弃。能够得以流传的习惯用法,都是那些持续演化以适应它们的应用环境的。意识到,并积极运用C++编码和设计中的习惯用法是产出清晰、有效和可维护的C++代码和设计的最确信无疑的坦途之一。

作为称职的专业软件工程师,我们可要时时刻刻告诫自己我们平常撰写的代码和设计都已被涵盖在某个习惯用法的语境里了41。一旦我们识别出了某种编码和设计中的习惯用法,我们既可以选择呆在它为我们营造的安乐小窝里,也可以选择在理性思考后为自己的特殊需要而暂时背离它。无论如何,大多数情况下我们还是因循守旧一点好。有一点可以肯定的是,如果我们连半点儿也没有意识到什么习惯用法的存在,我们的半路迷途就是注定的了。

我并不想在不经意间给你留下“C++软件开发中的习惯用法就像讨厌的紧身衣一样把设计流程的方方面面绑得死死的”这么个印象。远非如此。恰当运用习惯用法会让你的设计流程和有关设计的交流变得极其简化,给设计师们留下了发挥其创作天赋的无尽空间。也有这样的时候,哪怕是最合理的、最司空见惯的软件开发中的习惯用法都会在碰到某种设计时不合用。遇到这种情况,设计师就不得不另辟蹊径了。

最常用,也是最有用的C++语言的习惯用法之一就是复制操作的习惯用法。所有C++里的抽象数据型别都需要做出有关它的赋值运算符和复制构造函数的决定,那就是:允许编译器自行生成它们,还是软件工程师自己手写它们,还是干脆禁止对它们的访问(参见常见错误49)。

如果软件工程师打算手写这些操作,我们很清楚应该怎么写。当然了,编写这些操作的“标准”方法在过去的很多年里是不断演化的。这是习惯用法并非恣意妄为的好处之一:它们总是朝着适应当下用法趋势的方向演化。

class X {
public: 
  X( const X & );
  X &operator =( const X & );
  // ...
};```
虽然C++语言在如何定义复制操作这方面留下了很大的发挥空间,但是把它们像上面几行代码展示的那样声明却几乎肯定是个好主意:两个操作42都以一个指涉到常量的引用为实参,赋值运算符不是虚函数43,返回值是指涉到非常量的引用型别44。显然,这些操作中的任何一个都不应该修改其操作数。如果它们修改了,这是让人莫名其妙的。

X a;
X b( a ); // a不会被修改
a = b; // b不会被修改`
除了某些情况,比如C++标准库里的auto_ptr模板就比较特立独行。这是个资源句柄,它能够在从堆上分配的存储不再有用时,把这些存储的善后清理工作做好。

void f() {
  auto_ptr blob( new Blob );
  // ...
  // 在此处把分配给Blob型别对象的存储自动清除
}```
好极了。不过如果那些还在念书的实习生们写下这样大大咧咧的代码,可如何是好?

void g( auto_ptr arg ) {
  // ...
  // 在此处把分配给Blob型别对象的存储自动清除
}
void f() {
  auto_ptr blob( new Blob );
  g( blob );
  // 哎呀,在此处把分配给Blob型别对象的存储又清除了一遍!
}`
一种解决之道是把auto_ptr的复制操作彻底禁止,但这么一来就会严重限制它的用途,也使得好多auto_ptr的习惯用法化为泡影。另一种是为auto_ptr装备引用计数,但那么一来,使用资源句柄的代价就将膨胀。所以,标准的auto_ptr采取的做法是故意地背离了复制操作的习惯用法:

template  
class auto_ptr ③{
  public:
  auto_ptr( auto_ptr & );
  auto_ptr &operator =( auto_ptr & );
  // ... 
private:
  T *object_;
};```
③译者注:标准的`auto_ptr`还实现了这些非模板复制操作对应的模板成员函数,但是经验是相似的,参见常见错误88。
这里,操作符右边的操作数实参并不具有常量性!当一个`auto_ptr`使用另一个`auto_ptr`对象初始化或被赋值为另一个`auto_ptr`对象时,这个用于初始化或赋值的源对象便中止了对它指涉的从堆上分配的对象的所有权,具体做法是把它内部原本指涉到对象的指针置空。

就像背离了习惯用法通常所发生的那样,对于如何用好`auto_ptr`对象从一开始就引起了不少困惑。当然,这个对已经存在的习惯用法的背离也搞出了不少多产的、围绕着所用权议题的新用法,而将`auto_ptr`对象用作数据的“源”和“汇”看起来成为了获利颇丰的新的设计领域。从效果上说,对已经存在的业已成功的习惯用法采取了深思熟虑的背离,反而产生了一系列新的习惯用法45。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值