【Scott Meyers】C++5×5断想之五:我之C++拍案惊奇

Scott Meyers C++ 顶级权威之一,为世界各地客户提供培训和咨询服务。发表有畅销的 Effective C++ 系列图书(《 Effective C++ 》、《 More Effective C++ 》和《 Effective STL 》),设计了创新型的 Effective C++ CD Addison Wesley Effective Software Development Series 顾问编辑, The C++ Source (http://www.artima.com/cppsource/) 咨询板块专家。布朗大学计算机科学博士,他的网站是 www.aristeia.com  

 

 

 

在本系列第五也是最后一篇文章里,我将选出让自己拍案惊奇、豁然开朗的五个时刻。

如 果你从事某项工作的时间足够长,必然有几次在疑窦丛生的同时,忽感豁然开朗的经历(如果没有过,那你肯定是入错行了)。每当这些时刻来临的时候,我都惊得 呆了,禁不住大口吸气;好像原来只见黑白二色,突然穿越了时空,来到一个五光十色的世界。最后我慢慢回过味来,面带微笑。如此时刻让人激动。疑窦烟消云 散,洞明取代了它的位置。

这样的事情在 1978 年就出现过一次。经过长期的煎熬,有一天我突然明白了指针的工作原理——如果说软件学习之路上也有成年礼的话,那它就是吧。但我那时还在使用 Pascal 编程,因此不能将它列入这个和 C++ 相关的名单。现在公开我的选择吧:

 

认识到 C++ 中特殊成员函数可以声明为 private [ 注释 1] 1988 年。和很多朋友一样,那时我正在自学 C++ 。某天,刚毕业的同事 John Shewchuk 跑 到我的办公室问我,“如果得到一个不可拷贝对象?”在场的有好几个人,但都不知如何回答。我们知道,如果不定义拷贝构造函数和拷贝赋值操作符,那么编译器 会自动加入,最后得到的对象是可拷贝的。若要阻止编译器自动生成,我们就必须手工定义,但这样一来,对象还是可拷贝的。就像 Grinch[ 注释 2] 一样,我们个个迷惑不解,没有人能找到解决办法。

后来(可能是当天或第二天,我记不清了), John 宣称自己有了解决办法:将拷贝构造函数声明为 private 就搞定了。现在看来,这个问题是多么简单啊!但当时对于我们来说,不蒂于发现了新大陆;这是我们对 C++ 知识融会贯通的重要一步。三年后,我出《 Effective C++ 》第一版时,将这个简单的发现供奉在一个独立的条款(不到一页,大概是这本书中最短的条款)里。再后来,我愈加意识到这个发现的重要性,因此在《 Effective C++ 》的后面两版中都写了进去。 1988 年,我不觉得这种用 private 阻止编译器隐式生成函数的方法显而易见;现在是 2006 年,我还是这么认为。

 

理解 Barton Nackman 在单位分析 (译者注: Dimensional Analysis 。更学术化的叫法是量纲分析)法中提出的无类型模板参数 non-type template parameters 的用法 1995 年。 1988 5 月,我在《 IEEE Software 》上读到 Robert F. Cmelik Narain H. Gehani 合著的一篇文章——《 Dimensional Analysis with C++ 》。他们提出了一种在物理单位(如长度、速度和时间等)的计算过程中检测单位错误的方法。比如,用长度除以时间,再将结果和一个速度量比较是正确的,但和加速度量(它由长度除以时间的平方得来)比较就错了。 Cmelik Gehani 提出,可以将单位信息存储到对象中,然后在运行时进行错误检测。这种方法将使对象变大,而且耗费运行时间。我觉得应该有更好的办法,但折腾再三也没有结果,后来就不了了之。

John J. Barton Lee R. Nackman 在他们 1994 年出版的《 Scientific and Engineering C++ 》( Addison-Wesley 出版社)中提出了一个很好的单位问题解决方案。不过,虽然我当时也拿到了这本书,却没有注意到该项成果——老实说,这本书写得太糟糕了点,我开了个头就扔到一边。直到 1995 年,我通读了 Barton Nackman 发表在《 C++ Report 》 上的专栏文章,他们这次用通俗易懂的语言描述了自己的方案。结果给我留下了三方面深刻印象。第一,它涵盖了单位的所有可能组合,而不仅仅以命名为依据的组 合,因为命名是不完全的。例如,我们将长度除以时间的结果命名为速度,还将压力除以长度的平方的结果命名为压强,但却没有给长度乘以时间的平方再除以角速 度的立方的结果一个名份。至少我不知道。即便计算中产生了迄今为止还用不到的单位组合, B&N 方案也会确保单位分析的正确性。

第二是 B&N 方案的运行时消耗:没有。对象没有变大,程序也没有变慢。因此可以说 B&N 方案是无本而万利 [ 注释 3] 。这才是我真正感兴趣的组合方式。

不过最让连连称奇的,还是他们对无类型模板参数(代表各种基本单位的指数式)及其上算术指令(计算结果单位类型)的使用 [ 注释 4] 。这样,他们不仅解决了多年前搞得我兴趣索然的实际问题,而且使用的还是一项 C++ 特性(即无类型模板参数。它在那以前引发了我无穷的好奇心)。

直到今天,我还为 Barton Nackman 的成果激动。原本打算将他们在《 C++ Report 》上的文章列入我的“ C++ 历史上最重要文献”名单,但后来我发现它影响甚微——很少有人像我那样认为他们的成果具有重大意义。现在,我觉得自己有点可耻,因为我只顾自己满足,却没有将好东西与更多人分享。

 

理解 Visitor 模式的涵义 1996 1997 年。命名恰当,是软件工程的一个基本原则。这儿就有一个例子,充分说明了糟糕的命名会多么折磨人。我没觉得 Visitor 模式的设计机理有什么特别问题,但就是一直弄不明白它的意义。我无法将支离破碎的认识融会贯通。直到后来有一天,我终于明白: Visitor 模式和“访问”毫无关系。其实,它是一种体系设计方法——要求引入新的虚拟行为函数时不必改变原体系的结构。抓住这点后,我一下就理解了这个模式的含义。但其命名于我造成了巨大理解障碍,甚至看了《 Design Patterns 》( http://www.artima.com/cppsource/top_cpp_books.html#dp )如下的描述后:

Visitor 使你不用改变行为操作的元素的类,就可以定义新行为。

这个解释清楚而直接,现在看来很好理解,但我当时就是盯住了模式的名字,总觉得“ Visitor ”应该和“访问”、“遍历”啥的发生点关系。

出现这种结果,我想有两个可能原因。一是我死心眼,见识短浅,鼠目寸光。再有就可能是这个名字选得过于随意。如果名字本身指的张三,而使用文档上说的却是李四,那么至少一些人——比较执拗的那种——肯定要搞糊涂了。我倾向于后一种解释。

 

理解“ remove ”为什么实际上并没有删除任何东西 1998 年?我与 STL remove 算法相遇得不是时候。当我期望 Visitor 设计模式访问个啥的时候,我也认定 remove 算法就应该删除某个东西。但结果让我非常震惊,我发现在容器上执行 remove[ 注释 5] 时,容器内元素数目根本不会改变!我有一种被出卖的感觉——我是正儿八经要求删除啊!骗子!谎言!无聊的广告!

后来我读到一篇文章——可能是 Andrew Koenig 的《 C++ Containers are Not Their Elements 》(发表于《 C++ Report 1998 11-12 月刊)——它才让我明白 STL 内部的真相:算法不能改变容器内元素的数目,因为算法根本不知道容器的类型。容器还可能是一个数组呢,显然数组的大小是不可改变的 [ 注释 6] 。自然,算法应该和容器彼此独立,互不影响。我认识到,“ remove ”不会改变容器内元素数目,因为它不能。直到那时,我才算真正理解了 STL 的内部结构,知道迭代器( iterator )虽然通常由容器成员函数提供,但就像容器和算法一样,其实它也是完全独立的实体。后来,我把这篇文章读了很多次。类似上面的解释,可能别人都说过很多回了,但别怪我鹦鹉学舌,这可是我第一次真正理解 remove

自此以后,我就能与 remove 和睦相处了。再后来,当发现 remove 不仅将份内事情做得很好,而且效率超过绝大多数程序员自己编写的循环( remove 的运行时间是线性的,而普通循环是二次的)时,我甚至对它有点另眼相看了。虽然我仍然不太喜欢这个命名,但也说不清到底哪个名字既能准确描述其行为,又便于记忆。

 

理解 Boost 库里 shared_ptr deleter 如何工作 2004 年。 Boost 的引用计数智能指针 shared_ptr 很有趣——你可以向其构造器传递一个函数或者仿函数( function object ,或 functor ),当引用计数归零的时候,它将在被引用对象上调用删除器( deleter [ 译注 7] 。乍一看,似乎没啥了不起啊,但请看代码:

 

template

class shared_ptr {

public:

      template

      explicit shared_ptr(U* ptr, D deleter);

      ...

};

 

注意 shared_ptr 必然在析构时调用类型为 D 的删除器,然而它根本不知道 D 为何物。这个对象不能包含类型为 D 的数据成员,也不能指向类型为 D 的对象,因为声明其数据成员时, D 对它而言还是未知的。那么, shared_ptr 对象如何跟踪删除器(它在构造阶段传入;当 T 对象将被销毁时,还得使用它)呢?更通俗地说,构造器如何将未知类型的信息传递给它正在构造的对象,而这个对象本身对信息类型完全无知?

答案很简单:让此对象包含一个指向已知类型基类的指针( Boost 叫它 sp_counted_base ),然后让构造器以 D 为参数实例化一个派生于上述基类的模板( Boost 中叫 sp_counted_impl_p sp_counted_impl_pd ),最后用声明于基类、实现于派生类的虚函数( Boost 中使用 dispose )去调用删除器。用图表示更为直观:

 

 

完全明白了——只要你看过这个图 [ 译注 8 9] 。而且,看过此图后,我想你马上就会意识到它可以应用在很多领域;它为模板设计拓宽了思路,比如,模板化类使用很少的模板参数(例如 shared_ptr 只有一个),就可以跟踪无限个先前未知类型的信息)。当我想到这些的时候,我禁不住面露赞许的微笑,难抑钦佩之情 [ 译注 10]

 

好了,文章结束,这就是我的 5 × 5 系列的末篇。简单总结一下本系列全部文章: C++ 历史上最重要的图书、文献、软件、人物,最后是对我来说最难以忘怀的五个神奇时刻。我还将再次和大家讨论这类有趣的话题,不过那至少应该是又一个 18 年后了。

 

注释:

1. 可能不一定非叫“特殊”成员函数不可——不过《 Standard 》如此称呼——具体包括缺省构造器、拷贝构造器、拷贝赋值操作符和析构函数。之所以“特殊”,是因为如果使用了它们而又未显式声明,编译器一般会隐式生成。

2. Grinch 两腿冰冷站在雪地里,想了又想:‘怎么会这样呢?’”参见 Dr. Seuss 的《 Grinch 如何偷走圣诞节》( Random House 出版社, 1957 年)。不过在网上也能看到哦( http://www.kraftmstr.com/christmas/books/grinch.html ,别告诉 Random House )。

3. 我指的是不消耗更多运行时时间。他们大量使用了模板,当然会增加编译时间。

4. 你将两个数相乘时,就等于增加了指数次数,没忘记吧?

5. 我指的是 STL 的通用算法,而非 list 类的成员函数。

6. 重新分配内存不能算,不是所有的数组都是动态分配的。

7. 不要吃惊, TR1 shared_ptr 就是以 Boost shared_ptr 为基础并提供了相同功能。我正讨论的是 Boost shared_ptr ,因为它有一个实现,我们这里说的也是实现问题。 TR1 仅仅是一个规范,如果你书生意气,问我 TR1 里讲的东西如何实现,那就没意思了。

8. 更准确地说,你以前应该看到过相关解释。譬如我,我得到的解释(我经常要别人给我讲解 C++ 方面的问题)来自于 Usenet 新闻组的一个免费讨论 http://tinyurl.com/r66ql )。

9. 我相信它也是 外部多态 External Polymorphism http://www.cs.wustl.edu/~schmidt/PDF/External-Polymorphism.pdf )设计模式的一个应用。自从我读过 Chris Cleeland Douglas C. Schmidt 1998 9 月发表在《 C++ Report 》上的有关此模式的文章后,我就喜欢上它了。不过直到现在,我仍然没看到这个模式的广泛应用。

10. 我觉得有两点需要说明。第一,它是 Boost 中大量成功创新的一个典范, Boost 的创新性正是我将其列入“最重要 C++ 软件”的原因之一。第二,很可惜的是, Boost 中的这些创新未被整理并在 C++ 社区广泛传播,很多有趣的东西,都掩藏在 Boost 库的盖子下不为人知。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值