Effective C++ 中文第三版

hurui1983

贡献于2014-04-30

字数:0 关键词: C/C++开发 C/C++

\ IJ!I Scott Meye昭著 候她 t手 国5坦fJE吕t 55 Specific ways to Improve Your Prozrams and Designs 改普租用与设计的55 个儿饨做以 理豆豆E Effective C++ 中文版 Third Edition 第二版Effective C++中文版,第三版 Effective C++I Third Edition [美] Scott Meyers 著 侯捷译 穹手立营也局在社· Publishing House ofElectronics Industry 北京· BEIJING内容简介 有人说 C++程序员可以分成两类,读过 Effective C++的和没读过的。世界顶级 C++大师 S∞吐 Meyers 成名之作的第二版 的确当得起这样的评价。当您读过这本书之后,就获得了迅速提升自己 C++功力的一个契机。 在国际上.本书所引起的反响,波及整个计算技术出版领域,余音至今未绝。几乎在所有 C++书籍的推荐名单上,本 书都会位于前三名。作者高超的技术把握力、独特的视角、诙谐轻松的写作风格、独具匠心的内容组织,都受到极大的推崇 和仿效.这种奇特的现象.只能解释为人们对这本书衷心的赞美和推崇。 这本书不是读完一遍就可以束之高阁的快餐读物,也不是用以解决手边问题的参考手册,而是需要您去反复阅读体会 的, C++是真正程序员的语言,背后有着精深的思想与无与伦比的表达能力,这使得它具有类似宗教般的魅力。希望这本书 能够帮助您跨越 C++的重重险阻,领略高处才有的壮美风光,做一个成功而快乐的 C++程序员。 Au址)orized translation from 由e English language edition, entitled Effective C++:55 S严cific Ways to Improve Yo町Programs and Designs, 3'" edition, 0321334876 by Meyers,Sω缸, published by Pearson Education, Inc, publishing 皿 Aωison Wesley Prof,臼sional , Copyright©2005 pearson 剧皿础。 n,Inco All rights re阳ved.No 阳tof 也isb∞k may be reproduced or transmit时 in any form orby any m四血, electronic or mecha皿叫, including photocopying,自∞rding orby any information storage retrieval system, witltout permission from p,四!'SOn 田ucation, Inc. CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and PUBLISHING HOUSE OF ELECfORNICS 的DUSTRYCopyright ©2仪兑 本书简体中文版自电子工业出版社和P国!'SOn Education 培生教育出版亚洲有限公司合作出版。未经出版者预先书面许 可,不得以任何方式复制或抄袭本书的任何部分。 本书简体中文版贴有Pearson Education 培生教育出版集团激光防伪标签,无标签者不得销售。 版权贸易合同登记号:图字: 01-2∞15-3583 图书在版编目 CCIP) 数据 Effective C++中文版,第 3 版/ (美〉梅耶 (Meye白,S.) 著:侯捷译.一北京:电子工业出版社, 2αl6 .7 书名原文: Effective C++ ,咀由甘Edition ISBN 7-121-029ω-x I.E... II ①梅"②侯. III.C 语言一程序设计 N.TP312 中国版本图书馆CIP 数据核字。∞6) 第 081253 号 责任编辑:周鸳 印 刷:北京智力达印刷有限公司 出版发行:电子工业出版社 北京市海淀区万寿路173 信箱邮编 100036 经 销 z 各地新华书店 开 本: 787X980 1/16 印张: 21 字数: 380 千字 印 次: 2仪l6年 7 月第 1 次印刷 定价: 58. ∞元 凡购买电子工业出版社的图书,如有缺损问题,请向购买书店调换。若书店售缺,请与本社发行部联系。联系电话: COlO) 6827何770 质量投诉请发邮件至z1ts@phei.com.cn,盗版侵权举报请发邮件至dbqq@phei.com.CDoEffective C++第三版赢得的赞誉 Sco忧 Meyers的 «E庐ctive C++» 第三版萃取了原本必须历经艰辛才能学到的编程经 验。这本书是一份很棒的资源,我推荐给每一位专业C++ 程序员。 一一- Peter Dulimov, 儿fE, Engineer, Ranges andAssessing Unit, NAVSYSCOM, Aus 衍alia 第三版仍然是"如何将 C++ 各部件以高效、高凝聚方式结合起来"的最佳书籍。 声称自己是个 C++ 程序员之前,你一定得读过这本书。 一- Eric Nagler, Consultant, Instructor, andauthor of Learning C++ 本书第一版被我归类为少数(真的非常少数)在我成长为一个专业软件开发人员的 过程中有重大意义的书籍之一。它很实用又易阅读,却又装载着重要的忠告。 «Effective C++» 第二版延续这项传统。 C++ 是个威力十足的编程语言,如果C带 给你足够绞死自己的绳索, C++ 就是间五金店,挤满了许多准备为你绑绳结的人。 只要精通本书讨论的重点,便可明确增加高效运用C++ 的能力并减缓压力。 一一-Jack W. Reeves, Chi电(Executive 咿leer, Bleading Edge Soft仰'are Technologies 每一位参与我的开发团队的新手,都有一份功课要做:读这本书。 一一- Michael Lanzetta, Senior So.如'are Engineer 九年前我读了 «E庐ctive C++» 第一版,它立刻成为我最喜爱的一本 C++ 书籍。 我认为第三版对于那些希望以C++ 进行高效编程的人仍然是必备读物。如果每一位 C++ 程序员着手写下他们的第一行C++ 专业代码之前都先读过这本书,我们的世 界会变得更好一些。 Danny Rabbani, Software Development Engineer 当我还是个在第一线战场上努力搏斗的程序员,尝试怎么做比较好时,偶然机会遇 上了 Scott Meyers的 «Effective C++» 第一版。多美好的救星呀!我发现Meyers的忠 告很实际、有用,并且有效,百分之百履行了标题上的承诺。第三版带来在严肃开 发项目中使用C++ 的最新实用事物,并针对语言的最新发展和特性增加了新的篇 章。我很高兴发现,从一本我原本以为自己己有很好体验的书籍的新版中,仍然学 到一些有趣而新奇的东西。 一一- Michael Topic, Technical Program Manager 对于想要安全并高效使用 C忡,或打算从其它 00 语言移转到 C++ 阵营的任何人 而言,这一本来自著名 C++ 导师 Scott Meyers 的书籍,是最可靠的指引。本书以 清晰、简洁、有娱乐效果、见解深刻的方式,表现出极具价值的信息。 一一一 Siddhartha Karan Singh, So.斤ware Developer于一般性入门教科书之外,这应该是第二本任何 C++ 开发者应该阅读的书籍了。 它超越了 C++ 语言"如何做"以及"是什么"的范畴,直指 C++ 的"为什么"。 它帮助我对 C++ 的理解层次从语法晋升至编程哲学。 一- Timothy Knox, So.阳lare Developer 这是一本 C++ 经典书籍的惊人更新版本。Meyers 在这一版本中涵盖了许多新领域, 每一位认真的 C++ 程序员都应该拥有这一新版。 一一句trey Somers, Game Programmer «E1知ctive C++» 第三版涌盖编写程序时该做的事,并很好地解释了为什么那些事 情重要。把它视为编写C++ 程序的最佳训练吧。 一-JeffScherpelz, Soft'.也'are Development Engineer 当 C++ 拥抱改变, Scott Meyers 的 «Effective C++» 第三版也昂扬出发,对语言保持 完美的密集跟踪。 C++ 领域有许多优秀的导入性书籍,而"第二本书"应该站在它 们的肩膀上,你手上这本就是。跟随 Scott 指出的方向,让自己也昂扬高飞吧! Leor Zolman, C++ Trainer andPundit, BD Software 这是一本必须拥有的书籍,对 C++ 老手和新手都是。读过本书之后,它一定不会 在你的书架上吃灰尘,因为你会持续地参考它、引用它。 一-Sam Lee, So.们lare Developer 阅读本书,一步一步地运用 55 个可轻松阅读并各自描述某项技术或某个告诫的条 款,普通的 C++ 程序员也可以摇身一变成为专家级C忡程序员。 一-J~所ey D. Oldham, Ph.D. , Software Engineer, Google Scott Meyers 的 «E庐 ctive C++» 各个版本长期受到 C++ 编程新手和老手的需要。这 本新版并入近十年来的 C++ 发展价值,是截至目前最高密度的书籍。作者不仅描述 语言上的问题,也提出毫不模糊又容易奉行的忠告,用以避免陷阱并写出高效的 C++ 。我真希望每一位 C++ 程序员都能读过它。 一-Philipp K. Janert, Ph. D., So.斤ware Development Manager 对那些使用C++ 的时间长得足以被这一丰富语言内的潜伏圈套绊倒的开发人员而 言 , «E1知ctive C++» 的每个版本都是必须拥有的书籍。第三版大面积补充了新世 代的语言和程序库特性,以及为运用那些特性而进化的编程风格。 Scott极具魅力的 写作风格使其所整理的准则容易被消化吸收,协助你成为高效的C++ 开发者。 一一- DavidSmallberg, Instructor, DevelopMentor; Lecturer, Computer Science, UCLA «Effective C++» 己针对 21 世纪的 C++ 实务做出全面更新,因此得以继续声称其 为所有C++ 从业人员的首选"第二本书"。 一一- Matthew Wilson, Ph.D. , author of Imperfect C++Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs Scott MeyersAddisf)n-Wesley Professional Computing Series Brian W. Kernighan, Consulting Editor Ma时lew H. Austern, Generic Programming and the STL: Using and Extending the C++ Standard Template Library David R. Butenhof, Programming with POSI~ Threads Brent Callaghan, NFS Illustrated TomC 缸酬, C++ Programming Style William R. Cheswick/Steven M. Bellovin/Aviel D. Rubin, Firewalls and Internet Security, Second Edition: Repelling the 阳lyHac肋 DavidA.C 町ry, UNI~ System Security: A Guide 如 Users and System Administrators Steph四C. Dewhurst, C++ Gotchas: Avoiding Common Problems in Coding and Design Dan Farmer/Wie饱e 驰nema, Forensic Discovery Erich Gamma/Richard Helm/Ralph Johnson/John Vlissid白, Design Patterns: Elements ofReusable 0也'ject­ Oriented S呐ware Erich Gamma/Richard Helm/Ralph JohnsonlJohn Vlissides, Design Patterns CD: Elements ofReusable Object- Oriented S呐ware Peter Haggar, Practiω I Java~ Programming Language Guide David R. Hanson, C Interfaces and Implementat 初时 : Techniques for Creating Reusable S I:!斤 ware Mark Harrison/Michael McLennan, Effective Tel ,斤"k Programming: Writi 咆 Better Programs with Tel and Tk Michi Henning/Steve Vinoski, Advanced CORBA'" Programming with C++ Brian W. Kernighan/Rob Pike, The Practice ofProgramming S. Keshav, An Engineering Approach ω Computer Networking: ATM Networks, the Internet, and the Telephone Network John Lakos, Large-Scale C++ Software Des 您n Scott Meyers, Effective C++ CD: 85 Specific Ways to Improve Your Programs and D吨m Scott Meyers, Effective C++, Third Edition: 55 Specific Ways to Improve yiωr Programs and Designs Scott Meyers, Mo仰 Effective C++: 35 New Ways ω Improve yiωr Programs and Designs Scott Meyers, Effective STL: 50 Spec庐c Ways ω Improve Your Use ofthe Standard Template Library Robert B. Murray, C++ Strategies and Tactic百 David R. Musser/Gillmer J. Derge/Atul Saini, STL 刊 torial and R价 renee Guide, Second Edition: C++ Programming with the Standard Template Library John K. Ousterhout, Tel and the Tk Toolkit Craig Partridge, Gigabit Networking Radia Perlman, Interconnections, Second Edition: Bridges, Routers, Switches, and Internetworking Protocols S恒ph四A. Rago, UNIX'" System V Network Programming 阳c S. Raymond, The Art ofUNIX Programming MareJ. Roch挝nd, Advanc叫 UNIX Programming, Second Edition Curt 倒也nmel , UNIX'" Systems 户r Modem Architec 切 res: Symmetric Mult伊vcessing and Caching 户 r Kerne l 阳 19rammers W. Richard Stevens, TCP/IP Ill ustrated, 协lume 1: ηIe Protocols W. 阳ICha 时 Stevens ,下CP/IP Illustrated, Volume 3: TCP Jor Transactions, Hπ VIS忧 www.awprofeulonal.coml..rie‘刷'Ofeuionalc,棚'阴耐ng for rnor. In阳m8tIOnabout 伽...倒ft.Effective C++ 中文版 改善程序与设计的55 个具体做法 55 Specific Ways to Improve Your Programs and Designs [美] Scott Meyers 著 侯捷译ForNancy, without whom nothing would be much worth doing Wisdom and beauty form a very rare combination. 阳 V MXm 咄mmPQUAnd in memory ofPersephone 1995-2004译序 VII 译j事 按孙中山先生的说法,这个世界依聪明才智的先天高下得三种人:先知先觉得 发明家,后知后觉得宣传家,不知不觉得实践家。三者之中发明家最少最稀珍,最 具创造力。正是匠心独具的发明家创造了这个花花绿绿的计算机世界。 以文字、图书、授课形式来讲解、宣扬、引导技术的人,一般被视为宣传家而 非发明家。然而,有一类最高等级的技术作家,不但能将精辟独到的见解诉诸文字, 又能创造新的教学形式,引领风骚,对技术的影响和对产业的贡献不亚于技术或开 发工具的创造者。这种人当之发明家亦无愧矣。 Scott Meyers 就是这一等级的技术作家! 自从 1991 年出版 «Effective C++» 之后, Meyers 声名大噪。 1996 年的 «More E庐ctive C++» 和 1997 年的 «Effective C++» 2/e 以及 2001 年的 «Ej如 tive STU 让 他更上高楼。 Meyers 擅长探索编程语言的极限,穷尽其理,再以一支生花妙笔将复 杂的探索过程和前因后果写成环环相扣故事性甚强的文字。他的幽默文风也让读者 在高张力的技术学习过程中犹能享受"阅读的乐趣"一一这是我对技术作家的最高 礼赞。 以条款 (items) 传递专家经验,这种写作形式是否为 Meyers 首创我不确定, 但的确是他造成了这种形式的计算机书籍写作风潮。影响所及, «Exceptional C+ 吟、 «More Ex c<甲 tional C+ 啡、 «C++ Gotchas» 、 «C++ Coding Standards» 、 «Effective COM» 、 «Effective Java» 、 «Practical Java» 纷纷在书名或形式上"向 大师致敬"。 睽违 8 年之后 «Effective C++» 第三版面世了。我很开心继第二版再次受邀翻 译。 Meyers 在自序中对新版己有介绍,此处不待赘言。在此我适度修改第二版部分 译序,援引于下,协助读者迅速认识本书定位。 Ej知ctive C++ 中文版,第三版VIll C++ 是一个难学易用的语言! 译序 C++ 的难学,不仅在其广博的语法,以及语法背后的语义,以及语义背后的深 层思维,以及深层思维背后的对象模型;C++ 的难学还在于它提供了四种不同而又 相辅相成的编程范型 (programming paradigms) : procedural-based, object-based, object-oriented, generics 。 世上没有臼吃的午餐!又要有效率,又要有弹性,又要前瞻望远,又要回溯相 容,又要治大国,又要烹小鲜,学习起来当然就不可能太简单。在庞大复杂的机制 下,万千使用者前仆后继的动力是:一旦学成,妙用无穷。 C++ 相关书籍车载斗量,如天上繁星,如过江之脚。广博如四库全书者有之 (The C++ Programming Language 、 C++ Primer 、 Th inking in C+刊,深奥如重山复水者 有之 (Th e Annotated C++ R价 rence Manual, Inside the C++ Object Mode/) , 细说历 史者有之 (ηte Design and Evolution ofC++, Ruminations on C+ 刊,独沽一昧者有 之 (Polymorphism in C++) , 独树一帜者有之 (Design Patterns, Large Scale C++ S呐ware Design, C++ FAQs) , 另辟蹊径者有之 (Generic Programming and the SYL) , 程序库大全有之 (The C++ Standard Li brary) , 专家经验之累积亦有之 (Effective C++ , More Effective C++) 。这其中"专家经验之累积"对己具 C++ 相当基础的程序员 有着立竿见影的帮助,其特色是轻薄短小,高密度纳入作者浸理 C++IOOP 多年的 广泛经验。它们不但开展读者的视野,也为读者提供各种 C++IOOP 常见问题的解 决模型。某些主题虽然在百科型 C++ 语言书中也可能提过,但此类书籍以深度探 索的方式让我们了解问题背后的成因、最佳解沽,以及其他可能的牵扯。这些都是 经验的累积和心血的结晶,十分珍贵。 «El作ctive C++» 就是这样一本轻薄短小高密度的"专家经验累积"。 本中译版与英文版页页对译,保留索引,偶尔加上小量译注:愿能提供您一个 愉快的学习。千里之行始于足下,祝愿您从声名崇隆的本书展开一段新里程。同时, 我也向您推荐本书之兄弟 «More Effective C+ 吟,那是 Meyers 的另一本同样盛名 远播的书籍。 侯捷 2006/02/15 于台湾新竹 jjhou@ij hou.∞m h即 J/ww,叫jhou.∞m( 繁体) hi句 :lljjhou.四主wet( 简体) El作ctive C++ 中文版,第二版i李序 II术语对照 '足 这里,咱也本书出理之届理术面的英中时圃.本中立陋在海峡两岸同步芷行,因 此量也列出本书简暨南陋的术语时圃,方便某些读着从中一直两岸计算机用语. 表中带有·者表示本书时该词条大多直撞果用英文术语.中英术惕的选锋重 自以下众多考且中取其丰衡: ·业界和学界习惯.即便是学生读者,终也要离开学植进入职编 熟署业界和学 界的习惯用渴〈许多为真主) ,避免二ll;转候,很有必要. ·这是 本中立匾,需踊且中立回读的串盘和酣畅性.过多惺自英主术语会撞戚 阻面的破碎与勘乱'捕者适应但回英文术语,可避免某些盟主不但术语的中立 出理于字里行间造成阅 i嚣的困扰和停睡,有助于流畅的思考和自下榻刻印草. ·凡涉且 C++ 语吉共自理字之相关术语皆惺圄.例如 J class. s田ct , template. public. pnvale, protect时,由恤, inline, const, namespace .... ·以上术语可能衍生直合术语,例如与d邸S 捆荣的直合术活有base class, deriy国 d醋. super CI脯. subel 醋. class template 此类复合术语如果不长,犀皆惺圄 原宜,若太低'则咀情I5/jl 作处理〈也许中英并障目也许赋予特殊字体) • ·凡计算机科学所称之量提结构名称,思皆悻由.例如 Slac k., queue,田舍, hωhtable , map, set, do呵 ue , list, vector. array···· 偶尔将 ""Y 译为量组. ·某些流通但不被理认为足够理想主中译词,醒目匾直不译.例如reference. ·某些英宜术语瞌在割童U特殊字体褒理并酷圃,例如pass byre,曲四四、阳'ssby 回f旧 copy构造函酷、 QSS栩nmenf操作符• p阳cement昭w. ·少量术语为踊且词性平衡,时而*'用中立〈如指针、提型〉时而罩用1i~(如 pointer. type) • ·索引之子科技书睛非常重要.本书与英文版页页时译,因此原封不动悻面所有 真主索引. 过去以来在一直不甚楠意 object 和 type 两个术语的中译词"对象"和"费型", 认为它们缺王术语突出性〈前者正确性甚至有恃商榷) ,却卫踊置出现影响阅瞌, 因此常在踵的著作蓝译作中保圄其英主词或偶尔罪用置体版术语 "物件"和"型 知尸.但现在R刽.旺然大家己蛙很习惯这两个中文术语,也许在只是把人忧天. 因此本书景用大陆撞着警温习惯的译法.不过在仍要提醒嚣. Mobject M 在 Object Oriented 植术中的真正童且是"物体、物件·而非"时貌、目挥". E.Ifectiw C+· 中立版第三版'‘ 以下带有·者牵示本书时i革词条罪英文词,不译为中文 i李序 英主术通 简体版译词 '体版译词 abstract 抽象的 抽象的 .bs国clion 捕'良性、抽象件 抽1/1性抽画良件 ,"e., 访问 存取'取用 access level 访问级别 存iNalI U access fuωctlon 访问函数 存取面式 ",,"01町 适配 III 配接嚣 """"" 地址 地址 a剧......r 甸回2ω, 取地址操作符 咀址罩,事子 a22re2.8tlon 聚合 聚合 algori 让= II:法 演篝法 .11 阻Ol e 分配 配E allocator 分配 III 配E嚣 aDDlication 应用程Jl' 愿用理式 町-c hit c:cture 体系结掏 匾莱茵帽 E 案, 引世 ''''''v 数组 田到 orrow 晴rator 箭头II作符 箭国罩"子 属semblv lan2U8四 汇销语吉 E合画雷 ·ion 断吉 酣霄 剧 i gn(-ment} 赋值 田面 assi£f\ment ooeralor 回值'摩作符 回恒罩,事子 'b画cdωs 基突 基砸噩E~ ."",""" 基类型 基砸型E~ binary 路."址、 分查找 分檀尊 *bin缸yo可@ 卫树 元圈 binary operator 兀操作符 元罩,配子 binding 绑起 鹏在 ,自居 -bit 位 位元 -bitwise 〈以 bit 为单兀髦 .) block E块 匾擅 boolean 布尔值 布林恤 breakpoint 斯点 中断'自 build 建置 姐圃 build-in 内置 内建 boo 且线 匾班排 'b 叽 e 字节 也元植 cache 高越理存〈巨〉 快取(匾) call 调用 呼叫 callback 回调 回呼 call Opel飞ltor call 操作符 call 罩,摩子 E!Je时凹凸+中文鼠第三版译序 .. 英主术遁 简体版译词 '体 iii 诗词 character 字符 字兀 ·child class 子突 子姐~'J -class 樊 烦别 ~c1ass template 樊模饭 般~IJtl!缸 client 客尸 客乒 '''''' 代罔 桓武晤 compatible 嫌容 相睿 '0τmile time 编译期 圄嚣翩 compiler 编海雹 画嚣嚣 回mponent 组件 粗件 回mPOSition 直合 搜古 concrete 且2厚的 且 111的 concurrent 并芷 韭行 configuration 配置 酣睡 connectIon 迎接 啤幢,且银 cons田>0 1 的束〈条件〉 的束(售条件) construct 构件 幡{牛 contam町 容譬 容器 'const (C忡关键字.代在阳菌圆。 constant ,曹量 衔'皮 <0田tructor 构造函数 惶摘茸 ·∞py (动词〉 防贝 冉固、搜瞿 CODY (名词) 直仲、剧本 模件、剧本 create 创建 座生、睡宜生JOC custom .1£制 盯制白直 e 数据库 资料睡 data member 成员变量 成员'睡敏 fi t巳:rface 钱口 介面 Internet 五联网 嗣晦桐阳 mterpreter 解事奉告S E胃器 invariants 恒常性 国常性 invoke 调网 噎起 Iterator 迭代缉 选代嚣 library 程序库 程式障 linker 连岳磕 理桔器 literal 字面常量 字面常微 Efficti甜。+巾主版,第三版译序 萃'" 英文'"司' 简体版译词 量体艇部词 -list 链褒 盼到 load 统戴 雕λ -local 血郁的 E域的 lock 机领 幢鲸 1000 循环 罩圈 Ivalut 左值 左‘自 mocro 宏 Ii!阜 membe< 成员 成员 member function 成员函数 匾局面式 memory 内存 自己幢幢 memory leak 内存油漏 自己幢幢植由 meta- '" 菌 • meta-Drouamminll. 冗躏程 姐届程 m创:Ie ling 型模 檀塑 πlOd ule 候!I< 惺圄 πK对 ifier 修饰符 筒胃 multi-taslcin 多任务 多工 • namesoace 命名空间 命也"l!1IlI native 固有的 原生的 nested 般套 候套、巢肤 obiect 对象 鞠件 。bject based 羞于对象的 植基世骨件且相件篇矗砸 。biect me由 1 对象罐里 鞠件檀型 00; 自 t oriented 面向对象 鞠件耀向 。回"",d 操作数 噩'事兀 。·peratlng syste川、 操作革统 作蒙系就 。因 rator 操作符 盟第子 。verllow 溢出 上阻溢位 。verhead 徽外开销 踵夕$圃'自 刷刷刷d 置'震 量'院 override 理写 1I11 oaεk.. , 包 套件 parallel 并行 卒行 parameter ,放形, l Oll 金 oarent class ~l题 立盟别 D叫 M析 解析 oartial s时cial也allon 偏特化 僵持化 • pass by referenc:怒 t拿址传递 德址 'oass bv 咀I 回 核值传递 II值 oattern 模式 E式 'placement delete 〈某种特殊形式的 delete ()[阳1ltor ) •placement new 《某种特殊形式的时wo睛I1\lor) pomter 衍针 指樱 句foe"田 C++中文鼠盟三版... i拳序 真主术语 简体版译词 '体Ii<译词 polyπ lOrphism 多态 多型 P町阴阳『 预处理糖 前a理1II onn! 打印 ,到j印 onn!~ 打印机 即袋幢 proc= 选程 行捏 穰序 理式 程序虽 理式... progr虱mmtn2 编程 羁扭 project 项目 尊案 阴阳docode 伪同 简圈 q旧Iity 质量 品贸 咱皿uo 队 J' 拧,日j taW 匾始的未经处理的 原拍的‘未粗虞理的 recursIve i晶归 罩迥 refer to 指涉、指称‘指向 描涉、指帽、帽向 'rer\巳 renee 引用 '考、寻阳 regular expression 正如l 表达式 正且IJ算式 resolve 解析 t提醒 『、otu口、 远回 回堪‘傅固 retum tvoe 返回樊型 回返型jJlJ return value 返回值 回远笛 runume 运行则 锁行期 ""I田 右值 右‘自 ..四 在储 髓存 schedule 调匮 悻徨 ",hx 目最 川B 序言 阻' 致谢 阳'" 导读 l l 让自己习惯 c++...........................................................................................11 Accustoming Yourselfωc++............................................................................11 条耻 01 ,咀 C++ 为一个语言联邦 11 View C++ as a federation oflanguages................................. 11 条献。 2 ,思量以 canst. 因 um.inline 管挟 'define .................................13 Prefer consts.enums, and inlines (0 #defines ……… 13 条款 03 且可能使用 canst Use const whenever possible 条吕京 04 确定时象被使用前已先被韧曲化 Make sure Ihal objects are inilialized before they're used 77661122 2 构造/析构/匾值运算 34 Constructors. Destructors. and Assignment Operators .......................................34 条耻 05 了解 C++ 默默描写并调用哪些函盘 Know what functions C++ silently writes and calls 34 34 条耻 06 若不想使用植译辙自萌生腊的函盘,就该明确拒地 37 Explicitly disallow the use of com阴 ler-gene 划。d funCI阳时 you do not want... 37 条 ~07 ,为多击基费声明阴阳 .1 析构函散 40 Declare destructors vinual in polymorphic base classes. ..40 R萨en四 C++中主陋,第三陋xvm 目最 条款 08 别让异常逃离析构函盘 44 h酌"拭目 C 叩 tions from 1国 ving destn四阳军 ............................................44 条Ii.回绝不在构造农 l 昕构过程中调用 vinual 函量 48 N~~ 国 11 vinual functions duringα:ms truetion or destruction......................48 条Ii. 10 ,令。 perator- 温回一个 reference to *this................................... 52 Have assignment operators return a reference to ·this.................................... 52 条耻 II 在。 perator- 中处理"臼拽院由" .................................................53 Handle assignment to selfin operalor-………… 53 条耻 12 坦制 >1 忽时却志主 t每一个成分 Copyall 阴阳。 fan obiect … " 57 57 3 噩噩管理 61 Resource Management........................................................................................61 条 "13 , l;l对象管理置源 61 U提出j 院15 to manage r回a阻挡。 61 条':1 4 在暨源管理类中 4 心目'ping 行为 66 Thmk 姐refullyaboul ∞pying beha 闸门n=皿=-~皿 ging c1 as瞄- 国 条放 15: 在置面管理费中提供时匾站暨髓的访问 69 Provide aee础 ω"W =O皿"CCS in 晒剧 rc e- managing CI路峭 的 条瞌 16 成对使用 new 和 delete 时要果取相同JI'式 73 Use the 副me form in eOlTCsponding usc喝。 f new and delete. ..................73 条融 17 ,以植宜语句将 newed 时忽置入1'1能指针 75 Slore ncwcd 0同 ects In smart 阳nlcrs 10 stan 由 lonc stalcmcnts. … 75 4 设计与声明 78 D由igns and Declarations.................................................................................... 78 4挺直~ 18 ,让撞口容县瞌正确使用,不品匾误用 78 Make interfaces easy to use eOrTee‘ Iyand hard to use incorrectly. 78 条Ii. 19, ill:计"""犹如世计叩pc........................................................................84 Treat class design as type design. 84 条量 20 宁U 阳ss-by-rcfer回也e-Io-四回I f!换回ss-by-value 86 阶.r,町回,,-bγreference-In-号。 nSllO 阻ss-by-咀 luc........................................86 条款 21 必细垣回时且时,知j 妄想埠固其 referencc 90 Do的町 to return a refcrence when you must return an object. 90 条耻 22: 将fIX.虽变量声明为 private................... ...............................................94 Declare data membe市 private.........................................................................94 条融 23 ,宁 U 阳>mCr、 m吐白'"铛缺 mom~ 函量 98 Prefer non-member non-嗣町XI fimctions to mer羽kτfunctior 四 98 条Ii. 24 ,者所有"数皆菁英型转换,请为此果用 non-member 函瞌 102 O II 模板 (template) 声明式 class GraphNode; II"typename" 的使用见条款42 注意,我谈到整数 x 时称其为一个对象 (object) ,即使它是个内置类型。某 些人把"对象"一词保留给用户自定义类型(user-definedtype) 的变量,但我并不 如此。也请注意,函数numDigit 的返回类型是 std: :size t ,这表示类型 size t 位于命名空间 std 内。这个命名空间是几乎所有 C++ 标准程序库元素的栖身处。 然而 C (正确说法是 C89) 标准程序库也适用于 C++ ,而继承自 C 的符号(例如 size t>有可能存在于global 作用域或 std 内,甚或两者兼具,取决于哪个头文件 被含入(#included) 。本书之中我假设含入的都是C++ 头文件,这也就是为什么 我写 std: :size t 而不只是写 size t 。当我在文本中指称标准程序库内的组件时, 往往略去前导的std: :,你得自己认清像size t , vector, cout 这类东西都在 std 内。但范例码中我总是会含入 std,因为真实程序编译时不能没有它。 顺带一提, size t 只是一个 typedef,是 C++ 计算个数(例如cha沪-based 字 符串内的字符个数或 STL 容器内的元素个数等等)时用的某种不带正负号 (unsigned) 类型。它也是vector, deque 和 string 内的 operator[]函数接受的参 数类型。条款3 阐述当我们定义自己的operator[]函数时应该遵循的协议。 每个函数的声明揭示其签名式Csignature) , 也就是参数和返回类型。一个函数 E1知ctive C++中文版,第三版4 导读 的签名等同于该函数的类型。口urnDigits 函数的签名是 std: : size t (int) ,也就 是说"这函数获得一个 int 并返回一个 std: :size t" 0 C++ 对签名式的官方定 义并不包括函数的返回类型,不过本书把返回类型视为签名的一部分,这样比较有 帮助。 定义式( definition) 的任务是提供编译器一些声明式所遗漏的细节。对对象而 言,定义式是编译器为此对象拨发内存的地点。对 function 或 function template 而 言,定义式提供了代码本体。对 class 或 class template 而言,定义式列出它们的成员: int x; II对象的定义式 std::size t nurnDigits(int number) II 函数的定义式 II此函数返回其参数的数字个数, std::size t digitsSoFar = 1; II例如十位数返回2. 百位数返回 3. while ((number 1= 10) != 0) ++digitsSoFar; return digitsSoFar; class Widget ( public: Widget(); -Widget(); ternp1ate class GraphNode ( public: GraphNode(); -GraphNode(); Ilclass 的定义式 lltemplate 的定义式 初始化(Initialization) 是"给予对象初值"的过程。对用户自定义类型的对象 而言,初始化由构造函数执行。所谓default构造函数是一个可被调用而不带任何实 参者。这样的构造函数要不没有参数,要不就是每个参数都有缺省值2 class A( public: A(); Ildefault构造函数 class B( public: explicit B(int x = 0, bool b = true); Ildefauft构造函数; II关于 "explicit".见以下信息 EJ.知ctive C++中文版F 第三版导读 class C{ public: explicit C (工 nt x); II不是 default构造函数 5 上述的 classes B 和 C 的构造函数都被声明为 explic址,这可阻止它们被用来 执行隐式类型转换 (implicit type conversions) ,但它们仍可被用来进行显式类型转 换 (explicit type conversions) : void doSomething(B bObject); II 函数,接受→个类型为B 的对象 doSomething(28); doSomething(B(28)); B bObjl; doSomething(bObjl); B bObj2(28); II一个类型为B 的对象 II没问题,传递一个B 给 doSomething函数 II没问题,根据int28 建立一个B II(函数的 b∞1 参数缺省为 true) II错误! DoSomething 应该接受一个 B , II 不是二个时,而 int 至 B 之间 II 并没有隐式转换。 II 没问题,使用 B 构造函数将 int 显式转换 II (也就是转型,ωst) 为一个B 以促成此一调用. II (条款 27 对转型谈得更多) 被声明为 explicit 的构造函数通常比其non-explici兄弟更受欢迎,因为它 们禁止编译器执行非预期(往往也不被期望)的类型转换。除非我有一个好理由允 许构造函数被用于隐式类型转换,否则我会把它声明为explicit。我鼓励你遵循 相同的政策。 请注意我在上述代码中以不同的颜色特别强调转型动作。我以这样的强调方式 贯穿全书,让你特别注意值得注意的东西。 copy构造函数被用来"以同型对象初始化自我对象", copyass够nment 操作符 被用来"从另一个同型对象中拷贝其值到自我对象" : class Widget { public: Widget(); Widget(const Widget& rhs); Widget& operator=(const Widget& rhs); Widget wl; Widget w2 (wl) ; wl = w2; Ildefault构造函数 II copy 构造函数 Ilcopyass够nment 操作符 II 调用 default 构造函数 II 调用 copy 构造函数 II 调用 copyassignment 操作符 Effective C++ 中文版,第三版6 当你看到赋值符号时请小心,因为"="语法也可用来调用 copy 构造函数: 导读 Widget w3 = w2; II 调用 copy 构造函数! 幸运的是 " copy 构造"很容易和 "copy 赋值"有所区别。如果一个新对象被定 义(例如以上语句中的 w纱,一定会有个构造函数被调用,不可能调用赋值操作。 如果没有新对象被定义(例如前述的 "wi = w2" 语句) ,就不会有构造函数被调 用,那么当然就是赋值操作被调用。 copy 构造函数是一个尤其重要的函数,因为它定义一个对象如何 passed by value (以值传递)。举个例子,考虑以下代码: bool hasAcceptabieQuality(Widget w); Widget aWidget; if (hasAcceptableQuality(aWidget)) 参数 w 是以 by value 方式传递给 hasAcceptableQuality,所以在上述调用中 aWidget 被复制到 w 体内。这个复制动作由 Widget 的 copy 构造函数完成。 Pass-by-value意味"调用 copy构造函数"。以byvalue传递用户自定义类型通常是 个坏主意, Pass-by-reference-to-cons t 往往是比较好的选择:详见条款20 。 STL 是所谓标准模板库 (Standard Template Library) ,是 C++ 标准程序库的一 部分,致力于容器(如vector, list, set, map 等等)、迭代器(如vector: : iterator, set: :iterator 等等)、算法(如 for_each , find, sort 等等) 及相关机能。许多相关机能以函数对象 (funcrωno句ec齿)实现,那是"行为像函 数"的对象。这样的对象来自于重载 operator () (function call 操作符)的 classes 。 如果你对 STL 陌生,阅读本书时手边可能需要摆一本最新参考读物,因为STL 对 我太有用了,我不可能不用它。一旦你也用上它,你一定会有相同的感觉。 C++ 程序员如果原先来自诸如 Java 或 C# 语言阵营,可能会对所谓"不明确 行为" (undefined behavior) 感到惊讶。由于各种因素,某些 C++ 构件的行为没 有定义z 你无法稳定预估运行期会发生什么事。下面两个代码片段就带有"不明确 的行为" : int* p = 0; std: :cout « *p; E1知ctive C++中文版,第三版 lip 是个 null 指针 II对一个 null 指针取值 (dereferencing) II会导致不明确行为。导读 7 char name [] =吨arla勺 Ilname 是个数组,大小为 6 (别忘记最尾端的 null! ) char c = name[lO]; II指涉一个无效的数组索引 II导致不明确行为。 我要特别强调,不明确(未定义)行为的结果是不可预期的,很可能让人不偷 快。经验丰富的 C++ 程序员常说,带有不明确行为的程序会抹煞你的辛勤努力。 那是真的:一个带有不明确行为的程序会抹煞你的辛勤努力。但不一定如此,更可 能的是这样的程序会出现错误行为,有时执行正常,有时造成崩坏,有时更产出不 正确的结果。有战斗力的C++ 程序员都知道尽可能避开不明确行为。我会在书中 指出你需要密切注意的若干地方。 对其他语言转换至 C++ 阵营的程序员而言,另一个可能造成困惑的术语是接 口 (interface) 0 Java 和 .NET 语言都提供 Interfaces 为语言元素,但 C++ 没有, 尽管条款 31 讨论了如何近似它。当我使用术语"接口"时,我一般谈的是函数的 签名( signature) 或 class 的可访问元素(例如我可能会说 class 的 "public 接口"或 "protected 接口"或 "private 接口" ) ,或是针对某 template 类型参数需为有效的 一个表达式(见条款 4 J)。也就是我所说的接口完全是指一般性的设计观念。 所谓客户( client) 是指某人或某物,他(或它)使用你写的代码(通常是'些 接口)。函数的客户是指其使用者,也就是程序中调用函数(或取其地址)的那→ 部分,也可以说是编写并维护那些代码的人。 Class 或 template 的客户则是指程序 中使用 class 或 template 的那一部分,也可以说是编写并维护那些代码的人。说到 "客户"时通常我指的是程序员,因为程序员可能被迷惑、被误导、或因糟糕的接 口而恼怒,他们所写的代码却不会有这种情绪。 或许你不习惯想到客户,但我会花费大量时间试着说服你尽可能让他们的生活 轻松些。毕竟你也是其他人所开发的软件的客户。难道你不希望那些人为你把事情 弄得更轻松些吗?除此之外,在某个时间点你几乎必然会发现,你就是你自己的客 户(也就是使用你自己写的代码) ,那个时候你就会很高兴你在开发接口时把客户 放在心上了。 Effective C++ 中文版,第三版8 导读 本书中我常常掩盖 functions 和 function templates 之间的区别,以及 classes 和 class templates 之间的区别。那是因为对其中之一为真者往往对另一方也为真。当不 是这种情况的时候,我会区分 classes, functions 及它们所对应的 templates 。 当我在程序批注中提到构造函数和析构函数时,有时我会使用缩写字ctOY 和 dtoy。 命名习惯 (Naming Cony创ions) 我尝试挑选有意义的名称用于 obje邸, classes, functions, template吕等等身上,但 某些隐藏于名称背后的意义可能不是那么显而易见,例如我最喜爱的两个参数名称 lhs 和 rhs 。它们分别代表 "left-hand side" (左于端)和 "right-hand side" (右手端)。 我常常以它们作为二元操作符 (binary operators) 函数如 operator== 和 operator食 的参数名称。举个例子,如果 a 和 b 表示两个有理数对象,而如果 Rational 对象 可被一个 non-member operator* 函数执行乘法(如条款 24 所言),那么下面表达式: a* b 等价于以下的函数调用 z operator*(a, b) 在条款 24 中我声明此一 operator* 如下: const Rational operator* (const Rational& lhs, const Rational& rhs); 如你所见,左操作数a 变成函数内的 lhs ,右操作数b 则变成 rhs 。 对于成员函数,左侧实参由this 指针表现出来,所以有时我单独使用参数名 称 rhs。你可能已经在第5 页的若干 Widget 成员函数声明中注意到了这一点。对 了,我经常以Widget class 示例, "Widget" 并不代表任何东西,它只是当我需要一 个示范用的 class 名称时偶尔采用的名称,它和GUI toolkits 的 widgets 完全无关。 我常将"指向一个T 型对象"的指针命名为pt,意思是 "pointer to T" 。下面是 一些例子: Widget* pw; class Airplane; Airplane* pa; £1知ctive C++中文版,第三版 / /pw= 平Itt to Widget". / /pa="伊 to Airplane".导读 class GameCharacter; GameCharacter* pgc; //pgc="p仕 toG组neCh缸'llCter" 9 对于 referenc巳s 我使用类似习惯:即可能是个 reference ωWidget , ra 则是个 reference to Airplane 。 当我讨论成员函数时,偶尔会以 mf 为名。 关于线程(Threading Consideration) 作为一个语言, C忡对线程(世rreads) 没有任何意念一一一事实上它对任何并发 ( concurrency) 事物都没有意念。 C++ 标准程序库也一样。当 C++ 受到全世界关 注时多线程( multithreaded) 程序还不存在。 但现在它们存在了。本书的焦点放在标准可移植的 C++ ,但我不能忽略二个事 实:线程安全性 (thread safety) 是许多程序员面对的主题。我对"标准 C++ 和真 实世界之间的这个缺口"的处理方式是,如果我所检验的 C++ 构件在多线程环境 中有可能引发问题,就把它指出来。这远远无法构成一本 C++多钱程编程专著,却 能让一本 C++ 编程书籍尽管大量限制其自身处于单线程考虑之下仍承认多钱程的 存在,并指出"有线程概念的程序员"在评估我所提供的忠告时需特别谨慎的地方。 如果你不熟悉多线程或无需忧虑它,可以忽略本书的线程相关讨论。然而如果 你正在编写一个与线程有关的应用程序或程序库,请记住,我的注释或许比→般"以 C++ 解决问题时需注意……"的起点还多→些些。 TRl 和 Boost 你会发现,本书处处提到 TRI 和 Boost 。各有一个条款详细描述它们(条款 54 针对 TR I,条款 55 针对 Boost) 。不幸的是这些条款位于全书末尾(它们被放在那 儿是因为那样的安排比较好。真的,我试过其他许多摆法)。如果你喜欢,可以现 在就翻过去读它们,但如果你喜欢从头读起而不颠倒次序,下面的实施摘要将助你 飞渡难关: • TRI ("Technical Report I") 是一份规范,描述加入 C++ 标准程序库的诸多新机 能。这些机能以新的 class templates 和 function templates 形式体现,针对的题目 有 hash tables, reference-counting smart pointers, regular expressions ,以及更多。 所有 TRl 组件都被置于命名空间 t r1内,后者嵌套于命名空间 std 内。 £1知ctive C++ 中文版,第三版10 导读 • Boost 是个组织,亦是一个网站 (http://boost. org) ,提供可移植、同僚复审、源 码开放的 C++ 程序库。大多数 TRl 机能是以 Boost 的工作为基础。在编译器厂 商于其 C++ 程序库中含入 TRl 之前,对那些搜寻 TRl 实现品的开发人员而言, Boost 网站可能是第一个逗留点。 Boost 提供比 TRl 更多的东西,所以无论如何 值得了解它。 £1知ctive C++ 中文版,第三版1 条款。 I: 视 C++ 为一个语言联邦 11 1 让自己习惯 C++ Accustoming Yourself to C++ 不论你的编程背景是什么, C++ 都可能让你觉得有点儿熟悉。它是一个威力 强大的语言,带着众多特性,但是在你可以驾驭其威力并有效运用其特性之前,你 必须先习惯 C++ 的办事方式。本书谈的便是这个。总有某些东西比其他更基础些, 本章就是最基本的一些东西。 条款 01: 视 C++ 为一个语言联邦 View C++ as a federation oflanguages. 一开始 , C++只是 C 加上一些面向对象特性。 C++ 最初的名称C with Classes 也反映了这个血缘关系。 但是当这个语言逐渐成熟,它变得更活跃更无拘束,更大胆更冒险,开始接受 不同于 C with Classes 的各种观念、特性和编程战略。 Exceptions (异常)对函数的 结构化带来不同的做法(见条款 29) , templates (模板)将我们带到新的设计思考 方式(见条款 4 1), STL 则定义了一个前所未见的伸展性做法。 今天的 C++ 已经是个多重范型编程语言 (multiparadigm programming language) ,一个同时支持过程形式(procedural) 、面向对象形式(object-oriented)、 函数形式(functional) 、泛型形式 (generic) 、元编程形式(metaprogramming ) 的语言。这些能力和弹性使 C++ 成为一个无可匹敌的工具,但也可能引发某些迷 惑:所有"适当用法"似乎都有例外。我们该如何理解这样一个语言呢? 最简单的方法是将 C++ 视为一个由相关语言组成的联邦而非单一语言。在其 某个次语言 (sublanguage) 中,各种守则与通例都倾向简单、直观易懂、并且容易 Ei.作ctive C++ 中文版,第三版12 1 让自己习惯 C++ 记住。然而当你从一个次语言移往另一个次语言,守则可能改变。为了理解 C++ , 你必须认识其主要的次语言。幸运的是总共只有四个: • C 。说到底 C++ 仍是以 C 为基础。区块 (blocks) 、语句( statements) 、预处 理器( preprocessor) 、内置数据类型 (built-in data types) 、数组 (aπays) 、 指针 (pointers) 等统统来自 C 。许多时候 C++ 对问题的解法其实不过就是较高 级的 C 解法(例如条款 2 谈到预处理器之外的另一选择,条款 13 谈到以对象管 理资源) ,但当你以 C++ 内的 C 成分工作时,高效编程守则映照出 C 语言的 局限:没有模板(templates) ,没有异常(exceptions) ,没有重载(overloading) ..…· • Object-Oriented C忡。这部分也就是 C with Classes 所诉求的: classes (包括构 造函数和析构函数) ,封装( encapsulation) 、继承( inheritance) 、多态 (polymorphism) 、 virtual 函数(动态绑定) ......等等。这一部分是面向对象设 计之古典守则在 C++ 上的最直接实施。 • Template C忡。这是 C++ 的泛型编程 (generic programming) 部分,也是大多 数程序员经验最少的部分。 Template 相关考虑与设计己经弥漫整个C++ ,良好 编程守则中"惟 template 适用"的特殊条款并不罕见(例如条款46 谈到调用 template functions 时如何协助类型转换)。实际上由于 templates 威力强大,它 们带来崭新的编程范型 (programming paradigm) ,也就是所谓的 template metaprogramming (TMP,模板元编程)。条款48 对此提供了一份概述,但除 非你是 template 激进团队的中坚骨干,大可不必太担心这些。 TMP 相关规则很 少与 C++ 主流编程互相影响。 • STL. STL 是个 template 程序库,看名称也知道,但它是非常特殊的一个。它对 容器(containers) 、迭代器(iterators) 、算法(algorithms) 以及函数对象(function objects) 的规约有极佳的紧密配合与协调,然而templates 及程序库也可以其他 想法建置出来。 STL 有自己特殊的办事方式,当你伙同STL 一起工作,你必须 遵守它的规约。 记住这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要求 你改变策略时,不要感到惊讶。例如对内置〈也就是巳like) 类型而言 pass-by-va/ue 通常比 pass-by-reference高效,但当你从Cpa同 ofC++ 移往 Object-OrientedC++ , 由于用户自定义( user-defined )构造函数和析构函数的存在, pass-by-reference-ro-cons t 往往更好。运用 Template C++ 时尤其如此,因为彼时你 E1如ctive C++ 中文版,第三版1 条款 02: 尽量以 const, enum, inline 替换 #define 13 const double AspectRatio = 1.653: 甚至不知道所处理的对象的类型。然而一旦跨入STL 你就会了解,迭代器和函数对 象都是在C 指针之上塑造出来的,所以对STL 的迭代器和函数对象而言,旧式的C pass-by-value守则再次适用〈参数传递方式的选择细节请见条款20) 。 因此我说, C++ 并不是一个带有一组守则的一体语言:它是从四个次语言组 成的联邦政府,每个次语言都有自己的规约。记住这四个次语言你就会发现C++ 容 易了解得多。 请记住 • C++ 高效编程守则视状况而变化,取决于你使用 C++ 的哪→部分。 条款 02: 尽量以 canst, enum, inline 替换 #define Prefer consts,enums, and inlines to #defines. 这个条款或许改为"宁可以编译器替换预处理器"比较好,因为或许 #define 不被视为语言的一部分。那正是它的问题所在。当你做出这样的事情: #define ASPECT RATIO 1.653 记号名称 ASPECT RATIO 也许从未被编译器看见:也许在编译器开始处理源码 之前它就被预处理器移走了。于是记号名称ASPECT RATIO 有可能没进入记号表 (symbol table) 内。于是当你运用此常量但获得一个编译错误信息时,可能会带来 困惑,因为这个错误信息也许会提到 1.653 而不是 ASPECT RATIO 。如果 ASPECT RATIO 被定义在→个非你所写的头文件内,你肯定对 1.653 以及它来自何 处毫无概念,于是你将因为追踪它而浪费时间。这个问题也可能出现在记号式调试 器 (symbolic debugger) 中,原因相同 z 你所使用的名称可能并未进入记号表 (symbol table) 。 解决之道是以一个常量替换上述的宏 (#define) : II 大写名称通常用于宏, II 因此这里改变名称写法。 作为→个语言常量, AspectRatio 肯定会被编译器看到,当然就会进入记号表 内。此外对浮点常量 (floating point constant ,就像本例)而言,使用常量可能比使 用 #define 导致较小量的码,因为预处理器"盲目地将宏名称 ASPECT RATIO 替换 为 1.653" 可能导致目标码 (object code) 出现多份 1.653 ,若改用常量 AspectRatio 绝不会出现相同情况。 Effective C++ 中文版,第三版14 1 让自己习惯 C++ 当我们以常量替换 #defines ,有两种特殊情况值得说说。第一是定义常量指针 ( constant pointers) 。由于常量定义式通常被放在头文件内(以便被不同的源码含 入) , 因此有必要将指针(而不只是指针所指之物)声明为 const 。例如若要在头 文件内定义一个常量的(不变的) char*-based 字符串,你必须写 const 两次: const char* const authorName = "Scott Meyers"; 关于 const 的意义和使用(特别是当它与指针结合时),条款 3 有完整的讨论。 这里值得先提醒你的是, string对象通常比其前辈char*-based合宜,所以上述的 authorName const std::string authorName ("Scott Meyers"); 第二个值得注意的是class 专属常量。为了将常量的作用域(scope) 限制于 class 内,你必须让它成为class 的一个成员 (member) ;而为确保此常量至多只有一份 实体,你必须让它成为一个static 成员: class GamePlayer { private: static const int NumTurns = 5; int scores[NumTurns]; II常量声明式 II使用该常量 然而你所看到的是NumTurns 的声明式而非定义式。通常C++ 要求你对你所使 用的任何东西提供一个定义式,但如果它是个class 专属常量又是static 且为整数类 型 (integral type ,例如 ints, chars, bools) ,则需特殊处理。只要不取它们的地址, 你可以声明并使用它们而无须提供定义式。但如果你取某个class 专属常量的地址, 或纵使你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必 须另外提供定义式如下z const int GamePlayer::NumTurns; IINumTurns的定义: II下面告诉你为什么没有给予数值 请把这个式子放进一个实现文件而非头文件。由于class 常量已在声明时获得 初值(例如先前声明NumTurns 时为它设初值5) ,因此定义时不可以再设初值。 顺带一提,请注意,我们无法利用#define 创建一个 class 专属常量,因为 #defines并不重视作用域 (scope) 。一旦宏被定义,它就在其后的编译过程中有 E1如ctive C++中文版,第二版1 条款 02: 尽量以 const , enum, inline 替换 #define 15 效(除非在某处被 #undef) 。这意味 #defines 不仅不能够用来定义 class 专属常量, 也不能够提供任何封装性,也就是说没有所谓 private #define 这样的东西。而当然 const 成员变量是可以被封装的, Nur盯 urns 就是。 旧式编译器也许不支持上述语法,它们不允许 static 成员在其声明式上获得初 值。此外所谓的" in-class 初值设定"也只允许对整数常量进行。如果你的编译器 不支持上述语法,你可以将初值放在定义式: class CostEstimate { private: static const double FudgeFactor; I Istatic class 常量声明 II位于头文件内 const double I Istatic class 常量定义 CostEstimate:: FudgeFactor = 1.35; II位于实现文件内 这几乎是你在任何时候唯一需要做的事。唯一例外是当你在class 编译期间需 要一个 class 常量值,例如在上述的GamePlayer::scores的数组声明式中(是的, 编译器坚持必须在编译期间知道数组的大小)。这时候万一你的编译器(错误地) 不允许"static 整数型 class 常量"完成"in class 初值设定",可改用所谓的 "the enum hack" 补偿做法。其理论基础是: "一个属于枚举类型 (enumerated type) 的数值 可权充 ints 被使用",于是 GamePlayer 可定义如下: class GamePlayer { private: enum { NumTurns = 5 }; int scores[NumTurns]; I I"也eenumhack" 一令 NumTurns II 成为 5 的一个记号名称. II 这就没问题了 基于数个理由 enum hack 值得我们认识。第一, enum hack 的行为某方面说比 较像 #define 而不像 const,有时候这正是你想要的。例如取→个const 的地址是 合法的,但取一个 enum 的地址就不合法,而取→个#define 的地址通常也不合法。 £1知ctive C++ 中文版,第三版16 1 让自己习惯 e++ 如果你不想让别人获得一个 pointer 或 reference 指向你的某个整数常量, enum 可以 帮助你实现这个约束。(条款 18 对于"通过撰码时的决定实施设计上的约束条件" 谈得更多。)此外虽然优秀的编译器不会为"整数型 const 对象"设定另外的存储 空间(除非你创建一个 pointer 或 reference 指向该对象) ,不够优秀的编译器却可 能如此,而这可能是你不想要的。 Enur略和 #defines 一样绝不会导致非必要的内存 分配。 认识 enumhack 的第二个理由纯粹是为了实用主义。许多代码用了它,所以看 到它时你必须认识它。事实上 "enum hack" 是 template metaprogramming (模板元 编程,见条款 48) 的基础技术。 把焦点拉回预处理器。另一个常见的#define 误用情况是以它实现宏(macros) 。 宏看起来像函数,但不会招致函数调用(function call) 带来的额外开销。下面这个 宏夹带着宏实参,调用函数 f: II 以 a 和 b 的较大值调用 f #define CALL WITH MAX (a, b) f ((a) > (b) ? (a) : (b)) 这般长相的宏有着太多缺点,光是想到它们就让人痛苦不堪。 无论何时当你写出这种宏,你必须记住为宏中的所有实参加上小括号,否则某 些人在表达式中调用这个宏时可能会遭遇麻烦。但纵使你为所有实参加上小括号, 看看下面不可思议的事情: int a = 5 , b = 0; CALL W工 TH MAX(++a, b); CALL WITH MAX(++a , b+l0); Ila 被累加二次 Ila 被累加一次 在这里,调用 f 之前, a 的递增次数竟然取决于"它被拿来和谁比较"! 幸运的是你不需要对这种无聊事情提供温床。你可以获得宏带来的效率以及一 般函数的所有可预料行为和类型安全性 (type safety)一一只要你写出 template inline 函数(见条款 30) : template II 由于我们不知道 inline void callWithMax(const T& a , const T& b) liT 是什么,所以采用 llpass by refi町ence-ω-const. f(a > b ? a : b); II见条款 20. 这个 template 产出一整群函数,每个函数都接受两个同型对象,并以其中较大 E庐ctive C++中文版,第三版1 条款 03: 尽可能使用 const 17 者调用 f 。这里不需要在函数本体中为参数加上括号,也不需要操心参数被核算〈求 值)多次……等等。此外由于 callWithMax 是个真正的函数,它遵守作用域 (scope) 和访问规则。例如你绝对可以写出一个 "class 内的 private inline 函数"。一般而言 宏无法完成此事。 有了 cansts 、 enums 和 inlines ,我们对预处理器(特别是 #define) 的需求 降低了,但并非完全消除。 #include 仍然是必需品,而 #ifdef/#ifndef 也继续扮 演控制编译的重要角色。目前还不到预处理器全面引迫的时候,但你应该明确地给 予它更长更频繁的假期。 请记住 ·对于单纯常量,最好以 canst 对象或 enums 替换 #defineso ·对于形似函数的宏 (macros) ,最好改用 inline 函数替换 #defines 。 条款 03: 尽可能使用 const Use const whenever possible. canst 的一件奇妙事情是,它允许你指定一个语义约束(也就是指定一个"不 该被改动"的对象) ,而编译器会强制实施这项约束。它允许你告诉编译器和其他 程序员某值应该保持不变。只要这(某值保持不变)是事实,你就该确实说出来, 因为说出来可以获得编译器的襄助,确保这条约束不被违反。 关键字 canst 多才多艺。你可以用它在classes 外部修饰 global 或 namespace(见 条款 2) 作用域中的常量,或修饰文件、函数、或区块作用域(block scope) 中被 声明为 static 的对象。你也可以用它修饰classes 内部的 static 和 non-static 成员变 量。面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不〉是canst: .,qJ口Elt Je".,., e oqJαJE14nnqd14·1·1ett=Hee";eep qJrr znqdOJt -ls -J←」==口rleoqeppcnr'lqJ*t*trSEe--anaehohFPCCC 4xt*t ZESESaana 口 hhohoccccc Ilnon-const point町, non-const data Ilnon-const poin町, constdata IIconst poin t!汀, non-const data IIconst point眩, cω1st data E1知ctive C村中文版,第三版18 1 让自己习惯 C++ const 语法虽然变化多端,但并不莫测高深。如果关键字 const 出现在星号左 边,表示被指物是常量:如果出现在星号右边,表示指针自身是常量:如果出现在 星号两边,表示被指物和指针两者都是常量。 如果被指物是常量,有些程序员会将关键字 const 写在类型之前,有些人会把 它写在类型之后、星号之前。两种写法的意义相同,所以下列两个函数接受的参数 类型是一样的 z void f1(const Widget* pw); void f2(Widget const * pw); 两种形式都有人用,你应该试着习惯它们。 II 旦获得一个指针,指向一个 II常量的(不变的) Widget对象. IIf2 也是 STL 选代器系以指针为根据塑模出来,所以迭代器的作用就像个?指针。声 明选代器为 const 就像声明指针为 const 一样(即声明一个T* const 指针) ,表 示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果你希 望迭代器所指的东西不可被改动(即希望STL 模拟一个 const T* 指针) ,你需要 的是 const iterator: std::vector vec; const std::vector::iterator iter = vec.begin( ); 食 iter = 10; ++iter; std: :飞rector::const iterator clter = vec.begin( ); *clter = 10; ++clter; Iliter 的作用像个T* const II没问题,改变iter 所指物 II错误! iter 是∞nst Ilclter 的作用像个const T* II 错误! *clter 是∞ nst II 没问题,改变 cltero const 最具威力的用法是面对函数声明时的应用。在一个函数声明式内, const 可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。 令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于 放弃安全性和高效性。举个例子,考虑有理数(rational numbers,详见条款24) 的 operator* 声明式z class Rational { ... }; const Rational operator* (const Rational& lhs, const Rational& rhs); Effective C++ 中文版,第三版1 条款 03: 尽可能使用 const 19 许多程序员第一次看到这个声明时不免斜着眼睛说,晤,为什么返回…个 const 对象?原因是如果不这样客户就能实现这样的暴行 z Rational a, b, c; (a * b) = c; II在 a * b 的成果上调用 operator= 我不知道为什么会有人想对两个数值的乘积再做一次赋值( assignment) .但 我知道许多程序员会在无意识中那么做,只因为单纯的打字错误(以及一个可被隐 式转换为 bool 的类型) : if (a * b = c) ••• I I 喔欧,其实是想做一个比较〈∞mpari阳1) 动作! 如果 a 和 b 都是内置类型,这样的代码直截了当就是不合法。而一个"良好的 用户自定义类型"的特征是它们避免无端地与内置类型不兼容(见条款 18) .因 此允许对两值乘积做赋值动作也就没什么意思了。将 operato沪的回传值声明为 const 可以预防那个"没意思的赋值动作",这就是该那么做的原因。 至于 const 参数,没有什么特别新颖的观念,它们不过就像 local const 对象 一样,你应该在必要使用它们的时候使用它们。除非你有需要改动参数或 local 对 象,否则请将它们声明为 const 。只不过多打 6 个字符,却可以省下恼人的错误, 像是"想要键入'='却意外键成'=' "的错误,一如稍早所述。 const 成员函数 将 const 实施于成员函数的目的,是为了确认该成员函数可作用于 const 对象 身上。这一类成员函数之所以重要,基于两个理由。第一,它们使 class 接口比较 容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重 要。第二,它们使"操作 const 对象"成为可能。这对编写高效代码是个关键,因 为如条款 20 所言,改善 C++ 程序效率的一个根本办法是以 pass by reference-to-const 方式传递对象,而此技术可行的前提是,我们有const 成员函数 可用来处理取得(并经修饰而成)的canst 对象。 许多人漠视一件事实:两个成员函数如果只是常量性(constness )不同,可以 被重载。这实在是一个重要的 C++ 特性。考虑以下 class. 用来表现一大块文字: Effective C++ 中文版,第三版20 class TextBlock { public: const char& operator[] (std::size t position) { return text[position]; ) char& operator[] (std::size_t position) { return text[position]; ) private: std::string text; }; TextBlock 的 operator[]s 可被这么使用 z l 让自己习惯 C++ const Iloperator[] fur Ilconst 对象 I loperator [] for IInon-const 对象 TextBlock tb ("Hello") ; std::cout « tb[O]; II调用 non-const TextBlock::operator[] const TextBlock ctb("World"); std::cout « ctb[O]; II调用 const TextBlock::operator[] 附带一提,真实程序中const 对象大多用于 passed by pointer-ta-const 或 passed by reference-ta-const 的传递结果。上述的ctb 例子太过造作,下面这个比较真实: void print(const TextBlock& ctb) II此函数中 ctb 是 ω,nst std::cout « ctb[O]; II调用 const TextBlock: : operator [] 只要重载 operator门并对不同的版本给予不同的返回类型,就可以令const 和 non-const TextBlocks获得不同的处理: std::cout « tb[O]; tb[O] = 'x'; std::cout « ctb[O]; ctb[O] = 'x'; II没问题一读一个non-const TextBlock II没问题一写一个non-const TextBlock II没问题一读一个const TextBlock II错误!一写一个ω回 TextBlock 注意,上述错误只因operator[] 的返回类型以致,至于operator[] 调用动 作自身没问题。错误起因于企图对一个"由const 版之 operator[] 返回"的 const char& 施行赋值动作。 Effective C++ 中文版 F 第三版1 条款 03: 尽可能使用 const 21 也请注意, non-const operator[] 的返回类型是个 reference ωchar,不是 char。如果 operator[] 只是返回一个 char,下面这样的句子就无法通过编译: tb[O] = 'x'; 那是因为,如果函数的返回类型是个内置类型,那么改动函数返回值从来就不 合法。纵使合法, C++以 byvalue返回对象这一事实(见条款20) 意味被改动的其 实是 tb.text[O]的一个副本,不是tb.text[O] 自身,那不会是你想要的行为。 让我们为哲学思辨喊一次暂停。成员函数如果是const 意味什么?这有两个流 行概念: bitwise canstness (又称 physical constness) 和 logical constness。 bitwise canst 阵营的人相信,成员函数只有在不更改对象之任何成员变量(static 除外)时才可以说是 const。也就是说它不更改对象内的任何一个恼。这种论点的 好处是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。 bitwise canstness 正是 C++ 对常量'性(constness) 的定义,因此 const 成员函数不可以更 改对象内任何 non-static 成员变量。 不幸的是许多成员函数虽然不十足具备 const 性质却能通过 bitwise 测试。更 具体地说,一个更改了"指针所指物"的成员函数虽然不能算是const ,但如果只 有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const 不会引发编译 器异议。这导致反直观结果。假设我们有一个TextBlock-like class ,它将数据存储 为 char* 而不是 stri呵,因为它需要和一个不认识 string 对象的 CAPI 沟通: class CTextBlock { public: char& operator[] (std::size_t position) const Ilbitwiseconst声明, { return pText[position]; } II 但其实不适当. private: char* pText; 这个 class 不适当地将其 operator[] 声明为 const 成员函数,而该函数却返 回一个 reference 指向对象内部值(条款28 对此有深刻讨论)。假设暂时不管这个 El知ctive C++中文版,第三版22 1 让自己习惯 C++ 事实,请注意. operator []实现代码并不更改 pText。于是编译器很开心地为 operator[]产出目标码。它是bitwise canst. 所有编译器都这么认定。但是看看它 允许发生什么事: const CTextBlock cctb ("Hello"); I I 声明一个常量对象。 char* pc = &cctb[O]; II调用 const operator []取得一个指针, II 指向 cctb 的数据。 食pc = , J' ; II cctb 现在有了 "Jello" 这样的内容。 这其中当然不该有任何错误:你创建一个常量对象并设以某值,而且只对它调用 const 成员函数。但你终究还是改变了它的值。 这种情况导出所谓的 logi臼 I constness D 这一派拥护者主张,一个const 成员 函数可以修改它所处理的对象内的某些bits. 但只有在客户端侦测不出的情况下才 得如此。例如你的CTextBlockclass 有可能高速缓存 (cache) 文本区块的长度以便 应付询问 z class CTextBlock { public: std::size t length() const; private: char* pText; std::size t textLength; II最近一次计算的文本区块长度。 bool lengthIsValid; II 目前的长度是否有效。 std::size t CTextBlock::length() const if (!length工 sValid) { textLength = std::strlen(pText); lengthIsValid = true; return textLength; II错误!在 const 成员函数内 II 不能赋值给 textLength II 和 lengthIsValid。 length 的实现当然不是bitwise ∞nst. 因为 textLength和 lengthIsValid都 可能被修改。这两笔数据被修改对const CTextBlock对象而言虽然可接受,但编 译器不同意。它们坚持以twise constness。怎么办? 解决办法很简单:利用C++ 的一个与 const 相关的摆动场:mutable(可变的)。 mutable释放掉 non-static 成员变量的 bitwise constness 约束: Effective C++ 中文版,第三版l 条款 03: 尽可能使用 const class CTextBlock { public: std::size t length() const; private: char* pText; mutable std::size t textLength; mutable bool le口gthIsValid; } ; std::size t CTextBlock::length() const if (!lengthIsValid) { textLength = std::strlen(pText); lengthIsValid = true; } return textLength; 在 const 和 non-const 成员函数中避免重复 II这些成员变量可能总是 II会被更改,即使在 llconst成员函数内。 II现在,可以这样, II也可以这样。 23 对于 "bitwise- ∞nstness 非我所欲"的问题, mutable 是个解决办法,但它不 能解决所有的 const 相关难题。举个例子,假设 TextBlock (和 CTextBlock) 内 的 operator[] 不单只是返回一个 reference 指向某字符,也执行边界检验 (bounds checking) 、忘记访间信息 (logged access info. )、甚至可能进行数据完善性检验。 把所有这些同时放进 const 和 non-const operator[] 中,导致这样的怪物(暂且 不管那将会成为一个"长度颇为可议"的隐喻式inline 函数一一见条款 30) : class TextBlock { public: const char& operator[] (std::size t position) const { II边界检验 (bounds checking) II志记数据访问(logac臼侃侃侃) II检验数据完整性 (verify d刷 integrity ) return text[position]; } char& operator[] (std::size_t position) II边界检验 (bounds checking) II志记数据访问(log access data) II检验数据完整性(verify data integrity) return text[position]; private: std::string text; Effective C++ 中文版,第三版24 l 让自己习惯 C++ 哎哟!你能说出其中发生的代码重复以及伴随的编译时间、维护、代码膨胀等 令人头痛的问题吗?当然啦,将边界检验……等所杳代码移到另一个成员函数(往 往是个 private) 并令两个版本的 operator 门调用它,是可能的,但你还是重复了 )些代码,例如函数调用、两次 return 语句等等。 你真正该做的是实现 operator 口的机能一次并使用它两次。也就是说,你必 须令其中一个调用另←个。这促使我们将常量性转除 (casting away constness) 。 就一般守则而言,转型 (casting) 是一个糟糕的想法,我将贡献一整个条款来 谈这码事(条款 27) ,告诉你不要那么做。然而代码重复也不是什么令人愉快的 经验。本例中 const operator []完全做掉了 non-const 版本该做的一切,唯一的 不同是其返回类型多了一个const 资格修饰。这种情况下如果将返回值的const 转 除是安全的,因为不论谁调用non-const operator (]都一定首先有个 non-const 对 象,否则就不能够调用 non-const 函数。所以令 non-const operator 门调用其 const 兄弟是-个避免代码重复的安全做法一→即使过程中需要一个转型动作。下面是代 码,稍后有更详细的解释: class TextBlock { public: const char& operator[] (std::size_t position) const II一如既往 return text[position]; char& operator[] (std::size t position) II现在只调用 constop [] return const cast( II将 op(] 返回值的 const 转除 static cast(*this) II为 *this 加上 const [position] II调用∞nst op[] Effective C++ 中文版,第三版l 条款 03: 尽可能使用 const 25 如你所见,这份代码有两个转型动作,而不是一个。我们打算让 non-const operator []调用其 const 兄弟,但 non-const operator门内部若只是单纯调用 operator [] .会递归调用自己。那会大概……晤……进行一百万次。为了避免无穷 递归,我们必须明确指出调用的是 const operator [] .但 C++ 缺乏直接的语法可 以那么做。因此这里将 *this 从其原始类型 TextB工 ock& 转型为 const TextBlock&。是的,我们使用转型操作为它加上const! 所以这里共有两次转型: 第一次用来为女this 添加 const (这使接下来调用 operator[] 时得以调用 const 版本) .第二次则是从 const operator []的返回值中移除const。 添加 const 的那一次转型强迫进行了一次安全转型(将non-const 对象转为 const 对象) .所以我们使用 static cast。移除 const 的那个动作只可以藉由 const cast 完成,没有其他选择(就技术而言其实是有的:一个C-style 转型也行 得通,但一如我在条款 27 所说,那种转型很少是正确的抉择。如果你不熟悉 static cast 或 const cast. 条款 27 提供了→份概要)。 至于其他动作,由于本例调用的是操作符,所以语法有一点点奇特,恐怕无法 赢得选美大赛,但却有我们渴望的"避免代码重复"效果,因为它运用const operator[] 实现出 non-const 版本。为了到达那个目标而写出如此难看的语法是 否值得,只有你能决定,但"运用const 成员函数实现出其non-const孪生兄弟" 的技术是值得了解的。 更值得了解的是,反向做法一一-令const 版本调用 non-const 版本以避免重 复一-并不是你该做的事。记住,const 成员函数承诺绝不改变其对象的逻辑状态 (logical state) , non-const 成员函数却没有这般承诺。如果在 const 函数内调用 non-const 函数,就是冒了这样的风险 z 你曾经承诺不改动的那个对象被改动了。 这就是为什么 "co口 st 成员函数调用 non-const 成员函数"是一种错误行为:因为 对象有可能因此被改动。实际上若要令这样的代码通过编译,你必须使用一个 const cast 将 *this 身上的 const 性质解放掉,这是乌云罩顶的清晰前兆。反向调 用(也就是我们先前使用的那个〉才是安全的:non-const 成员函数本来就可以对 其对象做任何动作,所以在其中调用一个const 成员函数并不会带来风险。这就 是为什么本例以static cast 作用于气his 的原因 z 这里并不存在const 相关危险。 本条款一开始就提醒你, const 是个奇妙且非比寻常的东西。在指针和迭代器 身上:在指针、迭代器及references 指涉的对象身上:在函数参数和返回类型身上: El知ctive C++中文版,第三版26 1 让自己习惯 C++ 在 local 变量身上:在成员函数身上,林林总总不一而足。 const 是个威力强大的助 手。尽可能使用它。你会对你的作为感到高兴。 请记住 ·将某些东西声明为 const 可帮助编译器侦测出错误用法。 const 可被施加于任 何作用域内的对象、函数参数、函数返回类型、成员函数本体。 ·编译器强制实施 bitwise constness ,但你编写程序时应该使用"概念上的常量性" (conceptual constness) 。 ·当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调 用 const 版本可避免代码重复。 条款 04: 确定对象被使用前巳先被初始化 Make sure that objects are initialized before they're used. 关于"将对象初始化"这事, C++ 似乎反复无常。如果你这么写: int X; 在某些语境下 x 保证被初始化(为 0) ,但在其他语境中却不保证。如果你这么写: class Point { int x, y; Point p; p 的成员变量有时候被初始化(为 0) ,有时候不会。如果你来自其他语言阵营而 那儿并不存在"无初值对象",那么请小心,因为这颇为重要。 读取未初始化的值会导致不明确的行为。在某些平台上,仅仅只是读取未初始 化的值,就可能让你的程序终止运行。更可能的情况是读入一些"半随机"bits , 污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多令 人不愉快的调试过程。 现在,我们终于有了一些规则,描述"对象的初始化动作何时一定发生,何时 不一定发生"。不幸的是这些规则很复杂,我认为对记忆力而言是太繁复了些。 Effective C++ 中文版,第三版1 条款 04: 确定对象被使用前已先被初始化 27 通常如果你使用 Cpa同 of C++ (见条款I)而且初始化可能招致运行期成本, 那么就不保证发生初始化。一旦进入non-C pa同sofC忡,规则有些变化。这就很好 地解释了为什么array (来自 C part of C++)不保证其内容被初始化,而vector (来 自 STLpa同 ofC++) 却有此保证。 表面上这似乎是个无法决定的状态,而最佳处理办法就是:永远在使用对象之 前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。例如z int x = 0; const char* text = "A C-style string"; double d; II对 int 进行手工初始化 II对指针进行手工初始化 II ( 亦见条款3) std::cin » d; II 以读取 input s出am 的方式完成初始化 至于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors) 身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。 这个规则很容易奉行,重要的是别混淆了赋值(assi伊ment) 和初始化 ( initialization) 。考虑一个用来表现通讯簿的 class ,其构造函数如下: class PhoneNumber { ... }; class ABEntry { IIABEntry =叽Address Book Entry" public: ABEntry(const std::string& name, const std::string& address, const std::list& phones); private: std::string theName; std::string theAddress; std::list thePhones; int numTimesConsulted; ABEntry: :ABEntry(const std: :string& n缸ne , const std: : string& address, const std::list& phones) theName 二口arne; II这些都是赋值 (assignments) , theAddress = address; II耐博J始化 (initializations)。 thePhones = phones; numTimesConsulted = 0; El知ctive C++中文版F 第三版28 l 让自己习惯 C++ 这会导致 ABEntry 对象带有你期望(你指定)的值,但不是最佳做法。 C++ 规 定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在 ABEntry 构造 函数内, theNarr 吼 theAddress 和 thePhones 都不是被初始化,而是被赋值。初始 化的发生时间更早,发生于这些成员的 default 构造函数被自动调用之时(比进入 ABEntry 构造函数本体的时间更早)。但这对 numTimesConsu1ted 不为真,因为它 属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。 ABEntry 构造函数的一个较佳写法是,使用所谓的 member initialization list (成 员初值列〉替换赋值动作: ABEntry: :ABEntry(const std: :string& n缸ne , const std: :string& address, const std::1工 st& phones) :theNarne(narne) , theAddress(address) , II现在,这些都是初始化 (initializations ) thePhones(phones) , numTimesConsu1ted(O) (} II 现在,构造函数本体不必有任何动作 这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版 本(本例第一版本)首先调用 default 构造函数为 theNarne , theAddress 和 thePhones 设初值,然后立刻再对它们赋予新值。 default 构造函数的一切作为因此浪费了。成 员初值列 (member initialization list) 的做法(本例第二版本)避免了这一问题,因 为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实 参。本例中的 theNarne 以口arne 为初值进行 copy构造, theAddress 以 address 为 初值进行 copy构造, thePhones 以 phones 为初值进行 copy构造。 对大多数类型而言,比起先调用default构造函数然后再调用 copy assignment 操作符,单只调用一次 copy 构造函数是比较高效的,有时甚至高效得多。对于内 置型对象如 m凹'imesConsu1ted,其初始化和赋值的成本相同,但为了一致性最好 也通过成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量, 你都可以使用成员初值列,只要指定无物(nothing) 作为初始化实参即可。假设 ABEntry 有一个无参数构造函数,我们可将它实现如下: ABEntry::ABEntry( ) :theNarne() , theAddress() , thePhone 色() , numTimesConsu1ted(O) (} Effective C++ 中文版,第三版 II调用 theNarne 的 de向 ult 构造函数; II 为 theAddress 做类似动作; II 为 thePhones 做类似动作; II记得将 numTimesConsu1ted 显式初始化为 01 条款 04: 确定对象被使用前已先被初始化 29 由于编译器会为用户自定义类型 (user-defined types) 之成员变量自动调用 default 构造函数一一如果那些成员变量在"成员初值列"中没有被指定初值的话, 因而引发某些程序员过度夸张地采用以上写法。那是可理解的,但请立下一个规则, 规定总是在初值列中列出所有成员变量,以免还得记住哪些成员变量(如果它们在 初值列中被遗漏的话〉可以无需初值。举个例子,由于 numTimesConsulted 属于内 置类型,如果成员初值列 (member initialization list) 遗漏了它,它就没有初值,因 而可能开启"不明确行为"的潘多拉盒子。 有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相 同) .也一定得使用初值列。是的,如果成员变量是 const 或 ref注rences. 它们就 -定需要初值,不能被赋值(见条款们。为避免需要记住成员变量何时必须在成 员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样 做有时候绝对必要,且又往往比赋值更高效。 许多 classes 拥有多个构造函数,每个构造函数有自己的成员初值列。如果这 种 classes 存在许多成员变量和/或 base classes. 多份成员初值列的存在就会导致不 受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合 理地在初值列中遗漏那些"赋值表现像初始化一样好"的成员变量,改用它们的赋 值操作,并将那些赋值操作移往某个函数(通常是 private) .供所有构造函数调用。 这种做法在"成员变量的初值系由文件或数据库读入"时特别有用。然而,比起经 由赋值操作完成的"伪初始化" (pseudo-initialization) .通过成员初值列 (member initialization list) 完成的"真正初始化"通常更加可取。 C++ 有着十分固定的"成员初始化次序"。是的,次序总是相同: base class咽 更早于其 derived classes 被初始化(见条款 12) ,而 class 的成员变量总是以其声明 次序被初始化。回头看看 ABEntry. 其 theName 成员永远最先被初始化,然后是 theAddress. 再来是 thePhones. 最后是 numTimesConsulted 。即使它们在成员初 值列中以不同的次序出现(很不幸那是合法的) .也不会有任何影响。为避免你或 你的检阅者迷惑,并避免某些可能存在的晦涩错误,当你在成员初值列中条列各个 成员时,最好总是以其声明次序为次序。 译注:上述所谓晦涩错误,指的是两个成员变量的初始化带有次序性。例如初始化 array 时需要指定大小,因此代表大小的那个成员变量必须先有初值。 一旦你己经很小心地将"内置型成员变量"明确地加以初始化,而且也确保你 的构造函数运用"成员初值列"初始化 base classes 和成员变量,那就只剩唯一- E庐 ctive C++ 中文版F 第二版30 1 让自己习惯 C++ 件事需要操心,就是……昵……深呼吸……"不同编译单元内定义之 non-local static 对象"的初始化次序。 让我们一点一点地探钻这一长串词组。 所谓 static 对象,其寿命从被构造出来直到程序结束为止,因此 stack 和 heap-based 对象都被排除。这种对象包括 global 对象、定义于 namespace 作用域内 的对象、在 classes 内、在函数内、以及在 file 作用域内被声明为 static 的对象。 函数内的 static 对象称为 local static 对象(因为它们对函数而言是 local) ,其他 static 对象称为 non-local static 对象。程序结束时 static 对象会被自动销毁,也就是它们 的析构函数会在旧 in ()结束时被自动调用。 所谓编译单元 class NamedObject ( public: NamedObject(canst char* name, canst T& value); NamedObject(canst std::string& name, canst T& value); private: std::string nameValue; T abjectValue; 由于其中声明了二个构造函数,编译器于是不再为它创建default构造函数。这 恨重要,意味如果你用心设计一个class,其构造函数要求实参,你就无须担心编译 器会毫无挂虑地为你添加一个无实参构造函数(即default构造函数)而遮盖掉你的 版本。 NamedObject既没有声明 copy构造函数,也没有声明copy assignment操作符, 所以编译器会为它创建那些函数(如果它们被调用的话)。现在,看看copy构造函 数的用法z NamedObject nol("Smallest Prime Number" , 2); NamedObject na2(nal); II调用 copy构造函数 Effective C++ 中文版,第三版36 2 构造/析构/赋值运算 编译器生成的 copy 构造函数必须以 nol.nameValue 和 nol.objectValue 为初值 设定 n02.nameValue 和 n02.objectValue 。两者之中, nameValue 的类型是 string , 而标准 stri呵有个 copy 构造函数,所以口∞02.nan肥r s眈trin呵q 的 Cωapy 构造函数并以 nol.n旧an盹I Na皿me时dO∞ibject: :ob均je町ct凹Vaιlu四1坦e 的类型是 in忱1吐t (因为对此 template 具现体而言 T 是 i扣ntυ) ,那是个内置类型,所以 n02.objectValue 会以"拷贝口 o l. objectValue 内的每一个 bits" 来完成初始化。 编译器为 NamedObject 所生的 copy assignment 操作符,其行为基本上与 copy 构造函数如出一辙,但一般而言只有当生出的代码合法且有适当机会证明它有 意义(见下页) ,其表现才会如我先前所说。万一两个条件有一个不符合,编译器 会拒绝为 class 生出 operator= 。 举个例子,假设 NamedObject 定义如下,其中 nameValue 是个 reference ω string, objectValue 是个 const T: template class NamedObject { public: II 以下构造函数如今不再接受一个const 名称,因为 nameValue II如今是个reference-to-non-const stringo 先前那个 char* 构造函数 II 己经过去了,因为必须有个string可供指涉。 NamedObject(std::string& name, const T& value); II如前,假设并未声明。严mωF private: std::string& nameValue; II这如今是个reference const T objectValue; II这如今是个const 现在考虑下面会发生什么事: std: :string newDog("Persephone"); std: :string oldDog("Satch"); NamedObject p(newDog, 2); NamedObject s(oldDog, 36); p = s; II 当初撰写至此,我们的狗Persephone II即将度过其第二个生日。 II我小时候养的狗Satch 则是 36 岁, II一如果她还活着。 II现在p 的成员变量该发生什么事? 赋值之前,不论 p.nameValue和 s.nameValue都指向 string 对象(当然不是 同-个)。 赋值动作该如何影响 p.nameValue 呢? 赋值之后 p.nameValue 应该 Effective C++ 中文版,第三版2 条款 06: 若不想使用编译器自动生成的函数,就该明确拒绝 37 指向 s.nameValue 所指的那个 string 吗?也就是说 reference 自身可被改动吗?如 果是,那可就开辟了新天地,因为 C++ 并不允许"让 reference 改指向不同对象"。 换一个想法, p.nameValue 所指的那个 string 对象该被修改,进而影响"持有 pointers 或 references 而且指向该 string" 的其他对象吗?也就是对象不被直接牵扯到赋值 操作内?编译器生成的 copyassignment 操作符究竟该怎么做呢? 面对这个难题, C++ 的响应是拒绝编译那一行赋值动作。如果你打算在一个"内 含 reference 成员"的 class 内支持赋值操作 (assignment) ,你必须自己定义 copy ass匈nment 操作符。面对"内含 const 成员" (如本例之 objectValue) 的 classes , 编译器的反应也一样。更改 const 成员是不合法的,所以编译器不知道如何在它自 己生成的赋值函数内面对它们。最后还有一种情况:如果某个 base classes 将 copy ass匈nment 操作符声明为 private ,编译器将拒绝为其 derived classes 生成一个 copy assignment 操作符。毕竟编译器为 derived classes 所生的 copy assignment 操作符想 象中可以处理 base class 成分(见条款 12) ,但它们当然无法调用 derived class 无权 调用的成员函数。编译器两手一摊,无能为力。 请记住 ·编译器可以暗自为 class 创建 default 构造函数、 copy 构造函数、 copyassignment 操 作符,以及析构函数。 条款 06: 若不想使用编译器自动生成的函数,就该 明确拒绝 Explicitly disallow the use of compiler-generated functions you do not want. 地产中介商卖的是房子,一个中介软件系统自然而然想必有个 class 用来描述待 售房屋: class HomeForSale { ... }; 每一位真正的地产中介商都会说,任何→笔资产都是天上地下独→无二,没有 两笔完全相像。因此我们也认为,为 HomeForSale 对象做一份副本有点没道理。你 怎么可以复制某些先天独一无二的东西呢?因此,你应该乐意看到 HomeForSale 的 对象拷贝动作以失败收场: HomeForSale hl; HomeForSale h2; HomeForSale h3(hl); hl = h2; II企图拷贝 hl 一不该通过编译 II企图拷贝 h2 也不该通过编译 Effective C++ 中文版,第三版38 2 构造/析构/赋值运算 啊呀,阻止这→类代码的编译并不是很直观。通常如果你不希望 class 支持某一 特定机能,只要不声明对应函数就是了。但这个策略对 copy 构造函数和 copy assignment 操作符却不起作用,因为条款 5 已经指出,如果你不声明它们,而某些 人尝试调用它们,编译器会为你声明它们。 这把你逼到了一个困境。如果你不声明 copy 构造函数或 copy assignment 操作符, 编译器可能为你产出一份,于是你的 class 支持 copying 。如果你声明它们,你的 class 还是支持 copyingo 但这里的目标却是要阻止 copying! 答案的关键是,所有编译器产出的函数都是 public 。为阻止这些函数被创建出来, 你得自行声明它们,但这里并没有什么需求使你必须将它们声明为 public 。因此你可 以将 copy 构造函数或 copyassignment 操作符声明为 private 。藉由明确声明一个成员 函数,你阻止了编译器暗自创建其专属版本:而令这些函数为 private ,使你得以 成功阻止人们调用它。 一般而言这个做法并不绝对安全,因为 member 函数和企 iend 函数还是可以调用 你的 private 函数。除非你够聪明,不去定义它们,那么如果某些人不慎调用任何一 个,会获得一个连接错误 (linkage error) 0 "将成员函数声明为 private 而且故意 不实现它们"这一伎俩是如此为大家接受,因而被用在 C++ ios位earn 程序库中阻止 copying 行为。是的,看看你手上的标准程序库实现码中的 ios_base , basic_ios 和 sentry。你会发现无论哪一个,其 copy 构造函数和 copy assignment 操作符都被声明 为 private 而且没有定义。 将这个伎俩施行于 HomeForSale 也很简单: class HomeForSale { public: private: HomeForSale(const HomeForSale&); HomeForSale& operator=(const HomeForSale&); II只有声明 或许你注意到了,我没写函数参数的名称。晤,参数名称并非必要,只不过大 家总是习惯写出来。这个函数毕竟不会被实现出来,也很少被使用,指定参数名称 又有何用? 有了上述 class 定义,当客户企图拷贝HomeForSale对象,编译器会阻挠他。如 果你不慎在member 函数或 friend 函数之内那么做,轮到连接器发出抱怨。 £1知ctive c++中文版,第三版2 条款 06: 若不想使用编译器自动生成的函数,就该明确拒绝 39 将连接期错误移至编译期是可能的(而且那是好事,毕竟愈早侦测出错误愈好) , 只要将 copy 构造函数和 copyassignment 操作符声明为 private 就可以办到,但不是 在 HomeForSale 自身,而是在一个专门为了阻止 copying 动作而设计的 base class 内。 这个 base class 非常简单: class Uncopyable { protected: II允许 derived 对象构造和析构 Uncopyable () {} -Uncopyable(} {} private: Uncopyable(const Uncopyable&}; II但阻止 copying Uncopyable& operator=(const Uncopyable&); 为求阻止 HomeForSale对象被拷贝,我们唯-需要傲的就是继承Uncopyable: class HomeForSale: private Uncopyable { Ilclass 不再声明 Ilcopy 构造函数或 IIcopy assign. 操作符 这行得通,因为只要任何人一一甚至是member 函数或 friend 函数一一尝试拷贝 HomeForSale对象,编译器便试着生成一个copy 构造函数和-个 copyass够nment操 作符,而正如条款 12 所说,这些函数的"编译器生成版"会尝试调用其base class 的对应兄弟,那些调用会被编译器拒绝,因为其 base class 的拷贝函数是 private 。 Uncopyable class 的实现和运用颇为微妙,包括不一定得以public 继承它(见条 款 32 和 39) ,以及 Uncopyable的析构函数不一定得是virtual (见条款7)等等。 Uncopyable不含数据,因此符合条款39 所描述的 empty base class optimization 资 格。但由于它总是扮演base class,因此使用这项技术可能导致多重继承(译注:因 为你往往还可能需要继承其他class) (多重继承见条款40) ,而多重继承有时会阻 止 empty base class optimization (再次见条款 39) 。通常你可以忽略这些微妙点, 只像上面那样使用 Uncopyable,因为它完全像"广告"所说的能够正确运作。也可 以使用 Boost (见条款 55) 提供的版本,那个 class 名为 noncopyable,是个还不错 的家伙,我只是认为其名称有点……昵……不太自然。 请记住 ·为驳回编译器自动(暗自〉提供的机能,可将相应的成员函数声明为private 并且 不予实现。使用像 Uncopyable 这样的 base class 也是一种做法。 Effective C++ 中文版,第三版40 2 构造/析构/赋值运算 条款 07: 为多态墓类声明 virtual 析构函数 Declare destructors virtual in polymorphic base classes. 有许多种做法可以记录时间,因此,设计一个TimeKeeper base class 和一些 derived classes 作为不同的计时方法,相当合情合理: class TimeKeeper { public: TimeKeeper () ; -TimeKeeper () ; ); class AtomicClock: public TimeKeeper { ); class WaterClock: public TimeKeeper { ); class WristWatch: public TimeKeeper { ); II原子钟 II水钟 II腕表 TimeKeeper* getTimeKeeper(); 许多客户只想在程序中使用时间,不想操心时间如何计算等细节,这时候我们 可以设计 factory (工厂)函数,返回指针指向一个计时对象。Factory 函数会"返回 一个 base class 指针,指向新生成之derived class 对象" : II 返回一个指针,指向一个 IITimeKeeper 派生类的动态分配对象 为遵守 factory 函数的规矩,被 getTimeKeeper ()返回的对象必须位于 heap 。因 此为了避免泄漏内存和其他资源,将 factory 函数返回的每一个对象适当地 delete 掉很重要: TimeKeeper* ptk = getTimeKeeper(); II从 TimeKeeper继承体系 II获得一个动态分配对象。 II运用它... delete ptk; II释放它,避免资源泄漏。 条款 13 说"倚赖客户执行delete 动作,基本上便带有某种错误倾向",条款 18 则谈到 factory 函数接口该如何修改以便预防常见之客户错误,但这些在此都是次 要的,因为此条款内我们要对付的是上述代码的一个更根本弱点:纵使客户把每一 件事都做对了,仍然没办法知道程序如何行动。 问题出在 getTimeKeeper 返回的指针指向一个 derived class 对象(例如 AtomicClock) ,而那个对象却经由一个base class 指针(例如一个TimeKeeper*指 针)被删除,而目前的base class (TimeKeeper)有个 non-virtual 析构函数。 £1知ctive C++ 中文版,第三版2 条款 07: 为多态基类声明 virtual 析构函数 41 这是一个引来灾难的秘诀,因为 C++ 明白指出,当 derived class 对象经由一个 base class 指针被删除,而该 base class 带着-个 non-virtual 析构函数,其结果未有定义一一 实际执行时通常发生的是对象的 derived 成分没被销毁。如果 getTimeKeeper 返回 指针指向一个 AtomicClock 对象,其内的 AtomicClock 成分(也就是声明于 AtomicClock class 内的成员变量)很可能没被销毁,而AtomicClock的析构函数也 未能执行起来。然而其base class 成分(也就是 TimeKeeper这一部分〉通常会被销 毁,于是造成一个诡异的"局部销毁"对象。这可是形成资源泄漏、败坏之数据结 构、在调试器上浪费许多时间的绝佳途径喔。 消除这个问题的做法很简单:给base class 一个 virtual 析构函数。此后删除derived class 对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class 成分 z class TimeKeeper { public: TimeKeeper( ); virtual -TimeKeeper(); }; TimeKeeper* ptk = getTimeKeeper(); delete ptk; II现在,行为正确。 像 TimeKeeper这样的 base classes 除了析构函数之外通常还有其他vi阳al 函数, 因为 virtual 函数的目的是允许derived class 的实现得以客制化(见条款34) 。例如 TimeKeeper就可能拥有一个virtual getCurrentTime,它在不同的derived classes 中 有不同的实现码。任何class 只要带有 virtual 函数都几乎确定应该也有一个virtual 析构函数。 如果 class 不含 virtual 函数,通常表示它并不意图被用做一个base class。当 class 不企图被当作base class,令其析构函数为virtual 往往是个馒主意。考虑一个用来表 示二维空间点坐标的class: class Point { public: Point(int xCoord, int yCoord); -Point (); private: int x, y; II一个二维空间点。Dpoint> Effective C++ 中文版,第二版42 2 构造/析构/赋值运算 如果 int 占用 32 bits ,那么 Point 对象可塞入一个 64-bit 缓存器中。更有甚者, 这样一个 Point 对象可被当做一个 "64-bit 量"传给以其他语言如 C 或 FORTRAN 撰写的函数。然而当 Point 的析构函数是 virtual ,形势起了变化。 欲实现出 vi阳 al 函数,对象必须携带某些信息,主要用来在运行期决定哪一个 virtual 函数该被调用。这份信息通常是由一个所谓 vptr (virtual table pointer) 指针指 出。 vptr 指向一个由函数指针构成的数组,称为 vtbl (virtual table) ;每一个带有 virtual 函数的 class 都有一个相应的 vtbl 。当对象调用某一virtual 函数,实际被调用的函数 取决于该对象的 vptr 所指的那个 vtbl一一编译器在其中寻找适当的函数指针。 virtual 函数的实现细节不重要。重要的是如果Point class 内含 vi阳al 函数,其 对象的体积会增加z 在 32-bit 计算机体系结构中将占用64 bits (为了存放两个 ints) 至 96 bits (两个 ints 加上 vptr) ;在 64-bit 计算机体系结构中可能占用 64~128 bits, 因为指针在这样的计算机结构中占 64 bits 。因此,为 Point 添加一个 vptr 会增加其 对象大小达 50 争 100 宅 !Point 对象不再能够塞入一个 64-bit 缓存器,而 C++ 的 Point 对象也不再和其他语言(如 C) 内的相同声明有着一样的结构(因为其他语言的对 应物并没有 vptr) ,因此也就不再可能把它传递至(或接受自)其他语言所写的函 数,除非你明确补偿 vpt卜一那属于实现细节,也因此不再具有移植性。 因此,无端地将所有 class臼的析构函数声明为 virtual ,就像从未声明它们为 virtual 一样,都是错误的。许多人的心得是:只有当 class 内含至少一个 virtual 函数, 才为它声明 virtual 析构函数。 即使 class 完全不带 virtual 函数,被" non-virtual 析构函数问题"给咬伤还是有 可能的。举个例子,标准 string 不含任何 virtual 函数,但有时候程序员会错误地把 它当做 base class: class SpecialString: public std::string { II 馒主意! std: :string有个 Ilnon-叫m皿析构函数 乍看似乎无害,但如果你在程序任意某处无意间将一个pointer-to-SpecialStrin £1知ctive C++中文版,第三版2 条款 07: 为多态基类声明 Vi阳剖析构函数 43 转换为一个 pointer-to-stri 呵,然后将转换所得的那个 string 指针 delete 掉,你立 刻被流放到"行为不明确"的恶地上: SpecialString* pss = new SpecialString("Impending Doom"); std: :stri口g* ps; ps = pss; IISpecialString* => std::string* delete ps; II未有定义!现实中*ps 的 SpecialString资源会泄漏, II 因为 SpecialString析构函数没被调用。 相同的分析适用于任何不带virtual 析构函数的 class,包括所有 STL 容器如 vector, list, set, trl: :unordered map (见条款 54) 等等。如果你曾经企图继承一 个标准容器或任何其他"带有 non-virtual 析构函数"的 class ,拒绝诱惑吧! (恨不 幸 C++ 没有提供类似 Java 的 final classes 或 C# 的 sealed classes 那样的"禁止 派生"机制。) 有时候令 class 带一个 pure vi阳 al 析构函数,可能颇为便利。还记得吗, pure virtual 函数导致 abstract (抽象) classes 一一一也就是不能被实体化(instantiated) 的 class 。 也就是说,你不能为那种类型创建对象。然而有时候你希望拥有抽象class ,但手上 没有任何 pure virtual 函数,怎么办?晤,由于抽象class 总是企图被当作一个base class 来用,而又由于 base class 应该有个 virtual 析构函数,并且由于pure virtual 函数会导 致抽象 class ,因此解法很简单:为你希望它成为抽象的那个class 声明一个 pure virtual 析构函数。下面是个例子: class AWOV ( public: virtual -AWOV( ) = 0; I IAWOV= "Abstract w/o Virtuals" II 声明 pure 由国 l 析构函数 这个 class 有一个 pure virtual 函数,所以它是个抽象 class ,又由于它有个 vi阳al 析构函数,所以你不需要担心析构函数的问题。然而这里有个窍门 s 你必须为这个 pure vi阳剖析构函数提供一份定义: AWOV: : -AWOV () () Ilpure virtual 析构函数的定义 析构函数的运作方式是,最深层派生(most derived) 的那个 class 其析构函数最 先被调用,然后是其每一个base class 的析构函数被调用。编译器会在AWOV 的 derived Effective C++ 中文版,第三版44 2 构造/析构/赋值运算 classes 的析构函数中创建一个对 -AWOV 的调用动作,所以你必须为这个函数提供一 份定义。如果不这样做,连接器会发出抱怨。 "给 base classes →个 virtual 析构函数",这个规则只适用于 polymorphic (带多 态性质的)base classes 身上。这种 base classes 的设计目的是为了用来"通过 base class 接口处理 derived class 对象" 0 TimeKeeper 就是一个 polymorphic base class ,因为我 们希望处理 AtomicClock 和 WaterClock 对象,纵使我们只有 TimeKeeper 指针指向 它们。 并非所有 base classes 的设计目的都是为了多态用途。例如标准 string 和 STL 容器都不被设计作为 base classes 使用,更别提多态了。某些 classes 的设计目的是作 为 base classes 使用,但不是为了多态用途。这样的 classes 如条款 6 的 Uncopyable 和标准程序库的 input iterator tag (条款 47) ,它们并非被设计用来"经由base class 接口处置 derived class 对象",因此它们不需要virtual 析构函数。 请记住 • polymorphic (带多态性质的) base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。 • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性 (polymorphically) ,就不该声明 virtual 析构函数。 条款 08: 到让异常逃离析构函数 Prevent exceptions 企om leaving destructors. C++ 并不禁止析构函数吐出异常,但它不鼓励你这样做。这是有理由的。考虑 以下代码: class Widget { public: -Widget( ){ void doSomething() std::vector v; Effective C++ 中文版,第三版 II 假设这个可能吐出一个异常 Ilv 在这里被自动销毁2 条款 08: 别让异常逃离析构函数 45 当 vector v 被销毁,它有责任销毁其内含的所有 Widgetso 假设 v 内含十个 Widgets ,而在析构第一个元素期间,有个异常被抛出。其他九个 Widgets 还是应该 被销毁(否则它们保存的任何资源都会发生泄漏) .因此 v 应该调用它们各个析构 函数。但假设在那些调用期间,第二个 Widget 析构函数又抛出异常。现在有两个同 时作用的异常,这对 C++ 而言太多了。在两个异常同时存在的情况下,程序若不是 结束执行就是导致不明确行为。本例中它会导致不明确的行为。使用标准程序库的 任何其他容器(如 list , se t)或 TRl 的任何容器(见条款 54) 或甚至 array. 也会 出现相同情况。容器或 array 并非遇上麻烦的必要条件,只要析构函数吐出异常,即 使并非使用容器或 arrays. 程序也可能过早结束或出现不明确行为。是的 .C++ 不 喜欢析构函数吐出异常! 这很容易理解,但如果你的析构函数必须执行一个动作,而该动作可能会在失 败时抛出异常,该怎么办?举个例子,假设你使用一个 class 负责数据库连接 z class DBConnection { public: static DBConnection create(); void close(); II这个函数返回 II DBConn∞tion 对象: II 为求简化暂略参数。 II 关闭联机:失败则抛出异常。 为确保客户不忘记在 DBConnection 对象身上调用 close (),一个合理的想法是 创建一个用来管理 DBConnection 资源的 class. 并在其析构函数中调用 close 。这一 类用于资源管理的 classes 在第 3 章有详细探讨,这儿只要考虑它们的析构函数长相 就够了: class DBConn { public: -DBCorm () db.close ( ); private: DBConnection db; II这个 class 用来管理 DBConnection对象 II确保数据库连接总是会被关闭 这便允许客户写出这样的代码: Ej知ctive C++中文版,第三版46 2 构造/析构/赋值运算 II 开启一个区块 ChIωk) 。 DBConn dbc(DBConnection::create()); II建立 DBConnection对象并 II 交给 DBConn对象以便管理。 II通过 DBConn 的接口 II 使用 DBConnection对象。 II在区块结束点, DBConn对象 II 被销毁,因而自动 II 为 DBConnection对象调用 close 只要调用 close 成功,一切都美好。但如果该调用导致异常,DBConn析构函数 会传播该异常,也就是允许它离开这个析构函数。那会造戚问题,因为那就是抛出 了难以驾驭的麻烦。 两个办法可以避免这一问题。 DBConn 的析构函数可以= ·如果 close 抛出异常就结束程序。通常通过调用abort 完成 2 DBConn::-DBConn( ) try { db.close (); } catch (...) { 制作运转记录,记下对 close 的调用失败; std: :abort ( ); 如果程序遭遇一个"于析构期间发生的错误"后无法继续执行,"强迫结束 程序"是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那会导致不明 确的行为)。也就是说调用abort 可以抢先制"不明确行为"于死地。 ·吞下因调用 close 而发生的异常z DBConn: : -DBCo口n( ) try { db.close (); } catch (...) { 制作运转记录,记 F对 close 的调用失败; 一般而言,将异常吞掉是个坏主意,因为它压制了"某些动作失败"的重要 信息!然而有时候吞下异常也比负担"草率结束程序"或"不明确行为带来的风 险"好。为了让这成为一个可行方案,程序必须能够继续可靠地执行,即使在遭 遇并忽略一个错误之后。 £1知ctive C++ 中文版,第三版2 条款 08: 别让异常逃离析构函数 47 这些办法都没什么吸引力。问题在于两者都无法对"导致 close 抛出异常"的 情况做出反应。 一个较佳策略是重新设计 DBConn 接口,使其客户有机会对可能出现的问题作出 反应。例如 DBConn 自己可以提供一个 close 函数,因而赋予客户一个机会得以处理 "因该操作而发生的异常" 0 DBConn 也可以追踪其所管理之 DBConnection 是否己 被关闭,并在答案为否的情况下由其析构函数关闭之。这可防止遗失数据库连接。 然而如果 DBConnection 析构函数调用 close 失败,我们又将返回"强迫结束程序" 或"吞下异常"的老路 z class DBConn { public: void close () db.close( ); closed = true; -DBConn () II 供客户使用的新函数 II关闭连接(如果客户不那么做的话) if (! closed) { try { db. close ( ); catch (...) { 制作运转记录,记下对 close 的调用失败; II 如果关闭动作失败, II 记录下来并结束程序 II 或吞下异常。 private: DBConnection db; bool closed; 把调用 close 的责任从 DBConn析构函数手上移到DBConn 客户手上(但 DBConn 析构函数仍内含一个"双保险"调用)可能会给你"肆无忌惮转移负担"的印象。 你甚至可能认为它违反条款18 所提忠告(让接口容易被正确使用)。实际上这两项 污名都不成立。如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理 该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常 Effective C++ 中文版,第三版48 2 构造/析构/赋值运算 就是危险,总会带来"过早结束程序"或"发生不明确行为"的风险。本例要说的 是,由客户自己调用 close 并不会对他们带来负担,而是给他们一个处理错误的机 会,否则他们没机会响应。如果他们不认为这个机会有用(或许他们坚信不会有错 误发生) ,可以忽略它,倚赖 DBConn 析构函数去调用 close 。如果真有错误发生一-一 如果 close 的确抛出异常一一而且 DBConn 吞下该异常或结束程序,客户没有立场抱 怨,毕竟他们曾有机会第一手处理问题,而他们选择了放弃。 请记住 ·析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析 构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。 ·如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提 供一个普通函数(而非在析构函数中)执行该操作。 条款 09: 绝不在构造和析构过程申调用 virtual 函数 Never call virtual functions during construction or destruction. 本条款开始前我要先阐述重点2 你不该在构造函数和析构函数期间调用virtual 函数,因为这样的调用不会带来你预想的结果,就算有你也不会高兴。如果你同时 也是一位 Java 或 C# 程序员,请更加注意本条款,因为这是C++ 与它们不相同的一 个地方。 假设你有个 class 继承体系,用来塑模股市交易如买进、卖出的订单等等。这样 的交易一定要经过审计,所以每当创建→个交易对象,在审计日志(audit log) 中也 需要创建一笔适当记录。下面是一个看起来颇为合理的做法: class Transaction ( public: Transaction( ); virtual void logTransaction() const = 0; Effective C++ 中文版,第三版 II 所有交易的 baseclass II 做出一份因类型不同而不同 II 的日志记录 Oogen町)2 条款 09: 绝不在构造和析构过程中调用 virtual 函数 49 Transaction: :Transaction () Ilbase class 构造函数之实现 logTransaction(); II最后动作是志记这笔交易 class BuyTransaction: public Transaction { llderivedclass public: virtual void logTransaction() canst; II志记Clog) 此型交易 } ; class SellTransaction: public Transaction { public: virtual void logTransaction() canst; 现在,当以下这行被执行,会发生什么事: BuyTransaction b; II derived class II 志记Cl og) 此型交易 无疑地会有一个 BuyT ransaction 构造函数被调用,但首先 Transaction 构造 函数一定会更早被调用;是的, derived class 对象内的 base class 成分会在 derived class 自身成分被构造之前先构造妥当。 Transaction 构造函数的最后一行调用 virtual 函 数 logTransactio口,这正是引发惊奇的起点。这时候被调用的 logTransaction 是 Transaction 内的版本,不是 BuyTransaction 内的版本一一即使目前即将建立的对 象类型是 BuyTransaction。是的, base class 构造期间 virtual 函数绝不会下降到 derived classes 阶层。取而代之的是,对象的作为就像隶属 base 类型一样。非正式的 说法或许比较传神:在 base class 构造期间, virtual 函数不是 virtual 函数。 这一似乎反直觉的行为有个好理由。由于 base class 构造函数的执行更早于 derived class 构造函数,当 base class 构造函数执行时 derived class 的成员变量尚未初 始化。如果此期间调用的 virtual 函数下降至 derived classes 阶层,要知道 derived class 的函数几乎必然取用 local 成员变量,而那些成员变量尚未初始化。这将是一张通往 不明确行为和彻夜调试大会串的直达车票。"要求使用对象内部尚未初始化的成分" 是危险的代名词,所以 C++ 不让你走这条路。 其实还有比上述理由更根本的原因:在derived class 对象的 base class 构造期间, Effective C++ 中文版,第三版50 2 构造/析构/赋值运算 对象的类型是 base class 而不是 derived classo 不只 virtual 函数会被编译器解析至 (resolve to) base class ,若使用运行期类型信息 (runtime type information,例如 dynamic_cast (见条款 27) 和 typeid) ,也会把对象视为base class 类型。本例之 中,当 Transaction 构造函数正执行起来打算初始化"BUyTransaction 对象内的 base class 成分"时,该对象的类型是 Transaction。那是每二个 C++ 次成分(见条 款1)的态度,而这样的对待是合理的2 这个对象内的 "BUyTransaction 专属成分" 尚未被初始化,所以面对它们,最安全的做法就是视它们不存在。对象在derived class 构造函数开始执行前不会成为一个 derived class 对象。 相同道理也适用于析构函数。一旦 derived class 析构函数开始执行,对象内的 derived class 成员变量便呈现未定义值,所以 C++ 视它们仿佛不再存在。进入 base class 析构函数后对象就成为一个 base class 对象,而 C++ 的任何部分包括 vi由泊l 函 数、 dynamic casts 等等也就那么看待它。 在上述示例中, Transaction 构造函数直接调用一个 virtual 函数,这很明显而 且容易看出违反本条款。由于它很容易被看出来,某些编译器会为此发出一个警告 信息(某些则否,见条款 53 对警告信息的讨论)。即使没有这样的警告,这个问题 在执行前也几乎肯定会变得显而易见,因为 logTransaction 函数在 Transaction 内是个 pure virtualo 除非它被定义(不太有希望,但是有可能,见条款34) 否则程 序无法连接,因为连接器找不到必要的 Transaction:: logTransaction 实现代码。 但是侦测"构造函数或析构函数运行期间是否调用 virtual 函数"并不总是这般 轻松。如果 Transaction 有多个构造函数,每个都需执行某些相同工作,那么避免 代码重复的一个优秀做法是把共同的初始化代码(其中包括对工ogTransaction 的调 用)放进-个初始化函数如 init 内: class Transaction { public: Transaction( ) { init ( ); } virtual void logTransaction() const = 0; private: void init () logTransaction(); El作c伽eC++中文版,第三版 II调用 non-悦血Illl... II这里调用叽rtual!2 条款 09: 绝不在构造和析构过程中调用 virtual 函数 S1 这段代码概念上和稍早版本相同,但它比较潜藏并且暗中为害,因为它通常不 会引发任何编译器和连接器的抱怨。此时由于 logTransaction 是 Transaction 内 的一个 pure virtual 函数,当 pure virtual 函数被调用,大多执行系统会中止程序(通 常会对此结果发出一个信息)。然而如果 logTransaction 是个正常的(也就是 impure) virtual 函数并在 Transaction 内带有一份实现代码,该版本就会被调用, 而程序也就会兴高采烈地继续向前行,留下你百思不解为什么建立-个derived class 对象时会调用错误版本的 logTransaction。唯一能够避免此问题的做法就是z 确定 你的构造函数和析构函数都没有(在对象被创建和被销毁期间)调用virtual 函数, 而它们调用的所有函数也都服从同→约束。 但你如何确保每次-有 Transaction 继承体系上的对象被创建,就会有适当版 本的 logTransaction 被调用呢?很显然,在 Transaction 构造函数(s) 内对着对象 调用 virtual 函数是-种错误做法。 其他方案可以解决这个问题。一种做法是在 class Transaction 内将 logTransaction 函数改为 non-virtual. 然后要求 derived class 构造函数传递必要信 息给 Transaction 构造函数,而后那个构造函数便可安全地调用 non-vir阳 al logTransaction。像这样: class Transaction { public: explicit Transaction(const std::string& logI口 fo) ; void logTransaction(const std::string& logInfo) const; II如今是个 Ilnon-叽践Ia1函数 Transact工on::Transaction(const std::string& logInfo) { logTransaction(logInfo); II如今是个 I/non-训rtual 调用 class BuyTransaction: public Transaction { public: BuyTransaction( parameters) : Transaction(createLogString( parameters )) II将 log 信息 { ... } II传给 base class 构造函数 private: static std::string createLogString( parameters ); Effective C++ 中文版,第三版52 2 构造/析构/赋值运算 换句话说由于你无法使用 virtual 函数从 base classes 向下调用,在构造期间,你 可以藉由"令 derived classes 将必要的构造信息向上传递至 base class 构造函数"替 换之而加以弥补。 请注意本例之 BuyTransaction 内的 private static 函数 createLogString 的运 用。是的,比起在成员初值列 (member initialization list) 内给予 base class 所需数据, 利用辅助函数创建一个值传给 base class 构造函数往往比较方便(也比较可读)。令 此函数为 static ,也就不可能意外指向"初期未成熟之 BuyTransaction 对象内尚未 初始化的成员变量"。这很重要,正是因为"那些成员变量处于未定义状态",所 以"在 base class 构造和析构期间调用的 virtual 函数不可下降至 derived classes" 。 请记住 ·在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class (比起当前执行构造函数和析构函数的那层)。 条款 10: 令 operator= 返回一个 reference to *this Have assignment operators return a reference to *this. 关于赋值,有趣的是你可以把它们写成连锁形式: int x, y, z; x = y = z = 15; II赋值连锁形式 同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为2 x = (y = (z = 15)); 这里 15 先被赋值给 Z ,然后其结果(更新后的z) 再被赋值给 y ,然后其结果(更 新后的 y) 再被赋值给 x 。 为了实现"连锁赋值",赋值操作符必须返回一个reference 指向操作符的左侧 实参。这是你为classes 实现赋值操作符时应该遵循的协议: class Widget { public: E1.知ctive C++中文版,第二版2 条款 11 :在 operaωF 中处理"自我赋值" Widget& operator=(const Widget& rhs) { return* th工 s; 53 II 返回类型是个 referer邸, II 指向当前对象。 II 返回左侧对象 这个协议不仅适用于以上的标准赋值形式,也适用于所杳赋值相关运算,例如 z class Widget { public: Widget& operator+=(const Widget& rhs) { return *this; Widget& operator=(工nt rhs) return *this; II这个协议适用于 II +=, -=, *=,等等。 II此函数也适用,即使 II 此一操作符的参数类型 II 不符协定。 注意,这只是个协议,并无强制性。如果不遵循它,代码一样可通过编译。然 而这份协议被所有内置类型和标准程序库提供的类型如string, vector, complex, trl::sharedytr 或即将提供的类型〈见条款 54) 共同遵守。因此除非你有一个标 新立异的好理由,不然还是随众吧。 请记住 ·令赋值( assignment) 操作符返回一个 reference to *this。 条款 11: 在 operator= 申处理"自我赋值" Handle assignment to selfin operator=. "自我赋值"发生在对象被赋值给自己时: class Widget { ... ); Widget w; w = w; II 赋值给自己 这看起来有点愚蠢,但它合法,所以不要认定客户绝不会那么做。此外赋值动 Effective C++ 中文版,第三版54 作并不总是那么可被一眼辨识出来,例如: 2 构造/析构/赋值运算 ali] = a[j]; II潜在的自我赋值 如果 i 和 j 有相同的值,这便是个自我赋值。再看: *px = *py; II潜在的自我赋值 如果 px 和 py 恰巧指向同一个东西,这也是自我赋值。这些并不明显的自我赋 值,是"别名" ( aliasing) 带来的结果:所谓"别名"就是"有一个以上的方法指 称(指涉)某对象"。一般而言如果某段代码操作 pointers 或 references 而它们被用 来"指向多个相同类型的对象",就需考虑这些对象是否为同一个。实际上两个对 象只要来自同一个继承体系,它们甚至不需声明为相同类型就可能造成"别名", 因为一个 base class 的 reference 或 pointer 可以指向一个 derived class 对象: class Base { ... }; class Derived: public Base { ... }; void doSomething(const Base& rb, II 血和女pd 有可能其实是同一对象 Derived* pd); 如果遵循条款 13 和条款 14 的忠告,你会运用对象来管理资源,而且你可以确 定所谓"资源管理对象"在copy发生时有正确的举措。这种情况下你的赋值操作符 或许是"自我赋值安全的" (self-assignment-safe) ,不需要额外操心。然而如果你 尝试自行管理资源(如果你打算写一个用于资源管理的class 就得这样做) ,可能会 掉进"在停止使用资源之前意外释放了它"的陷阱。假设你建立一个class 用来保存 一个指针指向一块动态分配的位图(bitmap) : class Bitmap { ... }; class Widget { private: Bitmap* pb; II指针,指向一个从heap 分配而得的对象 下面是 operator= 实现代码,表面上看起来合理,但自我赋值出现时并不安全 (它也不具备异常安全性,但我们稍后才讨论这个主题〉。 Widget& Widget::operator=(const Widget& rhs) delete pb; pb = new Bitmap(*rhs.pb); return *this; El知ctive C++中文版,第三版 II 一份不安全的operator= 实现版本 II停止使用当前的bitmap, II使用 rhs's bitmap 的副本(复件)。 /1见条款 10 ,2 条款 11: 在 operator=中处理"自我赋值" 55 这里的自我赋值问题是, operator= 函数内的 *this (赋值的目的端)和 rhs 有可能是同一个对象。果真如此 delete 就不只是销毁当前对象的 bitmap ,它也销毁 rhs 的 bitmap 。在函数末尾, Widget-- 它原本不该被自我赋值动作改变的一一-发 现自己持有一个指针指向一个己被删除的对象! 欲阻止这种错误,传统做法是藉由 operator= 最前面的一个"证同测试 (identity test) "达到"自我赋值"的检验目的: Widget& Widget::operator=(const Widget& rhs) { if (this == &rhs) return 气his; I I 证同测试 (identity 阳t) : II 如果是自我赋值,就不做任何事。 delete pb; pb = new Bitmap(*rhs.pb); return *this; 这样做行得通。稍早我曾经提过,前一版operator= 不仅不具备"自我赋值安 全性",也不具备"异常安全性",这个新版本仍然存在异常方面的麻烦。更明确 地说,如果 "new Bitmap" 导致异常(不论是因为分配时内存不足或因为Bitrr回p 的 copy构造函数抛出异常) , Widget 最终会持有一个指针指向→块被删除的 Bitmap 。 这样的指针有害。你无法安全地删除它们,甚至无法安全地读取它们。唯一能对它 们做的安全事情是付出许多调试能量找出错误的起源。 令人高兴的是,让 operator= 具备"异常安全性"往往自动获得"自我赋值安 全"的回报。因此愈来愈多人对"自我赋值"的处理态度是倾向不去管它,把焦点 放在实现"异常安全性" (exception safety) 上。条款 29 深度探讨了异常安全性, 本条款只要你注意"许多时候一群精心安排的语句就可以导出异常安全(以及自我 赋值安全)的代码",这就够了。例如以下代码,我们只需注意在复制pb 所指东西 之前别删除 pb: widget& Widget::operator=(const Widget& rhs) Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this; II记住原先的pb II令 pb 指向女pb 的一个复件(副本) II删除原先的pb Effective C++ 中文版,第三版56 2 构造/析构/赋值运算 现在,如果 "newBitmap" 抛出异常, pb (及其栖身的那个Widget) 保持原状。 即使没有证同测试(identity test) ,这段代码还是能够处理自我赋值,因为我们对原 bitmap 做了一份复件、删除原bitmap、然后指向新制造的那个复件。它或许不是处 理"自我赋值"的最高效办法,但它行得通。 如果你很关心效率,可以把"证同测试" plnv(createlnvestment( )}; II调用 factory 函数 1/→如以往地使用plnv II经由 auto ptr 的析构函数自动删除plnv 这个简单的例子示范"以对象管理资源"的两个关键想法: ·获得资源后立刻放进管理对象(managing object) 内。以上代码中 createlnvestment返回的资源被当做其管理者autoytr 的初值。实际上"以 对象管理资源"的观念常被称为"资源取得时机便是初始化时机" (Resource Acquisition Is Initialization; RAil) ,因为我们几乎总是在获得一笔资源后于同一 语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值(而非初始化) 某个管理对象,但不论哪一种做法,每→笔资源都在获得的同时立刻被放进管 理对象中。 ·管理对象 (managingobject) 运用析构函数确保资源被释放。不论控制流如何离 开区块,→旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动 调用,于是资源被释放。如果资源释放动作可能导致抛出异常,事情变得有点 棘手,但条款8 己经能够解决这个问题,所以这里我们也就不多操心了。 由于 auto ptr 被销毁时会自动删除它所指之物,所以一定要注意别让多个 autoytr 同时指向同一对象。如果真是那样,对象会被删除→次以上,而那会使 你的程序搭上驶向"未定义行为"的快速列车上。为了预防这个问题,auto_ptrs 有一个不寻常的性质:若通过copy构造函数或 copy assignment操作符复制它们, 它们会变成null,而复制所得的指针将取得资源的唯一拥有权! El知ctive C++中文版,第三版64 3 资源管理 std::auto -p tr plnvl(createlnvestment( )); Ilplnvl 指向 II createlnvestment返回物. std::auto-ptr plnv2(plnvl); II现在 plnv2 指向对象, II plnvl 被设为 null. plnvl = plnv2; II现在目的1 指向对象, II plnv2 被设为 null. 这一诡异的复制行为,复加上其底层条件: "受 auto ptrs 管理的资源必须绝 对没有一个以上的 auto-ptr 同时指向它",意味 auto_ptrs 并非管理动态分配资 源的神兵利器。举个例子, STL 容器要求其元素发挥"正常的"复制行为,因此这 些容器容不得 auto ptro auto_ptr的替代方案是"引用计数型智慧指针"(r,价renee-countingsmartpointer; RCSP) 。所谓 RCSP 也是个智能指针,持续追踪共有多少对象指向某笔资源,并 在无人指向它时自动删除该资源。 RCSPs 提供的行为类似垃圾回收(garbage collection) ,不同的是 RCSPs 无法打破环状引用 (cycles of references,例如两个 其实已经没被使用的对象彼此互指,因而好像还处在"被使用"状态)。 TRl 的 trl::shared_ptr (见条款 54) 就是个 RCSP,所以你可以这么写f : void f( ) std::trl::shared ptr plnv(createlnvestment( )); II调用 factory 函数. II使用 plnv 一如以往 II经由 shared-ptr 析构函数自动删除plnv 这段代码看起来几乎和使用auto_ptr 的那个版本相同,但shared_ptrs 的复 制行为正常多了: vo工d f( ) std::trl::shared ptr plnvl(createlnvestment( )); Ilplnvl 指向 Ilcreatelnvestment返回物­ std::trl::shared ptr plnv2(plnvl); Ilplnvl 和 plnv2 指向同一个对象 plnvl = plnv2; II 同上,无任何改变. Ilplnvl 和 plnv2 被销毁, II它们所指的对象也就被自动销毁 Et如ctive C++中文版,第三版3 条款 13: 以对象管理资源 65 由于 trl: : sharedytrs 的复制行为"一如预期",它们可被用于 STL 容器以 及其他 "autoytr 之非正统复制行为并不适用"的语境上。 尽管如此,可别误会了,本条款并不专门针对 auto_ptr, trl::shared_ptr 或 任何其他智能指针,而只是强调"以对象管理资源"的重要性, auto_ptr 和 trl::sharedytr 只不过是实际例子。如果想知道 trl:shared_ptr 的更多信息, 请看条款 14 , 18 和 540 auto_ptr 和 trl::sharedytr 两者都在其析构函数内做 delete 而不是 delete[] 动作(条款 16 对两者的不同有些描述)。那意味在动态分配而得的 aπay 身上使用 auto_ptr 或 trl :: shared_ptr 是个馒主意。尽管如此,可叹的是,那么 做仍能通过编译z std::auto_ptr aps(new std::string[10]); std::trl::shared ptr spi(new int[1024); II馒主意!会用上错误的 II delete 形式。 II相同问题。 你或许会惊讶地发现,并没有特别针对 "C++ 动态分配数组"而设计的类似 autoytr 或 trl::sharedytr 那样的东西,甚至 TRl 中也没有。那是因为 vector 和 string 几乎总是可以取代动态分配而得的数组。如果你还是认为拥有针对数组 而设计、类似 autoytr 和 trl::shared_ptr 那样的 classes 较好,看看 Boost 吧(见 条款 55 )。在那儿你会很高兴地发现 boost::scoped array 和 boost: : shared array classes,它们都提供你要的行为。 本条款也建议,如果你打算于工释放资源(例如使用delete 而非使用一个资 源管理类; resource-managing class) ,容易发生某些错误。罐装式的资源管理类如 autoytr 和 trl: : shared_ptr 往往比较能够轻松遵循本条款忠告,但有时候你所 使用的资源是目前这些预制式classes 无法妥善管理的。既然如此就需要精巧制作你 自己的资源管理类。那并不是非常困难,但的确涉及若干你需要考虑的细节。那些 考虑形成了条款14 和条款 15 的标题。 Effective C++ 中文版,第三版66 3 资源管理 作为最后批注,我必须指出, createlnvestment 返回的"未加工指针" (raw pointer) 简直是对资源泄漏的一个死亡邀约,因为调用者极易在这个指针身上忘记 调用 deleteo (即使他们使用 autoytr或 trl: : sharedytr 来执行 delete ,他 们首先必须记得将 create 工 nvestment 的返回值存储于智能指针对象内。)为与此 问题搏斗,首先需要对 createlnvestment 进行接口修改,那是条款 18 面对的事。 请记住 ·为防止资源泄漏,请使用 RAIl对象,它们在构造函数中获得资源并在析构函数 中释放资源。 ·两个常被使用的RAil classes 分别是 trl: : sharedytr 和 auto ptr。前者通常 是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被 复制物)指向 null 。 条款 14: 在资源管理类申小Iω coping 行为 τ'hink carefully about copying behavior in resource-managing classes. 条款 13 导入这样的观念:"资源取得时机便是初始化时机"(ResourceAcquisition Is Initialization; RAil) ,井以此作为"资源管理类"的脊柱,也描述了autoytr 和 trl: : shared.:.-ptr 如何将这个观念表现在 heap-based 资源上。然而并非所有资 源都是 heap-based ,对那种资源而言,像 autoytr 和 trl: : sharedytr 这样的智 能指针往往不适合作为资源掌管者 (resource handlers) 。既然如此,有可能偶而你 会发现,你需要建立自己的资源管理类。 例如,假设我们使用 C API 函数处理类型为 Mutex 的互斥器对象 (mutex objects) ,共有 lock和 unlock两函数可用: void lock(Mutex* pm); II锁定 pm 所指的互斥器. void unlock(阳tex* pm); II将互斥器解除锁定. 为确保绝不会忘记将一个被锁住的Mutex 解锁,你可能会希望建立一个class 用来管理机锁。这样的class 的基本结构由RAIl守则支配,也就是"资源在构造期 间获得,在析构期间释放": class Lock { public: explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); } Effective C++ 中文版,第三版 II 获得资源3 条款 14: 在资源管理类中小心 coping 行为 67 ~Lock() { unlock(mutexPtr); } II释放资源 prl飞Tate: Mutex *mutexPtr; 客户对 Lock 的用法符合RAIl方式: Mutex m; II 定义你需要的互斥器 I II 建立一个区块用来定义 critical section. Lock ml(&m); II锁定互斥器. II 执行 critical section 内的操作. II 在区块最末尾,自动解除互斥器锁'走. 这很好,但如果 Lock 对象被复制,会发生什么事? Lock mll (&m) ; Lock m12 (mll) ; II锁定 m II 将 mll 复制到 m12 身上。这会发生什么事? 这是某个一般化问题的特定例子。那个一般化问题是每一位 RAIl class 作者一 定需要面对的: "当一个 RAIl对象被复制,会发生什么事? "大多数时候你会选 择以下两种可能: ·禁止复制。许多时候允许 RAIl 对象被复制并不合理。对一个像 Lock 这样的 class 这是有可能的,因为很少能够合理拥有"同步化基础器物" ( synchronization primitives) 的复件(副本)。如果复制动作对 RAIl class 并不合理,你便应该禁 止之。条款 6 告诉你怎么做z 将 copying 操作声明为 priv创e 。对 Lock 而言看起 来是这样: class Lock: private Uncopyable { public: II禁止复制。见条款6 。 II如前 ·对底层资源祭出"雪|用计数法"(reference-count)。有时候我们希望保有资源, 直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAIl对象时,应 该将资源的"被引用数"递增。trl::sharedytr便是如此。 通常只要内含一个 trl::sharedytr成员变量. RAIl classes 便可实现出 reference-∞unting copying 行为。如果前述的 Lock 打算使用 reference counting. 它可以改变 mutexPtr 的类型,将它从 Mutex* 改为 trl: :shared_ptr~知tex>。 然而很不幸trl ::sharedytr 的缺省行为是"当引用次数为0 时删除其所指物", 那不是我们所耍的行为。当我们用t一个 Mutex. 我们想要做的释放动作是解 El知ctive C++中文版,第三版68 3 资源管理 除锁定而非删除。 幸运的是 trl::shared ......p tr 允许指定所谓的"删除器" (deleted ,那是 一个函数或函数对象 (function object) ,当引用次数为0 时便被调用(此机能 并不存在于 auto_ptr-一一它总是将其指针删除)。删除器对trl::shared......p tr 构造函数而言是可有可无的第二参数,所以代码看起来像这样: class Lock { public: explicit Lock(Mutex* pm) II 以某个 Mutex 初始化 shared ......p tr : mutexPtr(pm, unlock) II 并以 unlock 函数为删除器. lock(mutexPtr.get()); II条款 15 谈到 "get" private: std::trl::shared......ptr mutexPtr; II使用 shared_ptr II替换 rawpoin阳 请注意,本例的 Lock class 不再声明析构函数。因为没有必要。条款5 说 过, class 析构函数(无论是编译器生成的,或用户自定的)会自动调用其non-static 成员变量(本例为 mutexPtr) 的析构函数。而 mutexptr 的析构函数会在互斥 器的引用次数为0 时自动调用 trl: : shared......p tr 的删除器(本例为 unlock) 。 (当你阅读这个 class 的原始码,或许会感谢其中有一条注释指出 z 你并没有忘 记析构,你只是倚赖了编译器生成的缺省行为。) ·复制底部资源。有时候,只要你喜欢,可以针对→份资源拥有其任意数量的复 件(副本)。而你需要"资源管理类"的唯一理由是,当你不再需要某个复件 时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包覆的 资源。也就是说,复制资源管理对象时,进行的是"深度拷贝"。 某些标准字符串类型是由"指向 heap 内存"之指针构成明白内存被用来存 放字符串的组成字符)。这种字符串对象内含一个指针指向一块 heap 内存。当 这样一个字符串对象被复制,不论指针或其所指内存都会被制作出一个复件。 这样的字符串展现深度复制 (deep copying) 行为。 ·转移底部资源的拥有权。某些罕见场合下你可能希望确保永远只有一个 RAil 对 象指向一个未加工资源 (raw resource) ,即使 RAIl 对象被复制依然如此。此时 资源的拥有权会从蓝皇型生扭转移到旦盔垫。一如条款13 所述,这是 aut0......ptr 奉行的复制意义。 E1知ctive C++中文版,第三版3 条款 15: 在资源管理类中提供对原始资源的访问 69 Coping 函数(包括 copy 构造函数和 copy assignment 操作符)有可能被编译器 自动创建出来,因此除非编译器所生版本做了你想要做的事(条款 5 提过其缺省行 为) ,否则你得自己编写它们。某些情况下你或许也想支持这些函数的一般版本, 这样的版本描述于条款的。 请记住 ·复制 RAIl对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAIl 对象的 copying 行为。 ·普遍而常见的 RAIl class copying 行为是:抑制 copying 、施行引用计数法 (reference counting) 。不过其他行为也都可能被实现。 条款 15: 在资源管理类申提供对原始资源的访问 Provide access to raw resources in resource-managing classes. 资源管理类 (resource-managing classes) 很棒。它们是你对抗资源泄漏的堡垒。 排除此等泄漏是良好设计系统的根本性质。在一个完美世界中你将倚赖这样的 classes 来处理和资源之间的所有互动,而不是站污双手直接处理原始资源(raw resources) 。但这个世界并不完美。许多APls 直接指涉资源,所以除非你发誓(这 其实是一种少有实际价值的举动)永不录用这样的APls ,否则只得绕过资源管理对 象 (resource-managing objects) 直接访问原始资源 (raw resources) 。 举个例子,条款 13 导入一个观念 z 使用智能指针如 autoytr 或 trl: :sharedytr保存 factory 函数如 createInvestme std::trl::shared ptrpInv(createInvestment()); II见条款 13 假设你希望以某个函数处理Investment对象,像这样: int daysHeld(const Investment* pi); II返回投资天数 Effective C++ 中文版,第三版70 你想要这么调用它: int days = daysHeld(pI口v) ; 却通不过编译,因为 d也ay归sHe恒eld 需要的是 In川ve臼str且肥I 型为 trl:口:s由ha缸re创dytr 和 operator吟,它们允许隐 式转换至底部原始指针 z class Investment { public: bool isTaxFree() const; Investment* createInvestment(); std::trl::sharedytr pil(createInvestment( »; bool taxablel = ! (pil->isTaxFree(»; II investment 继承体系的根类 Ilfactory 函数 II令 trl: : shared ptr II 管理一笔资源。 II经由 operator-> 访问资源。 std::autoytr pi2(createInvestment(»; II令 autoytr II管理一笔资源。 bool taxable2 = ! ((平 i2) .isTaxFree(»; II经由 operator* 访问资源。 由于有时候还是必须取得RAil 对象内的原始资源,某些RAIl class 设计者于是 联想到"将油脂涂在滑轨上",做法是提供一个隐式转换函数。考虑下面这个用于 字体的 RAIl class (对 CAPI 而言字体是一种原生数据结构): FontHandle getFont(); El知ctive C++中文版,第三版 II这是个CAPI。为求简化暂略参数。3 条款 15: 在资源管理类中提供对原始资源的访问 71 void releaseFont(FontHandle fh); class Font { public: explicit Font(FontHandle fh) : f(fh) {} -Font( ){ releaseFont(f ); } private: FontHandle f; II来自同一组CAPl I IRATI class II 获得资源; II 采用 pass-by-value , II 因为 CAPI 这样傲。 II释放资源 II 原始 (raw) 字体资源 假设有大量与字体相关的 CAPI ,它们处理的是 FontHandles ,那么"将 Font 对象转换为 FontHandle" 会是一种很频繁的需求。 Font class 可为此提供一个显式 转换函数,像 get 那样: class Font { public: FontHandle get() const { return f;} II显式转换函数 不幸的是这使得客户每当想要使用API 时就必须调用 get: void changeFontSize(FontHandle f , int newSize); Font f(getFont()); int newFontSize; IICAPI changeFontSize(f.get() , newFontSize); II 明白地将 Font 转换为 FontHandle 某些程序员可能会认为,如此这般地到处要求显式转换,足以使人们倒尽胃口, 不再愿意使用这个class,从而增加了泄漏字体的可能性,而Font class 的主要设计 目的就是为了防止资源(字体)泄漏。 另一个办法是令Font 提供隐式转换函数,转型为FontHandle: class Font { public: operator FontHandle() const { return f; } 这使得客户调用 CAPI 时比较轻松且自然 z II 隐式转换函数 Effective C++ 中文版,第三版72 Font f(getFont(»; int newFontSize; ch缸1geFontSize(f, newFontSize); 3 资源管理 II将 Font 隐式转换为 FontHandle FontHandle f2 = fl; 但是这个隐式转换会增加错误发生机会。例如客户可能会在需要Font 时意外 创建一个 FontHandle: Font fl(getFont( »); II喔欧!原意是要拷贝一个Font 对象, II却反而将 fl 隐式转换为其底部的FontHandle II然后才复制它。 以上程序有个FontHandle由 Font 对象 fl 管理,但那个 FontHandle也可通过 直接使用 f2 取得。那几乎不会有好下场。例如当fl 被销毁,字体被释放,而f2 因此成为"虚吊的" (dangle) 。 是否该提供一个显式转换函数(例如get 成员函数)将RAIl class 转换为其底 部资源,或是应该提供隐式转换,答案主要取决于RAIl class 被设计执行的特定工 作,以及它被使用的情况。最佳设计很可能是坚持条款18 的忠告: "让接口容易 被正确使用,不易被误用"。通常显式转换函数如get 是比较受欢迎的路子,因为 它将"非故意之类型转换"的可能性最小化了。然而有时候,隐式类型转换所带来 的"自然用法"也会引发天秤倾斜。 你的内心也可能认为, RAIl class 内的那个返回原始资源的函数,与"封装" 发生矛盾。那是真的,但一般而言它谈不上是什么设计灾难。RAIl classes 并不是 为了封装某物而存在:它们的存在是为了确保一个特殊行为一一资源释放一一会发 生。如果一定要,当然也可以在这基本功能之上再加一层资源封装,但那并非必要。 此外也有某些RAIl classes 结合十分松散的底层资源封装,藉以获得真正的封装实 现。例如 trl :: shared_ptr 将它的所有引用计数机构封装了起来,但还是让外界很 容易访问其所内含的原始指针。就像多数设计良好的classes 一样,它隐藏了客户不 需要看的部分,但备妥客户需要的所有东西。 El知ctive C++中文版,第三版3 条款 16: 成对使用 new 和 delete 时要采取相同形式 73 请记住 • APls 往往要求访问原始资源( raw resources) ,所以每一个 RAIl class 应该提供 →个"取得其所管理之资源"的办法。 ·对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全, 但隐式转换对客户比较方便。 条款 16: 成对使用 new 和 delete 时要采取相同形式 Use the same form in corresponding uses ofnew and delete. 以下动作有什么错? std::string* stringArray = new std::string[lOO}; delete stringArray; 每件事看起来都井然有序。使用了new,也搭配了对应的 delete。但还是有 某样东西完全错误:你的程序行为不明确(未有定义)。最低限度.stringArray 所含的 100 个 string 对象中的 99 个不太可能被适当删除,因为它们的析构函数很 可能没被调用。 当你使用 new (也就是通过口ew 动态生成一个对象) ,有两件事发生。第一, 内存被分配出来(通过名为operatornew 的函数,见条款49 和条款 5 1)。第二, 针对此内存会有一个(或更多)构造函数被调用。当你使用delete. 也有两件事 发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过 名为 operatordelete 的函数,见条款51) 0 delete 的最大问题在于 z 即将被删 除的内存之内究竟存有多少对象?这个问题的答案决定了有多少个析构函数必须 被调用起来。 实际上这个问题可以更简单些:即将被删除的那个指针,所指的是单一对象或 对象数组?这是个必不可缺的问题,因为单一对象的内存布局一般而言不同于数组 的内存布局。更明确地说,数组所用的内存通常还包括"数组大小"的记录,以便 delete 知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。你可以把 两种不同的内存布局想象如下,其中 n 是数组大小: El作ctive C++ 中文版F 第三版74 3 资源管理 +对象 I Object I 对象数组 I n I a阳 1 0加t I Object I 当然啦,这只是个例子。编译器不需非得这么实现不可,虽然很多编译器的确 是这样做的。 当你对着一个指针使用 delete ,唯一能够让 delete 知道内存中是否存在一个 "数组大小记录"的办法就是:由你来告诉它。如果你使用 delete 时加上中括号 (方括号) , delete 便认定指针指向一个数组,否则它便认定指针指向单一对象 z std: :stri口g* stringPtrl = new std::string; std::string* stringPtr2 = new std::string[lOO]; delete stringptrl; delete [] stringPtr2; II删除一个对象 II删除一个由对象组成的数组 如果你对 stringPtrl 使用 "delete []"形式,会发生什么事?结果未有定 义,但不太可能让人愉快。假设内存布局如上,delete会读取若干内存并将它解释 为"数组大小",然后开始多次调用析构函数,浑然不知它所处理的那块内存不但 不是个数组,也或许并未持有它正忙着销毁的那种类型的对象。 如果你没有对 stringPtr2使用 "delete []"形式,又会发生什么事呢?晤, 其结果亦未有定义,但你可以猜想可能导致太少的析构函数被调用。犹有进者,这 对内置类型如 int 者亦未有定义(甚至有害),即使这类类型并没有析构函数。 游戏规则很简单:如果你调用new 时使用[] .你必须在对应调用delete 时也 使用[]。如果你调用口ew 时没有使用[].那么也不该在对应调用delete 时使用[]。 当你撰写的 class 含有一个指针指向动态分配内存,并提供多个构造函数时, 上述规则尤其重要,因为这种情况下你必须小心地在所有构造函数中使用相同形式 的 new 将指针成员初始化。如果没这样做,又如何知道该在析构函数中使用什么形 式的 delete 呢? 这个规则对于喜欢使用typedef 的人也很重要,因为它意味typedef 的作者 必须说清楚,当程序员以new 创建该种 typedef 类型对象时,该以哪一种delete 形式删除之。考虑下面这个typedef: Effective C++ 中文版,第三版3 条款 17: 以独立语句将 newed 对象置入智能指针 75 typedef std::string AddressLines[4]; II每个人的地址有4 行, II每行是一个 string 由于 AddressLines是个数组,如果这样使用new: std: :strin俨 pal = new AddressLines; II注意. "new AddressLines" 返回 II 一个 string 食,就像 II"new string[4]" 一样。 那就必须匹配"数组形式"的 delete: delete pal; delete [] pal; II行为未有定义! II很好。 为避免诸如此类的错误,最好尽量不要对数组形式做typedefs 动作。这很容 易达成,因为C十+标准程序库(条款54) 含有 stri呵. vector 等 templates. 可将 数组的需求降至几乎为零。例如你可以将本例的AddressLines定义为"由 stringS 组成的一个 vector" .也就是其类型为飞rector。 请记住 ·如果你在 new 表达式中使用[].必须在相应的delete表达式中也使用[]。如果 你在 new 表达式中不使用[].一定不要在相应的delete表达式中使用(]。 条款 17: 以独立语句将newed 对象置λ智能指针 Store newed objects in smart pointers in standalone statements. 假设我们有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配 所得的 Widget 上进行某些带有优先权的处理: int priority () ; void processWidget(std::trl::shared_ptr pw, int priority); 由于谨记"以对象管理资源" (条款 13 )的智慧铭言. processWidget决定对 其动态分配得来的Widget运用智能指针(这里采用trl::shared-ptr) 。 现在考虑调用 processWidget: processWidget(new Widget, priority()); Effective C++ 中文版,第三版76 3 资源管理 等等,不要考虑这个调用形式。它不能通过编译。 trl: : shared_ptr 构造函数 需要一个原始指针 (raw pointer) ,但该构造函数是个explicit构造函数,无法进 行隐式转换,将得自"口ewWidget"的原始指针转换为processWidget所要求的 trl: : sharedytr 。如果写成这样就可以通过编译: processWidget(std::trl::shared ptr (new Widget) , priority()); 令人惊讶的是,虽然我们在此使用"对象管理式资源"( object-managing resources) ,上述调用却可能泄漏资源。稍后我再详加解释。 编译器产出一个processWidget调用码之前,必须首先核算即将被传递的各个 实参。上述第二实参只是一个单纯的对priority 函数的调用,但第一实参 std::trl:: sharedytr (new Widget) 由两部分组成z ·执行 "new Widget" 表达式 ·调用 trl: : shared ptr 构造函数 于是在调用 processWidget之前,编译器必须创建代码,做以下三件事z ·调用 priority ·执行 "new Widget" ·调用 trl: : sharedytr 构造函数 C++ 编译器以什么样的次序完成这些事情呢?弹性很大。这和其他语言如 Java 和 C# 不同,那两种语言总是以特定次序完成函数参数的核算。可以确定的是 "new Widget" 一定执行于 trl: : sharedytr 构造函数被调用之前,因为这个表达式的结 果还要被传递作为trl: : sharedytr 构造函数的一个实参,但对priority的调用 则可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行它(说不定可 因此生成更高效的代码,谁知道!) ,最终获得这样的操作序列 z 1.执行 "new Widget" 2. 调用 priority 3. 调用 trl: :sharedytr构造函数 现在请你想想,万一对priority 的调用导致异常,会发生什么事?在此情况 下 "new Widget" 返回的指针将会遗失,因为它尚未被置入trl: : sharedytr 内, 后者是我们期盼用来防卫资源泄漏的武器。是的,在对processWidget的调用过程 中可能引发资源泄漏,因为在"资源被创建(经由"new widget") "和"资源被 Ej知ctive C++中文版,第三版3 条款 17: 以独立语句将 newed 对象置入智能指针 转换为资源管理对象"两个时间点之间有可能发生异常干扰。 77 避免这类问题的办法很简单:使用分离语旬,分别写出(1)创建 Widge , (2) 将 它置入一个智能指针内,然后再把那个智能指针传给 processWidget: std::trl::shared ptr pw(new Widget); II在单独语句内以 II 智能指针存储 II newed 所得对象。 processWidget(pw, priority()); II这个调用动作绝不至于造成泄漏。 以上之所以行得通,因为编译器对于"跨越语旬的各项操作"没有重新排列的 自由(只有在语句内它才拥有那个自由度)。在上述修订后的代码内,"newWidget" 表达式以及"对 trl :: sharedytr 构造函数的调用"这两个动作,和"对priority 的调用"是分隔开来的,位于不同语句内,所以编译器不得在它们之间任意选择执 行次序。 请记住 ·以独立语句将 newed 对象存储于(置入)智能指针内。如果不这样做,一旦异 常被抛出,有可能导致难以察觉的资源泄漏。 Effective C++ 中文版,第三版78 4 设计与声明 4 设计与声明 Designs and Declarations 所谓软件设计,是"令软件做出你希望它做的事情"的步骤和做法,通常以颇为 一般性的构想开始,最终演变成十足的细节,以允许特殊接口(interfaces) 的开发。 这些接口而后必须转换为C++ 声明式。本章中我将对良好C++ 接口的设计和声明发 起攻势。我以或许最重要、适合任何接口设计的一个准则作为开端:"让接口容易被 正确使用,不容易被误用"。这个准则设立了一个舞台,让其他更专精的准则对付一 大范围的题目,包括正确性、高效性、封装性、维护性、延展性,以及协议的一致性。 以下准备的材料并不覆盖你需要知道的优良接口设计的每一件事,但它强调某些 最重要的考虑,对某些最频繁出现的错误提出警告,为class 、 function 和 template 设计 者经常遭遇的问题提供解答。 条款 18: 让接口容易被E确使用,不易被误用 Make interfaces e出Y to use correctly and hard to use incorrectly. C++ 在接口之海漂浮。 function 接口、 class 接口、 template 接口......每一种接口都 是客户与你的代码互动的手段。假设你面对的是一群"讲道理的人",那些客户企图 把事情做好。他们想要正确使用你的接口。这种情况下如果他们对任何其中→个接口 的用法不正确,你至少也得负一部分责任。理想上,如果客户企图使用某个接口而却 没有获得他所预期的行为,这个代码不该通过编译:如果代码通过了编译,它的作为 就该是客户所想要的。 欲开发一个"容易被正确使用,不容易被误用"的接口,首先必须考虑客户可能 做出什么样的错误。假设你为一个用来表现日期的class 设计构造函数: Ej知ctive C++ 中文版,第二版4 条款 18: 让接口容易被正确使用,不易被误用 class Date { public: Date(int month, int day, int year); 79 乍见之下这个接口通情达理(至少在美国如此),但它的客户很容易犯下至少两 个错误。第一,他们也许会以错误的次序传递参数: Date d(30, 3, 1995); II喔欧!应该是 "3 , 30" 而不是 "30, 3" 第二,他们可能传递一个无效的月份或天数: Date d(2, 30, 1995); I I 喔欧!应该是'飞 30" 而不是吃, 30" (上个例子也许看起来很蠢,但别忘了,键盘上的 2 就在 3 旁边。打岔一个键的 情况并不是太罕见。) 许多客户端错误可以因为导入新类型而获得预防。真的,在防范"不值得拥有的 代码"上,类型系统( type system) 是你的主要同盟国。既然这样,就让我们导入简单 的外覆类型 (wrapper types) 来区别天数、月份和年份,然后于Date 构造函数中使用 这些类型 z struct Day { e}变p1icit Day(int d) : val (d) {} int val; struct Month { explicit Month(int m) val (m) {} int val; }; struct Year { explicit Year(int y) : val (y) {} int val; class Date { public: Date(const Month& m, const Day& d, const Year& y); Date d(30, 3, 1995); II错误!不正确的类型 Date d(Day(30) , Month( 匀, Year (1995) ); I I 错误!不正确的类型 Date d (Month (匀, Day(30) , Year(1995)); IIO~ 类型正确 令 Day, Month和 Year 成为成熟且经充分锻炼的classes 并封装其内数据,比简单 使用上述的 structs 好(见条款22) 。但即使 structs 也已经足够示范:明智而审慎 地导入新类型对预防"接口被误用"有神奇疗效。 Effective C++ 中文版,第三版80 4 设计与声明 一旦正确的类型就定位,限制其值有时候是通情达理的。例如一年只有 12 个有效 月份,所以 Month 应该反映这一事实。办法之一是利用 enum 表现月份,但 enums 不 具备我们希望拥有的类型安全性,例如 enums 可被拿来当一个 ints 使用(见条款 2) 。 比较安全的解法是预先定义所有有效的 Months: class Month { public: static Month Jan() { return Month(l); } static Month Feb() { return Month(2); } static Month Dec() { return Month(12); } private: explicit Month(int m); Date d(Month::Mar() , Day(30) , Year(1995»; II 函数,返回有效月份。 II稍后解释为什么。 II这些是函数而非对象。 II其他成员函数 II 阻止生成新的月份。 II这是月份专属数据。 如果"以函数替换对象,表现某个特定月份"让你觉得诡异,或许是因为你忘记 了 non-local static 对象的初始化改序有可能出问题。建议阅读条款4 恢复记忆。 预防客户错误的另一个办法是,限制类型内什么事可做,什么事不能做。常见的 限制是加上 const。例如条款 3 曾经说明为什么"以const 修饰 operator* 的返回类 型"可阻止客户因"用户自定义类型"而犯错: if (a * b = c) II喔欧,原意其实是要做一次比较动作! 下面是另一个一般性准则"让types 容易被正确使用,不容易被误用"的表现形式: "除非有好理由,否则应该尽量令你的types 的行为与内置可pes 一致"。客户已经知 道像 int 这样的 type 有些什么行为,所以你应该努力让你的types 在合样合理的前提 下也有相同表现。例如,如果a 和 b 都是 ints,那么对 a*b 赋值并不合法,所以除非 你有好的理由与此行为分道扬辘,否则应该让你的types 也有相同的表现。是的,一旦 怀疑,就请拿 ints 做范本。 避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。很少有其 他性质比得上"一致性"更能导致"接口容易被正确使用",也很少有其他性质比得 Effective C++ 中文版,第三版4 条款 18: 让接口容易被正确使用,不易被误用 81 上"不一致性"更加剧接口的恶化。 STL 容器的接口十分一致(虽然不是完美地一致) , 这使它们非常容易被使用 o 例如每个 STL 容器都有一个名为 size 的成员函数,它会 告诉调用者目前容器内有多少对象。与此对比的是 Java ,它允许你针对数组使用 length property , 对 Strings 使用 length method , 而对 Lists 使用 size method; .NET 也一 样坦乱,其 Arrays 有个 prope即名为 Length,其 ArrayLists 有个 property 名为 Count 。 有些开发人员会以为整合开发环境 (integrated development environments, IDEs) 能使这 般不→致性变得不重要,但他们错了。不一致性对开发入员造成的心理和精神上的摩 擦与争执,没有任何一个 IDE 可以完全抹除。 任何接口如果要求客户必须记得做某些事情,就是有着"不正确使用"的倾向, 因为客户可能会忘记做那件事。例如条款 13 导入了一个 factory 函数,它返回-个指 针指向 Investment 继承体系内的一个动态分配对象 z Investment* createInvestment(); II来自条款 13; 为求简化暂略参数。 为避免资源泄漏, cr臼ea眈teIn川1口lV四Te臼st回1且且n阳阳l巴ent 返回的指针最终必须被删除,但那至少开启 了两个客户错误机会:没有删除指针,或删除同一个指针超过)次。 条款 13 表明客户如何将 createlnvestment 的返回值存储于一个智能指针如 auto_ptr或 trl::sharedytr内,因而将 delete 责任推给智能指针。但万一客户忘 记使用智能指针怎么办?许多时候,较佳接口的设计原则是先发制人,就令factory 函 数返回一个智能指针: std::trl::sharedytr create工 nvestment() ; 这便实质上强迫客户将返回值存储于一个 trl:: shared_ptr 内,几乎消弹了忘记 删除底部 Investment 对象(当它不再被使用时)的可能性。 实际上,返回 trl:: shared_ptr 让接口设计者得以阻止一大群客户犯下资源泄漏 的错误,因为就如条款 14 所言, trl::sharedytr 允许当智能指针被建立起来时指定 一个资掠释放函数(所谓删除器, "deleter") 绑定于智能指针身上 (autoytr 就没有 这种能耐)。 假设 class 设计者期许那些"从 createInvestment 取得 Investment* 指针"的客 户将该指针传递给→个名为 getRidOfInvestment 的函数,而不是直接在它身上动刀 (使用 delete) 。这样-个接口叉开启通往另一个客户错误的大门,该错误是"企图 使用错误的资源析构机制" (也就是拿 delete 替换 getRidOfInvestment) 。 Effective C++ 中文版,第三版std::trl::sharedytr plnv(O, getRidOfInvestment); 4 设计与声明 createInvestment 的设计者可以针对此问题先发制人:返回一个"将 getRidOfInvestment绑定为删除器 (deleter) "的 trl::sharedytr。 trl: : sharedytr 提供的某个构造函数接受两个实参z 一个是被管理的指针,另­ 个是引用次数变成 O 时将被调用的"删除槽'。这启发我们创建一个null trl: : shared_ptr 并以 getRidOflnvestment 作为其删除器,像这样: II 企图创建一个 null sharedytr II并携带→个自定的删除器。 II此式无法通过编译。 啊呀,这不是有效的C忡。 trl::sharedytr构造函数坚持其第一参数必须是个 指针,而 O 不是指针,是个int。是的,它可被转换为指针,但在此情况下并不够好, 因为 trl:: sharedytr坚持要一个不折不扣的指针。转型(cast) 可以解决这个问题: std::trl::sharedytr pInv( static cast(O) , getRidOfInvestment); II建立一个 null sharedytr 并以 IlgetRidOfInvestment为删除器; II条款 27 提到 static cast 因此,如果我们要实现createInvestment使它返回一个 trl: : shared_ptr 并夹 带 getRidOfIn飞.restment 函数作为删除器,代码看起来像这样: std::trl::sharedytr createInvestment() { std::trl::sharedytr retVal(static_cast(0) , getRidOflnvestment); retVal = ...; II令 retVal 指向正确对象 return retVal; 当然啦,如果被pInv 管理的原始指针 (raw pointer) 可以在建立plnv 之前先确定 下来,那么"将原始指针传给plnv 构造函数"会比"先将pInv 初始化为 null 再对它 做一次赋值操作"为佳。至于其原因,请见条款26 。 trl::sharedytr有一个特别好的性质是z 它会自动使用它的"每个指针专属的删 除器",因而消除另一个潜在的客户错误:所谓的"cross-DLL problem"。这个问题发 生于"对象在动态连接程序库CDLL)中被 ηew 创建,却在另一个DLL 内被 delete 销毁"。在许多平台上,这一类"跨DLL 之 new/delete 成对运用"会导致运行期错 误。 trl::sharedytr没有这个问题,因为它缺省的删除器是来自"trl: : shared_ptr 诞生所在的那个DLL" 的 delete. 这意思是......晤……让我举个例子,如果Stock派 生自 Investment而 createInvestment实现如下: Effective C++ 中文版 F 第三版4 条款 18: 让接口容易被正确使用,不易被误用 std::trl::shared-ptr createInvestment() { return std::trl::shared-ptr(new Stock); 83 返回的那个 trl: : shared-ptr 可被传递给任何其他DLLs,无需在意 "cross-DLL problem"。这个指向 Stock 的 trl: :shared_ptrs 会追踪记录"当Stock 的引用次数变 成 0 时该调用的那个DLL's delete" 。 本条款并非特别针对 trl: : shared-ptr ,而是为了"让接口容易被正确使用,不 容易被误用"而设。但由于trl::shared-ptr 如此容易消除某些客户错误,值得我们 核计其使用戚本。最常见的trl::shared-ptr 实现品来自 Boost (见条款 55) 0 Boost 的 shared-ptr 是原始指针 (raw pointer) 的两倍大,以动态分配内存作为簿记用途和 "删除器之专属数据",以v挝ual 形式调用删除器,并在多线程程序修改引用次数时 蒙受线程同步化 (thread synchronization) 的额外开销。(只要定义一个预处理器符号 就可以关闭多钱程支持)。总之,它比原始指针大且慢,而且使用辅助动态内存。在 许多应用程序中这些额外的执行成本并不显著,然而其"降低客户错误"的成效却是 每个人都看得到。 请记住 ·好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这 些性质。 • "促进正确使用"的办法包括接口的一致性,以及与内置类型的行为兼容。 • "阻止误用"的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除 客户的资源管理责任。 • trl::shared-ptr 支持定制型删除器( custom deleter) 。这可防范DLL 问题,可被 用来自动解除互斥锁 (mutexes; 见条款 14) 等等。 El知ctive C++中文版,第三版84 条款 19: 设计 class 犹如设计 type Treat class design 副 type design. 4 设计与声明 C++ 就像在其他 OOP (面向对象编程)语言一样,当你定义一个新class. 也就定 义了一个新 type。身为 C++ 程序员,你的许多时间主要用来扩张你的类型系统(type system) 。这意味你并不只是 class 设计者,还是 type 设计者。重载(overloading) 函 数和操作符、控制内存的分配和归还、定义对象的初始化和终结……全都在你手上。 因此你应该带着和"语言设计者当初设计语言内置类型时"一样的谨慎来研讨class 的 设计。 设计优秀的 classes 是一项艰巨的工作,因为设计好的 types 是一项艰巨的工作。 好的 types 有自然的语法,直观的语义,以及一或多个高效实现品。在C++ 中,一个 不良规划下的 class 定义恐怕无法达到上述任何一个目标。甚至class 的成员函数的效 率都有可能受到它们"如何被声明"的影响。 那么,如何设计高效的 classes 呢?首先你必须了解你面对的问题。儿乎每一个class 都要求你面对以下提问,而你的回答往往导致你的设计规范z ·新 type 的对象应该如何被创建和销毁?这会影响到你的class 的构造函数和析构函 数以及内存分配函数和释放函数(operatornew, operatornew [l; operator delete 和 operator delete[]一一见第 8 章)的设计,当然前提是如果你打算撰写它们。 ·对象的初始化和对象的赋值该有什么样的差别?这个答案决定你的构造函数和赋 值( assignment) 操作符的行为,以及其间的差异。很重要的是别混淆了"初始化" 和"赋值",因为它们对应于不同的函数调用(见条款的。 ·新 type 的对象如果被passed by value ( 以值传递) ,意味着什么?记住 , copy 构造 函数用来定义一个 type 的 pass-by-value 该如何实现。 ·什么是新 type 的"合法值It ?对 class 的成员变量而言,通常只有某些数值集是有 效的。那些数值集决定了你的 class 必须维护的约束条件 头文件并在其中内含 std 命名空间内的每一样东西, 而是有数十个头文件 «vector> , ,如emory> 等等) ,每个头文件声明 std 的某些机能。如果客户只想使用 vector 相关机能,他不需要 #include ; 如果客户不想使用 list ,也不需要#include 。这允许客户只对他们所用的那 一小部分系统形成编译相依(见条款3 I.其中讨论降低编译依存性的其他做法)。以 此种方式切割机能并不适用于class 成员函数,因为一个 class 必须整体定义,不能被 分割为片片段段。 将所有便利函数放在多个头文件内但隶属同→个命名空间,意味客户可以轻松扩 展这一组便利函数。他们需要做的就是添加更多non-member non-企iend 函数到此命名 空间内。举个例子,如果某个WebBrowser客户决定写些与影像下载相关的便利函数, 他只需要在 WebBrowserStuff命名空间内建立一个头文件,内含那些函数的声明即可。 新函数就像其他旧有的便利函数那样可用且整合为一体。这是class 无法提供的另一 Effective C++ 中文版,第三版102 4 设计与声明 个性质,因为 class 定义式对客户而言是不能扩展的。当然啦,客户可以派生出新的 classes, @ derived classes 无法访问 base class 中被封装的(即 private) 成员,于是如 此的"扩展机能"拥有的只是次级身份。此外一如条款 7 所说,并非所有 classes 都被 设计用来作为 base classes 。 请记住 ·宁可拿 non-member non-企iend 函数替换 member 函数。这样做可以增加封装性、包 裹弹性 (packaging flexibility )和机能扩充性。 条款 24 :若所育参数皆需类型转换,请为此采用 non-member 函数 Declare non-member functions when type conversions should apply to all parameters. 我在导读中提过,令classes 支持隐式类型转换通常是个糟糕的主意。当然这条规 则有其例外,最常见的例外是在建立数值类型时。假设你设计一个class 用来表现有理 数,允许整数"隐式转换"为有理数似乎颇为合理。的确,它并不比C++ 内置从 int 至 double 的转换来得不合理,而还比C++ 内置从 double 至 int 的转换来得合理些。 假设你这样开始你的Rational class: class Rational ( public: Rational(int numerator = 0, int denominator = 1); int numerator() const; int denominator() const; .private: II构造函数刻意不为explicit; II允许 int-to-Rational 隐式转换。 II分子 (numerator) 和分母 (denominator) II 的访问函数 (aωessors) 见条款 22 。 你想支持算术运算诸如加法、乘法等等,但你不确定是否该由member 函数、 non-member函数,或可能的话由non-member台iend 函数来实现它们。你的直觉告诉你, 当你犹豫就该保持面向对象精神。你知道有理数相乘和Rational class 有关,因此恨 自然地似乎该在Rational class 内为有理数实现operator飞条款 23 曾经反直觉地主 张,将函数放进相关class 内有时会与面向对象守则发生矛盾,但让我们先把那放在一 E1知ctive C++中文版,第二版4 条款 24: 若所有参数皆需类型转换,请为此采用 non-member 函数 旁,先研究一下将 operator* 写成Rational 成员函数的写法: class Rational { public: const Rational operator* (const Rational& rhs) const; 103 (如果你不确定为什么这个函数被声明为此种形式,也就是为什么它返回一个 const by-value 结果但接受一个reference-t~const 实参,请参考条款3 , 20 和 210 ) 这个设计使你能够将两个有理数以最轻松自在的方式相乘: Rational oneEighth(l, 8); Rational oneHalf(l, 2); Rational result = oneHalf * oneEighth; II很好 result = result 女 oneEighth; II 很好 但你还不满足。你希望支持混合式运算,也就是拿 Rationals 和……嗯......例如 ints 相乘。毕竟很少有什么东西会比两个数值相乘更自然的了一-即使是两个不同类 型的数值。 然而当你尝试混合式算术,你发现只有一半行得通: result = oneHalf * 2; result = 2 * oneHalf; II很好 II错误! 这不是好兆头。乘法应该满足交换律,不是吗? 当你以对应的函数形式重写上述两个式子,问题所在便一目了然了: result = oneHalf.operator*(2); result = 2.operator*(oneHalf); II很好 II错误! 是的, oneHalf 是一个内含 operator* 函数的 class 的对象,所以编译器调用该 函数。然而整数2 并没有相应的 class,也就没有 operator*成员函数。编译器也会尝 试寻找可被以下这般调用的non-member operator* (也就是在命名空间内或在global 作用域内) : result = operator*(2, oneHalf); II错误! 但本例并不存在这样一个接受int 和Rational 作为参数的 non-member operato沪,因此查找失败。 Effective C++ 中文版F 第三版104 4 设计与声明 再次看看先前成功的那个调用。注意其第二参数是整数 2 .但 Rational::operator* 需要的实参却是个 Rational 对象。这里发生了什么事?为什 么 2 在这里可被接受,在另一个调用中却不被接受? 因为这里发生了所谓隐式类型转换 (implicit type conversion) 。编译器知道你正在 传递一个 int ,而函数需要的是 Rational; 但它也知道只要调用 Rational 构造函数 并赋予你所提供的 int ,就可以变出一个适当的 Rational 来。于是它就那样做了。换 句话说此→调用动作在编译器眼中有点像这样: const Rational temp(2); result = oneHalf * temp; II根据 2 建立→个暂时性的Rational 对象。 II等同于 oneHalf.operator门 temp) ; 当然,只因为涉及 non-expl ici t 构造函数,编译器才会这样做。如果Rational 构造函数是 explicit. 以下语句没有一个可通过编译: result = oneHalf * 2; II错误! (在 explicit 构造函数的情况下) II无法将 2 转换为→个Rational。 result = 2 * oneHalf; II一样的错误,一样的问题。 这就很难让 Rational class 支持混合式算术运算了,不过至少上述两个句子的行 为从此一致。。 然而你的目标不仅在一致性,也要支持混合式算术运算,也就是希望有个设计能 让以上语句通过编译。这把我们带回到上述两个语句,为什么即使Rational构造函数 不是 explicit. 仍然只有一个可通过编译,另→个不可以: result = oneHalf 女 2; II 没问题(在 non-explicit 构造函数的情况下) result = 2 * oneHalf; II错误! (甚至在 non-explicit 构造函数的情况下〉 结论是,只有当参数被列于参数列(parameterlist) 内,这个参数才是隐式类型转 换的合格参与者。地位相当于"被调用之成员函数所隶属的那个对象"一-即this 对 象一一的那个隐喻参数,绝不是隐式转换的合格参与者。这就是为什么上述第一次调 用可通过编译,第二次调用则杏,因为第一次调用伴随一个放在参数列内的参数,第 二次调用则否。 然而你一定也会想要支持混合式算术运算。可行之道终于拨云见日z 让 operator* 成为一个 non-member函数, f卑允许编译器在每-个实参身上执行隐式类型转换t El作ctive C++中文版,第三版4 条款 24: 若所有参数皆需类型转换,请为此采用 non-member 函数 class Rational { 105 }; const Rational operator*(const Rational& lhs, const Rational& rhs) II不包括 operator食 II现在成了一个 Ilnon-member函数 return Rational(lhs.numerator() * rhs.numerator( ), lhs.denominator() * rhs.denominator()); Rational oneFourth(l, 4); Rational result; result = oneFourth * 2; result = 2 * oneFourth; II没问题 II万岁,通过编译了! 这当然是个快乐的结局,不过还有一点必须操心:operator* 是否应该成为 Rational class 的一个企iend 函数呢? 就本例而言答案是否定的,因为operator* 可以完全藉由 Rational 的 public 接 口完成任务,上面代码已表明此种做法。这导出一个重要的观察:member 函数的反面 是 non-member函数,不是企iend 函数。太多 C++ 程序员假设,如果一个"与某class 相关"的函数不该成为一个member (也许由于其所有实参都需要类型转换,例如先前 的Rational 的 operator* 函数) ,就该是个 friend。本例表明这样的理由过于牵强。 无论何时如果你可以避免仕iend 函数就该避免,因为就像真实世界一样,朋友带来的 麻烦往往多过其价值。当然有时候企iend 有其正当性,但这个事实依然存在:不能够 只因函数不该成为member. 就自动让它成为 friend。 本条款内含真理,但却不是全部的真理。当你从Object-Oriented C++ 跨进 Template C++ (见条款1)并让Rational成为一个 class template 而非 class,又有一 些需要考虑的新争议、新解法、以及一些令人惊讶的设计牵连。这些争议、解法和设 计牵连形成了条款460 请记住 ·如果你需要为某个函数的所有参数(包括被this 指针所指的那个隐喻参数)进行 类型转换,那么这个函数必须是个non-member。 Effective C++ 中文版,第三版106 4 设计与声明 条款 25: 弩虑写出一个不抛异常的 swap 函数 Consider support for a non-throwing swap. swap 是个有趣的函数。原本它只是STL 的一部分,而后成为异常安全性编程 (exception-safe programming,见条款 29) 的脊柱,以及用来处理自我赋值可能性(见 条款 I I)的一个常见机制。由于swap 如此有用,适当的实现很重要。然而在非凡的重 要性之外它也带来了非凡的复杂度。本条款探讨这些复杂度及因应之道。 所谓 swap(置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下~叩 动作可由标准程序库提供的swap 算法完成。其典型实现完全如你所预期: namespace std { template void swap( T& a , T& b) T temp(a); a = b; b = temp; Ilstd: :swap 的典型实现; II置换 a 和 b 的值. 只要类型 T 支持 copying (通过 copy构造函数和 copy assignment操作符完成) , 缺省的 swap 实现代码就会帮你置换类型为 T 的对象,你不需要为此另外再做任何工作。 这缺省的 swap 实现版本十分平泼,无法剌激你的肾上腺。它涉及三个对象的复制 z a 复制到 temp , b 复制到 a ,以及 temp 复制到 b 。但是对某些类型而言,这些复制动 作无一必要:对它们而言 swap 缺省行为等于是把高速铁路铺设在慢速小巷弄内。 其中最主要的就是"以指针指向一个对象,内含真正数据"那种类型。这种设计 的常见表现形式是所谓 "pimpl 手法" (pimpI 是 "pointerωimplementation" 的缩写, 见条款 3 I)。如果以这种手法设计 Widget class ,看起来会像这样: class Widgetlmpl { public: private: int a , b, c; std::vector v; Effective C++ 中文版,第三版 II 针对 Widget 数据而设计的 class; II 细节不重要。 II 可能有许多数据, II 意味复制时间很长。4 条款 25: 考虑写出一个不抛异常的 swap 函数 class Widget ( public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) { 食 plmpl = *(rhs.plmpl); private: Widgetlmpl* plmpl; 107 II 这个 class 使用 pimpl 手法 II 复制 Widget 时,令它复制其 II Widgetlmpl 对象。 II 关于 operator=的一般性实现 II 细节,见条款 10 , 11 和 12 。 II指针,所指对象内含 II Widget 数据。 一旦要置换两个 Widget 对象值,我们唯一需要做的就是置换其 plmpl 指针,但缺 省的 swap 算法不知道这一点。它不只复制三个 Widgets ,还复制三个 Widge tImpl 对 象。非常缺乏效率!一点也不令人兴奋。 我们希望能够告诉 std:: swap: 当 Widgets 被置换时真正该做的是置换其内部的 pI呻l 指针。确切实践这个思路的一个做法是 2 将 std: :swap 针对 Widget 特化。下面 是基本构想,但目前这个形式无法通过编译: namespace std ( template<> void swap( Widget& a , Widget& b ) swap(a.plmpl, b.plmpl); II这是 std: :swap 针对 II"T 是 Widget" 的特化版本。 II 目前还不自掘过编译。 II 置换 Widgets 时只要置换它们的 II plmpl 指针就好。 这个函数一开始的 "template<>" 表示它是 std: :swap 的一个全特化( total template specialization) 版本,函数名称之后的 "" 表示这一特化版本系针对 "T 是 Widget" 而设计。换句话说当一般性的 swap template 施行于 Widgets 身上便会 启用这个版本。通常我们不能够(不被允许)改变 std 命名空间内的任何东西,但可 以(被允许)为标准 templates (如 swap) 制造特化版本,使它专属于我们自己的 classes (例如 Widget>。以上作为正是如此。 但是一如稍早我说,这个函数无法通过编译。因为它企图访问 a 和 b 内的 plmpl 指针,而那却是 private 。我们可以将这个特化版本声明为企ie时,但和以往的规矩不太 El知ctive C++ 中文版,第三版108 4 设计与声明 一样:我们令 Widget 声明一个名为 swap 的 public 成员函数做真正的置换工作,然后 将 std: :swap 特化,令它调用该成员函数: class Widget { public: void swap(Widget& other) using std::swap; swap(pImpl, other.pImpl); } ; narnespace std { template<> void swap( Widget& a , Widget& b ) a.swap (b); II与前同,唯一差别是增加swap 函数 II这个声明之所以必要,稍后解释。 II若要置换Widgets 就置换其pImpl 指针。 II修订后的 std: :swap 特化版本 II若要置换Widgets,调用其 Ilswap 成员函数。 这种做法不只能够通过编译,还与STL 容器有一致性,因为所有STL 容器也都提 供有 public swap 成员函数和 std: :swap 特化版本(用以调用前者)。 然而假设 Widget 和 WidgetImpl 都是 class templates 而非 classes,也许我们可以试 试将 WidgetI呻l 内的数据类型加以参数化: template class WidgetImpl { ... }; template class Widget { ... }; 在 Widget 内(以及 WidgetImpl 内,如果需要的话)放个swap 成员函数就像以往 一样简单,但我们却在特化std: :swap 时遇上乱流。我们想写成这样: n臼nespace std { template void swap< Widget >(Widget& a , Widget& b) { a.swap(b); } Effective C++ 中文版,第二版 II 错误!不合法!4 条款 25: 考虑写出→个不抛异常的 swap 函数 109 看起来合情合理,却不合法。是这样的,我们企图偏特化 (partially specialize) 一 个 function template (std: : swap) .但 C++ 只允许对 class templates 偏特化,在 function templates 身上偏特化是行不通的。这段代码不该通过编译(虽然有些编译器错误地接 受了它)。 当你打算偏特化一个function template 时,惯常做法是简单地为它添加一个重载版 本,像这样: narnespace std. ( ternplate void swap{Widget& a , Widget& b) ( a.swap(b); } Ilstd: :swap 的一个重载版本 II (注意 "swap" 之后没有"<...>" /υ/稍后我会告诉你,这也不合法。 一般而言,重载 function templates 没有问题,但 std 是个特殊的命名空间,其管 理规则也比较特殊。客户可以全特化std 内的 templates. 但不可以添加新的 templates (或 classes 或 functions 或其他任何东西)到 std 里头。 std 的内容完全由 C++ 标准 委员会决定,标准委员会禁止我们膨胀那些已经声明好的东西。啊呀,所谓"禁止" 可能会使你沮丧,其实跨越红线的程序几乎仍可编译和执行,但它们的行为没有明确 定义。如果你希望你的软件有可预期的行为,请不要添加任何新东西到std 里头。 那该如何是好?毕竟我们总是需要一个办法让其他人调用swap 时能够取得我们提 供的较高效的 template 特定版本。答案很简单,我们还是声明一个non-member swap 让它调用 member swap. 但不再将那个non-member swap 声明为 std: :swap 的特化版 本或重载版本。为求简化起见,假设Widget 的所有相关机能都被置于命名空间 WidgetStuff 内,整个结果看起来便像这样: narnespace WidgetStuff { template class Widget { ... }; template void swap(Widget& a , Widget& b) a.swap(b) ; II模板化的 Widget 工mpl 等等。 II 同前,内含 swap 成员函数。 Ilnon-member swap 函数: II 这里并不属于 std 命名空间。 Effective C++ 中文版,第三版110 4 设计与声明 现在,任何地点的任何代码如果打算置换两个 Widget 对象,因而调用 swap , C++ 的名称查找法则 (name lookup rules: 更具体地说是所谓argument-dependent100归p 或 Koenig lookup 法则)会找到 WidgetStuff 内的 Widget 专属版本。那正是我们所耍的。 这个做法对 classes 和 class templates 都行得通,所以似乎我们应该在任何时候都 使用它。不幸的是有一个理由使你应该为classes 特化 std: :swap (很快我会描述它) , 所以如果你想让你的" class 专属版" swap 在尽可能多的语境下被调用,你需得同时在 该 class 所在命名空间内写一个 non-member 版本以及一个 std: :swap 特化版本。 顺带一提,如果没有像上面那样额外使用某个命名空间,上述每件事情仍然适用 (也就是说你还是需要一个 non-member swap 用来调用 member swap) 。但,何必在 global 命名空间内塞满各式各样的 class , template, function, enum, enumerant 以及 typedef 名称呢?难道你对所谓"得体与适度"失去判断力了吗? 目前为止我所写的每一样东西都和 swap 编写者有关。换位思考,从客户观点看看 事情也有必要。假设你正在写一个 function template ,其内需要置换两个对象值 z template void doSomething(T& objl, T& obj2) swap(objl, obj2); 应该调用哪个swap? 是 std 既有的那个一般化版本?还是某个可能存在的特化版 本?抑或是一个可能存在的T 专属版本而且可能栖身于某个命名空间(但当然不可以 是 std) 内?你希望的应该是调用T 专属版本,并在该版本不存在的情况下调用std 内的一般化版本。下面是你希望发生的事: template void doSomething(T& objl, T& obj2) { using std:: swap; I I令 std: :swap 在此函数内可用 swap(objl, obj2); II为 T 型对象调用最佳swap 版本 Efj告ctive C++中文版,第三版4 条款 25: 考虑写出一个不抛异常的 sw叩函数 111 一旦编译器看到对 swap 的调用,它们便查找适当的 swap 并调用之。 C++ 的名称 查找法则 (name lookup rules) 确保将找到 global 作用域或 T 所在之命名空间内的任何 T 专属的 swap。如果 T 是 Widget 并位于命名空间 WidgetStuff 内,编译器会使用"实 参取决之查找规则" (argument-dependent lookup) 找出 WidgetStuff 内的 swap。如果 没有 T 专属之 swap 存在,编译器就使用 std 内的 swap,这得感谢 using 声明式让 std: :swap 在函数内曝光。然而即使如此编译器还是比较喜欢std:: swap 的 T 专属特 化版,而非一般化的那个template,所以如果你已针对T 将 std: :swap特化,特化版会 被编译器挑中。 因此,令适当的 swap 被调用是很容易的。需要小心的是,别为这一调用添加额外 修饰符,因为那会影响C++ 挑选适当函数。假设你以这种方式调用swap: std::swap(objl, obj2); II这是错误的 swap 调用方式 这便强迫编译器只认std 内的 swap (包括其任何template 特化) ,因而不再可能 调用一个定义于它处的较适当T 专属版本。啊呀,某些迷途程序员的确以此方式修饰 swap 调用式,而那正是"你的classes 对 std: :swap 进行全特化"的重要原因:它使得 类型专属之 swap 实现版本也可被这些"迷途代码"所用(这样的代码出现在某些标准 程序库实现版中,如果你有兴趣不妨帮助这些代码尽可能高效运作)。 此刻,我们已经讨论过default swap、 memberswaps、 non-memberswap喝、 std: :swap 特化版本、以及对swap 的调用,现在让我把整个形势做个总结。 首先,如果 swap 的缺省实现码对你的class 或 class template 提供可接受的效率, 你不需要额外做任何事。任何尝试置换(swap) 那种对象的人都会取得缺省版本,而 那将有良好的运作。 其次,如果 swap 缺省实现版的效率不足(那几乎总是意味你的class 或 template 使用了某种 pimpl 手法) ,试着做以下事情2 1.提供一个public swap 成员函数,让它高效地置换你的类型的两个对象值。稍后我将 解释,这个函数绝不该抛出异常。 2. 在你的 class 或 template 所在的命名空间内提供一个non-memberswap,并令它调用 上述 swap 成员函数。 Ej知ctive C++中文版,第三版112 4 设计与声明 3. 如果你正编写一个 class (而非 class template) ,为你的 class 特化 std: :swap。并 令它调用你的 swap 成员函数。 最后,如果你调用 swap,请确定包含一个using 声明式,以便让 std: :swap 在你 的函数内曝光可见,然后不加任何namespace 修饰符,赤裸裸地调用swap。 唯一还未明确的是我的劝告:成员版swap 绝不可抛出异常。那是因为swap 的一 个最好的应用是帮助 classes (和 class templates) 提供强烈的异常安全性 (exception咽fety )保障。条款29 对此主题提供了所有细节,但此技术基于一个假设z 成员版的 swap 绝不抛出异常。这→约束只施行于成员版!不可施行于非成员版,因为 swap 缺省版本是以 copy 构造函数和 copy Qssi9n~ent操作符为基础,而一般情况下两 者都允许抛出异常。因此当你写下一个自定版本的swap,往往提供的不只是高效置换 对象值的办法,而且不抛出异常。一般而言这两个swap 特性是连在一起的,因为高效 率的 swap唱几乎总是基于对内置类型的操作(例如pimpl 手法的底层指针) ,而内置类 型上的操作绝不会抛出异常。 请记住 ·当 std: :swap 对你的类型效率不高时,提供一个swap 成员函数,并确定这个函数 不抛出异常。 ·如果你提供一个member swap,也该提供→个non-member swap 用来调用前者。对 于 classes (而非 templates) ,也请特化 std: :swapo ·调用 swap 时应针对 std~ :swap 使用 using 声明式,然后调用 swap 并且不带任何"命 名空间资格修饰"。 ·为"用户定义类型"进行std templates 全特化是好的,但千万不要尝试在std 内加 入某些对 std 而言全新的东西。 Efj告ctive C++ 中文版,第三版5 条款 26: 尽可能延后变量定义式的出现时间 113 5 实现 Implementations 大多数情况下,适当提出你的classes (和 class templates) 定义以及 functions (和 function templates) 声明,是花费最多心力的两件事。一旦正确完成它们,相 应的实现大多直截了当。尽管如此,还是有些东西需要小心。太快定义变量可能造 成效率上的拖延:过度使用转型(casts) 可能导致代码变慢又难维护,又招来微妙 难解的错误:返回对象"内部数据之号码牌(handles) "可能会破坏封装并留给客 户虚吊号码牌 (dangling handles) ;未考虑异常带来的冲击则可能导致资源泄漏和 数据败坏:过度热心地 inlining 可能引起代码膨胀;过度糯合 (coupling) 则可能导 致让人不满意的冗长建置时间 (build times) 。 所有这些问题都可避免。本章逐-解释各种做法。 条款 26: 尽可能延后变量定义式的出现时间 Postpone variable definitions as long as possible. 只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的 控制流 (control flow) 到达这个变量定义式时,你便得承受构造成本:当这个变量 离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费 这些成本,所以你应该尽可能避免这种情形。 或许你会认为,你不可能定义一个不使用的变量, {§.话不要说得太早!考虑下 面这个函数,它计算通行密码的加密版本而后返回,前提是密码够长。如果密码太 短,函数会丢出一个异常,类型为 logic error (定义于 C++ 标准程序库,见条 款 54) : El知ctive C++ 中文版,第三版114 II 这个函数过早定义变量 "encrypted" std::string encryptPassword(const std::string& password) using namespace std; string encrypted; if (password.length() < Mi nimumPassword工.e ngth) ( throw logic error("Password is too short"); II必要动作, {.卑能将一个加密后的密码 II置入变量 encrypted内 return encrypted; 5 实现 对象 encrypted在此函数中并非完全未被使用,但如果有个异常被丢出,它就 真的没被使用。也就是说如果函数encryptPassword 丢出异常,你仍得付出 encrypted的构造成本和析构成本。所以最好延后encrypted的定义式,直到确实 需要它z II这个函数延后 "encrypted" 的定义,直到真正需要它 std::string encryptPassword(const std::string& password) using namespace std; if (password.length() < MinimumPasswordLength) ( throw logic_error("Password is too short"); string encrypted; II必要动作, {.卑能将一个加密后的密码 II置入变量 encrypted内 return encrypted; 但是这段代码仍然不够稼纤合度,因为encrypted虽获定义却无任何实参作为 初值。这意味调用的是其default构造函数。许多时候你该对对象做的第一次事就是 给它个值,通常是通过一个赋值动作达成。条款4 曾解释为什么"通过default构造 函数构造出一个对象然后对它赋值"比"直接在构造时指定初值"效率差。那个分 析当然也适用于此。举个例子,假设encryptPassword的艰难部分在以下函数中进 行: void encrypt(std::string& s); II在其中的适当地点对s 加密 El知ctive C++中文版,第三版5 条款 26: 尽可能延后变量定义式的出现时间 于是 encryptPassword 可实现如下,虽然还不算是最好的做法: II 这个函数延后 "encrypted" 的定义,直到需要它为止。 II 但此函数仍然有着不该有的效率低落。 std::string encryptPassword(const std::string& password) 115 std: :str工ng encrypted; encrypted = password; encrypt(encrypted); return encrypted; II检查 length,如前。 Ildefault心onstructencrypted II赋值给 encrypted 更受欢迎的做法是以password作为 encrypted的初值,跳过毫无意义的default 构造过程: II终于,这是定义并初始化encrypted 的最佳做法 std::string encryptPassword(const std::string& password) II检查长度。 std::string encrypted(password); II通过 copy构造函数 II定义并初始化。 encrypt(encrypted); return encrypted; 这让我们联想起本条款所谓"尽可能延后"的真正意义。你不只应该延后变量 的定义,直到非得使用该变量的前→刻为止,甚至应该尝试延后这份定义直到能够 给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以 避免无意义的 default构造行为。更深一层说,以"具明显意义之初值"将变量初始 化,还可以附带说明变量的目的。 "但循环怎么办? "你可能会感到疑惑。如果变量只在循环内使用,那么把它 定义于循环外并在每次循环迭代时赋值给它比较好,还是该把它定义于循环内?也 就是说下面左右两个一般性结构,哪一个比较好? II方法 A: 定义于循环外 Widget w; for (int i = 0; i < n; ++i) { w =取决于 i 的某个值; II 方法 B: 定义于循环内 for (int i = 0; i < n; ++i) ( Widget w(取决于 i 的某个值) ; Effective C++ 中文版,第三版116 5 实现 这里我把对象的类型从 string 改为 Widget ,以免造成读者对于"对象执行构 造、析构、或赋值动作所需的成本"有任何特殊偏见。 在 Widget 函数内部,以上两种写法的成本如下 z ·做法 A: 1 个构造函数 +1 个析构函数 +n 个赋值操作 ·做法 B: n 个构造函数 +n 个析构函数 如果 classes 的一个赋值成本低于一组构造+析构成本,做法 A 大体而言比较高 效。尤其当 n 值很大的时候。否则做法 B 或许较好。此外做法 A 造成名称 w 的作 用域(覆盖整个循环〉比做法 B 更大,有时那对程序的可理解性和易维护性造成冲 突。因此除非 (I) 你知道赋值成本比"构造+析构"成本低, (2) 你正在处理代码 中效率高度敏感 (performance-sensitive) 的部分,否则你应该使用做法 B 。 请记住 ·尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。 条款 27: 尽量少做转型动作 Minimize casting. C++ 规则的设计目标之一是,保证"类型错误"绝不可能发生。理论上如果你 的程序很"干净地"通过编译,就表示它并不企图在任何对象身上执行任何不安全、 无意义、愚蠢荒谬的操作。这是一个极具价值的保证,可别草率地放弃它。 不幸的是,转型 (casts) 破坏了类型系统 (type system) 。那可能导致任何种 类的麻烦,有些容易辨识,有些非常隐晦。如果你来自C , Java 或 C# 阵营,请特 别注意,因为那些语言中的转型 (casting) 比较必要而无法避免,也比较不危险(与 C++ 相较)。但 C++ 不是 C ,也不是 Java 或 C# 。在 C++ 中转型是一个你会想带 着极大尊重去亲近的一个特性。 让我们首先回顾转型语法,因为通常有三种不同的形式,可写出相同的转型动 作。 C 风格的转型动作看起来像这样z (T)expression II将 expression转型为 T 函数风格的转型动作看起来像这样: T(expression) £1知ctive C++中文版,第三版 II将 expression转型为 T5 条款 27: 尽量少做转型动作 117 两种形式并无差别,纯粹只是小括号的摆放位置不同而已。我称此二种形式为 "旧式转型 " (old-style casts) 。 C++ 还提供四种新式转型(常常被称为 new-style 或 C++-style casts) : canst_cast( e}φression ) dynamic_cast( expression) reinterpret_cast( expression) static_cast(ωψression ) 各有不同的目的: • canst cast 通常被用来将对象的常量性转除(cast away the constness) 。它也 是唯一有此能力的 C++-style 转型操作符。 • d归amic cast 主要用来执行"安全向下转型" ( safe downcasting) ,也就是用 来决定某对象是否归属继承体系中的某个类型。它是唯-无法由旧式语法执行 的动作,也是唯一可能耗费重大运行成本的转型动作(稍后细谈〉。 • reinterpret_cast 意固执行低级转型,实际动作(及结果)可能取决于编译器, 这也就表示它不可移植。例如将一个 pointer ωint 转型为一个 int 。这一类转 型在低级代码以外很少见。本书只使用二次,那是在讨论如何针对原始内存 (raw memory) 写出一个调试用的分配器 (debugging allocator) 时,见条款"。 • static cast 用来强迫隐式转换 (implicit conversions) ,例如将 non-const对 象转为 const 对象(就像条款3 所为) ,或将 int 转为 double 等等。它也可以 用来执行上述多种转换的反向转换,例如将void食指针转为 typed 指针,将 pointer-to-base 转为 pointer-to-derived。但它无挂将 const 转为 non-canst'一一这 个只有 canstcast 才办得到。 旧式转型仍然合法,但新式转型较受欢迎。原因是:第一,它们很容易在代码 中被辨识出来(不论是人工辨识或使用工具如grep) ,因而得以简化"找出类型系 统在哪个地点被破坏"的过程。第二,各转型动作的目标愈窄化,编译器愈可能诊 断出错误的运用。举个例子,如果你打算将常量性(constness)去掉,除非使用新 式转型中的 constcast 否则无法通过编译。 我唯一使用旧式转型的时机是,当我要调用一个explicit 构造函数将一个对 象传递给一个函数时。例如z E}知cfive C++ 中文版,第三版118 class Widget { public: explicit Widget(int size); 5 实现 void doSomeWork(const Widget& w); doSomeWork(Widget(15)); II 以一个 int 加上"函数风格"的 II转型动作创建一个Widget。 doSomeWork(static_cast(15)); II 以个 int 加上 "c++ 风格"的 II转型动作创建一个Widget。 从某个角度来说,蓄意的"对象生成"动作感觉不怎么像"转型",所以我很 可能使用函数风格的转型动作而不使用static cast。但我要再说一次,当我们写 下一段日后出错导致"核心倾印" (core dump) 的代码时,撰写之时我们往往"觉 得"通情达理,所以或许最好是忽略你的感觉,始终理智地使用新式转型。 许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一 种类型。这是错误的观念。任何一个类型转换(不论是通过转型操作而进行的显式 转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。 例如在这段程序中: int x , y; double d = static_cast(x)/y; Ilx 除以 y ,使用浮点数除法 将 intx 转型为 double 几乎肯定会产生一些代码,因为在大部分计算器体系 结构中, int 的底层表述不同于 double 的底层表述。这或许不会让你惊讶,但下 面这个例子就有可能让你稍微睁大眼睛了z class Base { ... }; class Derived: public Base { ... }; Derived d; Base* pb = &d; II 隐喻地将 Derived* 转换为 Base* 这里我们不过是建立一个 base class 指针指向一个 derived class 对象,但有时候 上述的两个指针值并不相同。这种情况下会有个偏移量 (offse t)在运行期被施行于 De rived* 指针身上,用以取得正确的 Base 食指针值。 上个例子表明,单一对象(例如一个类型为 Derived 的对象〉可能拥有一个以 上的地址(例如"以 Base* 指向它"时的地址和"以 Derived* 指向它"时的地址。 C 不可能发生这种事, Java 不可能发生这种事, C# 也不可能发生这种事。但 C++ 可 能 l 实际上一旦使用多重继承,这事几乎一直发生着。即使在单一继承中也可能发 El知ctive C++ 中文版,第三版5 条款 27: 尽量少做转型动作 119 生。虽然这还有其他意涵,但至少意味你通常应该避免做出"对象在 C++ 中如何 如何布局"的假设。当然更不该以此假设为基础执行任何转型动作。例如,将对象 地址转型为 char* 指针然后在它们身上进行指针算术,几乎总是会导致无定义(不 明确)行为。 但请注意,我说的是有时候需要一个偏移量。对象的布局方式和它们的地址计 算方式随编译器的不同而不同,那意味"由于知道对象如何布局"而设计的转型, 在某一平台行得通,在其他平台并不一定行得通。这个世界有许多悲惨的程序员, 他们历经千辛万苦才学到这堂课。 另一件关于转型的有趣事情是:我们很容易写出某些似是而非的代码(在其他 语言中也许真是对的)。例如许多应用框架 (application frameworks) 都要求 derived classes 内的 virtual 函数代码的第一个动作就先调用 base class 的对应函数。假设我 们有个 Window base class 和→个 SpecialWindowderived class,两者都定义了 virtual 函数 onResize。进一步假设 SpecialWindow 的 onResize 函数被要求首先调用 Window 的 onResize。下面是实现方式之→,它看起来对,但实际上错: class Window { public: virtual void onResize( ){... } Ilbase class libωeonResize 实现代码 lid出vedclass Ilderivedo口.Resize 实现代码 II将女 this 转型为 Window , II 然后调用其 onResize; II这不可行! II这里进行 SpecialWindow 专属行为。 class SpecialWindow: public Window { public: virtual void onResize( ){ static cast (*this) .onResize(); 我在代码中强调了转型动作(那是个新式转型,但若使用旧式转型也不能改变 以下事实)。一如你所预期,这段程序将*this 转型为 Window,对函数 onResize 的调用也因此调用了 Window::onResizeo 但恐怕你没想到,它调用的并不是当前 对象上的函数,而是稍早转型动作所建立的一个"女this 对象之 base class 成分"的 暂时副本身上的 onResize! (译注 z 函数就是函数,成员函数只有一份, "调用 起哪个对象身上的函数"有什么关系呢?关键在于成员函数都有个隐藏的this 指 El知ctive C++中文版,第三版120 5 实现 针,会因此影响成员函数操作的数据。)再说一次,上述代码并非在当前对象身上 调用 Window::onResize 之后又在该对象身上执行 SpecialWindow 专属动作。不, 它是在"当前对象之 base class 成分"的副本上调用 Window::onResize ,然后在当 前对象身上执行 SpecialWindow 专属动作。如果 Window::o nResize 修改了对象内 容(不能说没有可能性,因为 onResize 是个 non-const 成员函数) ,当前对象其 实没被改动,改动的是副本。然而 SpecialWindow::onResize 内如果也修改对象, 当前对象真的会被改动。这使当前对象进入一种"伤残"状态:其 base class 成分 的更改没有落实,而 derived class 成分的更改倒是落实了。 解决之道是拿掉转型动作,代之以你真正想说的话。你并不想哄骗编译器将 *this 视为一个 base class 对象,你只是想调用 base class 版本的 onResize 函数, 令它作用于当前对象身上。所以请这么写: class SpecialWindow: public Window { public: virtual void onResize( ){ Window::onResize(); II调用 Window::onResize作用于*this 身上 这个例子也说明,如果你发现你自己打算转型,那活脱是个警告信号:你可能 正将局面发展至错误的方向上。如果你用的是dynamic cast 更是如此。 在探究 dynamic_cast设计意涵之前,值得注意的是,dynamic_cast的许多实 现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于"class 名称之字符 串比较",如果你在四层深的单继承体系内的某个对象身上执行dynamic cast, 刚才说的那个实现版本所提供的每一次 dynamic cast 可能会耗用多达四次的 strcmp 调用,用以比较 class 名称。深度继承或多重继承的成本更高!某些实现版 本这样做有其原因(它们必须支持动态连接)。然而我还是要强调,除了对一般转 型保持机敏与猜疑,更应该在注重效率的代码中对 dynamic casts 保持机敏与猜 疑。 之所以需要 dynamic cast ,通常是因为你想在一个你认定为 derived class 对象 身上执行 derived class 操作函数,但你的手上却只有-个"指向 base" 的 pointer 或 reference ,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题。 £j知ctive C++ 中文版,第三版5 条款 27: 尽量少做转型动作 121 第二,使用容器并在其中存储直接指向 derived class 对象的指针(通常是智能 指针,见条款 13) ,如此便消除了"通过 base class 接口处理对象"的需要。假设 先前的 Window/SpecialWindow 继承体系中只有 SpecialWindows 才支持闪烁效果, 试着不要这样做 z class Window { ... }; class SpecialWindow: public Window { public: void blink () ; typedef II关于 trl::sharedytr std::vector > VPW; II 见条款 13. VPW winPtrs; for (VPW::iterator iter = winPtrs.begin(); II不希望使用 iter != winPtrs.end();++iter) { Ildynamic_cast. if (SpecialWindow * psw = dynam工 c_cast (iter->get ()) psw->blink () ; 应该改而这样做: typedef std::vector > VPSW; VPSW winPtrs; for ( VPSW::iterator iter = winPtrs.begin(); II这样写比较好, iter != winptrs.四d(); II不使用 dynamic_cast ++iter) (*iter)->blink(); 当然啦,这种做法使你无法在同一个容器内存储指针"指向所有可能之各种 Window派生类"。如果真要处理多种窗口类型,你可能需要多个容器,它们都必须 具备类型安全性(type-safe) 。 另一种做法可让你通过base class 接口处理"所有可能之各种Window派生类", 那就是在 base class 内提供 virtual 函数做你想对各个Window派生类傲的事。举个例 子,虽然只有 SpecialWindows可以闲烁,但或许将闪烁函数声明于base class 内并 提供一份"什么也没做"的缺省实现码是有意义的: E1知clive C++中文版F 第三版122 class Window { public: virtual void blink() {} }; class SpecialWindow: public Window { public: virtual void blink() { ... }; 5 实现 II缺省实现代码"什么也没做"; II条款 34 告诉你为什么 II 缺省实现代码可能是个馒主意。 II在此 class 内, Ilblink做某些事。 typedef std::vector > VPW; VPW winPtrs; II容器,内含指针,指向 II 所有可能的Window类型。 for (VPW::iterator iter = winPtrs.begin( ); iter != winPtrs.end(); ++iter) II注意,这里没有 (*iter) ->blink () ; II dynamic cast。 不论哪一种写法一一"使用类型安全容器"或"将virtual 函数往继承体系上方 移动"一一都并非放之四海皆准,但在许多情况下它们都提供一个可行的 dynami 绝对必须避免的一件事是所谓的"连串(cascading) dynamic_casts" ,也就 是看起来像这样的东西: class Window { ... }; lid町ivedclass臼定义在这里 typedef std::vector > VPW; VPW winptrs; for (VPW::iterator iter = winPtrs.begin( ); iter != winPtrs.end(); ++iter) if (SpecialWindowl * pswl = dynamic_cast(iter->get())) { else if (SpecialWindow2 * psw2 = dynamic cast(iter->get())) { else if (SpecialWindow3 * psw3 = dynamic_cast(iter->get())) { Effective C++ 中文版,第三版5 条款 28: 避免返回 handles 指向对象内部成分 123 这样产生出来的代码又大又慢,而且基础不稳,因为每次 Window class 继承体 系一有改变,所有这一类代码都必须再次检阅看看是否需要修改。例如一旦加入新 的 derived class ,或许上述连串判断中需要如入新的条件分支。这样的代码应该总 是以某些"基于 virtual 函数调用"的东西取而代之。 优良的 C++ 代码很少使用转型,但若说要完全摆脱它们又太过不切实际。例 如 p. 1l 8 从 int 转型为 do由 Ie 就是转型的一个通情达理的使用,虽然它并非绝对 必要(那段代码可以重新写过,声明一个类型为 double 的新变量并以 x 值初始化)。 就像面对众多蹊晓可疑的构造函数一样,我们应该尽可能隔离转型动作,通常是把 它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏握艇的动作 影响。 请记住 ·如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic casts 。 如果有个设计需要转型动作,试着发展无需转型的替代设计。 ·如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数, 而不需将转型放进他们自己的代码内。 回宁可使用 C++-style (新式)转型,不要使用旧式转型。前者很容易辨识出来, 而且也比较有着分门别类的职掌。 条款 28: 避免返回 handles 指向对象内部成分 Avoid returning "handles" to object internals. 假设你的程序涉及矩形。每个矩形由其左上角和右下角表示。为了让一个 Rectangle对象尽可能小,你可能会决定不把定义矩形的这些点存放在Rectangle 对象内,而是放在一个辅助的struct 内再让 Rectangle去指它: class Point ( II 这个 class 用来表述"点" public: Point{int X , int y); void setX(int newVal); void setY(int newVal); Effective C++ 中文版,第三版124 struct RectData { Point ulhc; Point lrhc; class Rectangle { II 这些"点"数据用来表现一个矩形 I lulhc = "up{町 left-hand corner" (左上角) II lrhc = "lower right-hand corner" (右下角) 5 实现 rec.upperLeft( ) .setX(50); Point coordl(O, 0); Point coord2(100, 100); const Rectangle rec(coordl, coord2); private: std::trl::shared-ptr pData; II关于 trl: : shared-ptr, II 见条款 13 Rectangle 的客户必须能够计算 Rectangle 的范围,所以这个 class 提供 upperLeft 函数和 lowerRight 函数。 Point 是个用户自定义类型,所以根据条款 20 给我们的忠告(它说以 by reference 方式传递用户自定义类型往往比以 by value 方式传递更高效) ,这些函数于是返回 references ,代表底层的 Point 对象: class Rectangle { public: Point& upperLeft( ) const { return pData->ulhc; ) Point& lowerRight( ) const { return pData->lrhc; ) 这样的设计可通过编译,但却是错误的。实际上它是自我矛盾的。一方面 upperLeft 和 lowerRight 被声明为 const 成员函数,因为它们的目的只是为了提 供客户一个得知 Rectangle 相关坐标点的方法,而不是让客户修改 Rectangle (见 条款 3) 。另一方面两个函数却都返回 references 指向 private 内部数据,调用者于 是可通过这些 references 更改内部数据!例如: Ilrec 是个 const 矩形, II 从 (0 , 0) 到 (100 , 100) II 现在 "rec 却变成 II 从 (50 , 0) 到 (100 , 100) 这里请注意, upper Left 的调用者能够使用被返回的 reference (指向 rec 内部 的 Point 成员变量〉来更改成员。但 rec 其实应该是不可变的 (cons t) ! 这立刻带给我们两个教训 II 。第一,成员变量的封装性最多只等于"返回其 reference" 的函数的访问级别。本例之中虽然 ulhc 和 lrhc 都被声明为 pri飞Tate , 它们实际上却是 public ,因为 public 函数 upperLeft 和 lowerRight 传出了它们的 Effective C++ 中文版,第三版5 条款 28: 避免返回 handles 指向对象内部成分 125 references 。第二,如果 const 成员函数传出一个 reference ,后者所指数据与对象自 身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。 这正是 bitwise constness 的一个附带结果,见条款 3 。 上面我们所说的每件事情都是由于"成员函数返回 references" 。如果它们返 回的是指针或迭代器,相同的情况还是发生,原因也相同。 References 、指针和迭 代器统统都是所谓的 handles ( 号码牌,用来取得某个对象) ,而返回一个"代表 对象内部数据"的 handle ,随之而来的便是"降低对象封装性"的风险。同时,一 如稍早所见,它也可能导致"虽然调用 const 成员函数却造成对象状态被更改"。 通常我们认为,对象的"内部"就是指它的成员变量,但其实不被公开使用的 成员函数(也就是被声明为 protected 或 private 者)也是对象"内部"的一部分。 因此也应该留心不要返回它们的 handles 。这意味你绝对不该令成员函数返回一个指 针指向"访问级别较低"的成员函数。如果你那么做,后者的实际访问级别就会提 高如同前者(访问级别较高者) ,因为客户可以取得→个指针指向那个"访问级别 较低"的函数,然后通过那个指针调用它。 然而"返回指针指向某个成员函数"的情况毕竟不多见,所以让我们把注意力 收回,专注于 Rectangle class 和它的 upperLeft 以及 lowerRight 成员函数。我们 在这些函数身上遭遇的两个问题可以轻松去除,只要对它们的返回类型加上 const 即可 z {e、iqdna←」chus· 工 slh 由cp· const Point& upperLeft( ) const { return pData->ulhc; } const Point& lowerRight( ) const { return pData->lrhc; } 有了这样的改变,客户可以读取矩形的 Points ,但不能涂写它们。这意味当初 声明 upperLeft 和 upperRight 为 const 不再是个谎言,因为它们不再允许客户更 改对象状态。至于封装问题,我们总是愿意让客户看到 Rectangle 的外围 Points , 所以这里是蓄意放松封装。更重要的是这是个有限度的放松 z 这些函数只让渡读取 权。涂写权仍然是被禁止的。 但即使如此, upperLeft 和 lowerRight 还是返回了"代表对象内部"的 handles , 有可能在其他场合带来问题。更明确地说,它可能导致 dangling handles ( 空悬的号 Effective C++ 中文版,第三版126 5 实现 码牌) :这种 handles 所指东西(的所属对象〉不复存在。这种"不复存在的对象" 最常见的来源就是函数返回值。例如某个函数返回 GUI 对象的外框 (bounding box), 这个外框采用矩形形式: class GUIObject { ... }; const Rectangle II 以 by value 方式返回一个矩形 boundingBox(const GUIObject& obj); II条款 3 谈过为什么返回类型是const 现在,客户有可能这么使用这个函数: GUIObject* pgo; II让 pgo 指向某个GUIObject const Point* pUpperLeft = II 取得一个指针指向外框左上点 &(boundingBox(*pgo) .upperLeft()); 对 boundingBox的调用获得一个新的、暂时的Recta呵le 对象。这个对象没有 名称,所以我们权且称它为tempo 随后 upperLeft 作用于 temp 身上,返回一个 reference 指向 temp 的一个内部成分,更具体地说是指向一个用以标示temp 的 Points。于是 pUpperLeft指向那个 Point 对象。目前为止一切还好,但故事尚未 结束,因为在那个语句结束之后,boundingBox的返回值,也就是我们所说的temp , 将被销毁,而那间接导致temp 内的 Points 析构。最终导致pUpperLeft指向一个 不再存在的对象:也就是说一旦产出pUpperLeft的那个语句结束, pUpperLeft也 就变成空悬、虚吊 (dangling) ! 这就是为什么函数如果"返回一个 handle 代表对象内部成分"总是危险的原因。 不论这所谓的 handle 是个指针或选代器或 reference ,也不论这个 handle 是否为 const ,也不论那个返回 handle 的成员函数是否为 const 。这里的唯一关键是,有 个 handle 被传出去了,一旦如此你就是暴露在 "handle 比其所指对象更长寿"的风 险下。 这并不意味你绝对不可以让成员函数返回 handle 。有时候你必须那么做。例如 operator[] 就允许你"摘采" strings 和 vectors 的个别元素,而这些 operator[]s 就是返回 references 指向"容器内的数据" (见条款 3) ,那些数据会随着容器被 销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。 请记住 ·避免返回 handles (包括 references 、指针、迭代器)指向对象内部。遵守这个条 款可增加封装性,帮助 const 成员函数的行为像个 const ,并将发生"虚吊号码 牌" (dangling handles) 的可能性降至最低。 Effective C++ 中文版F 第三版5 条款 29: 为"异常安全"而努力是值得的 条款 29: 为"异常安全"而努刀是值得的 S位ive for exception-safe code. 127 异常安全性 (Exception safety) 有几分像是......昵......怀孕。但等等,在我们 完成求偶之前,实在无法确实地谈论生育。 假设有个 class 用来表现夹带背景图案的 QUI 菜单单。这个 class 希望用于多钱 程环境,所以它有个互斥器 (mutex) 作为并发控制 (concurrency control) 之用: class Prett抖1enu { public: void changeBackground(std::istrem口& imgSrc); II改变背景图像 private: Mutex mutex; Image* bgImage; int imageChanges; II互斥器 II 目前的背景图像 II背景图像被改变的次数 下面是 Prett如enu 的 changeBackground函数的一个可能实现: void Prett如1enu::changeBackground(std::istream&imgSrc) lock(&mutex) ; delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); unlock(&mutex); II取得互斥器(见条款14) II摆脱旧的背景图像 II修改图像变更次数 II安装新的背景图像 II释放互斥器 从"异常安全性"的观点来看,这个函数很糟。"异常安全"有两个条件,而 这个函数没有满足其中任何一个条件。 当异常被抛出时,带有异常安全性的函数会: ·不泄漏任何资源。上述代码没有做到这-点,因为一旦"newImage(imgSrc)" 导 致异常,对 unlock 的调用就绝不会执行,于是互斥器就永远被把持住了。 ·不允许数据败坏。如果"new Image (imgSrc)" 抛出异常, bgImage就是指向一 个己被删除的对象, imageChanges也己被累加,而其实并没有新的图像被成功 安装起来。(但从另一个角度说,旧图像已被消除,所以你可能会争辩说图像 El知ctive C++中文版,第三版128 5 实现 还是"改变了" )。 解决资源泄漏的问题很容易,因为条款 13 讨论过如何以对象管理资源,而条 款 14 也导入了 Lock class 作为一种"确保互斥器被及时释放"的方法: void PrettyMenu::changeBackground(std::istream& imgSrc) Lock ml(&mutex); II来自条款 14: 获得互斥器并确保它稍后被释放 delete bgImage; ++imageChanges; bgImage = new Image(imgSrc); 关于"资源管理类" (resource management classes) 如 Lock 者,一个最棒的事 情是,它们通常使函数更短。你看,不再需要调用 unlock 了不是吗?有个一般性 规则是这么说的:较少的码就是较好的码,因为出错机会比较少,而且一旦有所改 变,被误解的机会也比较少。 把资源泄漏抛诸脑后,现在我们可以专注解决数据的败坏了。此刻我们需要做 个抉择,但是在我们能够抉择之前,必须先面对一些用来定义选项的术语。 异常安全函数 (Exception-safe functions) 提供以下三个保证之-. ·基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有 任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态 〈例如所有的 class 约束条件都继续获得满足)。然而程序的现实状态 (exact state) 恐怕不可预料。举个例子,我们可以撰写 changeBackground使得一旦有 异常被抛出时, PrettyMenu 对象可以继续拥有原背景图像,或是令它拥有某个 缺省背景图像,但客户无法预期哪一种情况。如果想知道,他们恐怕必须调用 某个成员函数以得知当时的背景图像是什么。 ·强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认 知:如果函数成功,就是完全成功,如果函数失败,程序会回复到"调用函数 之前"的状态。 El知ctive C++ 中文版,第三版5 条款 29: 为"异常安全"而努力是值得的 129 和这种提供强烈保证的函数共事,比和刚才说的那种只提供基本承诺的函 数共事,容易多了,因为在调用一个提供强烈保证的函数后,程序状态只有两 种可能:如预期般地到达函数成功执行后的状态,或回到函数被调用前的状态。 与此成对比的是,如果调用→个只提供基本承诺的函数,而真的出现异常,程 序有可能处于任何状态一一只要那是个合法状态。 ·不抛掷 (no也row) 保证,承诺绝不抛出异常,因为它们总是能够完成它们原先 承诺的功能。作用于内置类型(例如 ints ,指针等等)身上的所有操作都提供 nothrow 保证。这是异常安全码中一个必不可少的关键基础材料。 如果我们假设,函数带着"空白的异常明细" (empty exception specification) 者必为 nothrow 函数,似乎合情合理,其实不尽然。举个例子,考虑以下函数z int doSomething() throw(); II注意"空白的异常明细" II (empty exception 吨JeC) 这并不是说 doSomething 绝不会抛出异常,而是说如果 doSomething 抛出 异常,将是严重错误,会有你意想不到的函数被调用'。实际上 doSometl山19 也 许完全没有提供任何异常保证。函数的声明式(包括其异常明细一一如果有的 话)并不能够告诉你是否它是正确的、可移植的或高效的,也不能够告诉你它 是否提供任何异常安全性保证。所有那些性质都由函数的实现决定,无关乎声 明。 异常安全码 (Exception-safe code) 必须提供上述三种保证之一。如果它不这样 做,它就不具备异常安全性。因此,我们的抉择是,该为我们所写的每一个函数提 供哪一种保证?除非面对不具异常安全性的传统代码(我将在本条款末尾讨论那种 情况) ,否则你应该只在一种情况下才不提供任何异常安全保证 z 你那"天才班" 需求分析团队确认你的应用程序有"泄漏资源"并"在执行过程中带着败坏数据" 的需要。 一般而言你应该会想提供可实施之最强烈保证。从异常安全性的观点视之, no伽ow 函数很棒,但我们很难在 Cpa民 ofC++ 领域中完全没有调用任何一个可能 抛出异常的函数。任何使用动态内存的东西(例如所有 STL 容器)如果无法找到足 '关于所谓"意想不到的函数",请咨询你最常用的搜索引擎或广泛的 e++ 文件。搜 寻 set u阻碍ected 或许会得到较好的结果:此函数用来指定那个"意想不到的函数"。 Effective C++ 中文版,第三版130 5 实现 够内存以满足需求,通常便会抛出一个 bad alloc 异常(见条款 49) 。是的,可 能的话请提供 nothrow 保证,但对大部分函数而言,抉择往往落在基本保证和强烈 保证之间。 对 changeBackground 而言,提供强烈保证几乎不困难。首先改变 Prett yMenu 的 bgImage 成员变量的类型,从一个类型为 Image 食的内置指针改为一个"用于资 源管理"的智能指针(见条款 13) 。坦白说,这个好构想纯粹只是帮助我们防止资 源泄漏。它对"强烈之异常安全保证"的帮助仅仅只是强化了条款 13 的论点:以 对象(例如智能指针)管理资源是良好设计的根本。以下代码中我使用 trl: :sharedytr,因为它比 autoytr更直观的行为使它更受欢迎。 第二,我们重新排列changeBackground内的语句次序,使得在更换图像之后 才累加 imageChanges。一般而言这是个好策略:不要为了表示某件事情发生而改 变对象状态,除非那件事情真的发生了。 下面是结果: class PrettyMenu { std::trl::sharedytr bgImage; void PrettyMenu::changeBackground(std::istream& imgSrc) Lock ml (&mutex) ; bgImage.reset( 口ew Image(imgSrc)); II 以"new 工mage" 的执行结果 II设定 bgImage 内部指针 ++imageChanges; 注意,这里不再需要手动delete 旧图像,因为这个动作已经由智能指针内部 处理掉了。此外,删除动作只发生在新图像被成功创建之后。更正确地说,trl:: sharedytr::reset函数只有在其参数(也就是"newImage(imgSrc)"的执行结果) 被成功生成之后才会被调用。delete 只在 reset 函数内被使用,所以如果从未进 入那个函数也就绝对不会使用delete。也请注意,以对象(trl::sharedytr) 管 理资源(这里是动态分配而得的Image) 再次缩减了 changeBackground的长度。 如我稍早所言,这两个改变几乎足够让changeBackground提供强烈的异常安 全保证。美中不足的是参数imgSrc。如果 Image 构造函数抛出异常,有可能输入 E}知ctive C++中文版,第三版5 条款 29: 为"异常安全"而努力是值得的 131 流(i nput stream) 的读取记号 (read marker) 已被移走,而这样的搬移对程序其余 部分是一种可见的状态改变。所以 changeBackground 在解决这个问题之前只提供 基本的异常安全保证。 然而,让我们把它放在一旁,佯装 changeBackground 的确提供了强烈保证(我 有信心你可以想出个什么办法顺利过渡,或许你可以改变它的参数类型,从 istrearn 改为一个内含图像数据的文件名称)。有个一般化的设计策略很典型地会导致强烈 保证,很值得熟悉它。这个策略被称为 copy and swap。原则很简单:为你打算修 改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修 改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的 那个副本和原对象在一个不抛出异常的操作中置换(swap) 。 实现上通常是将所有"隶属对象的数据"从原对象放进另一个对象内,然后赋 予原对象一个指针,指向那个所谓的实现对象(implementationobject,即副本)。 这种手法常被称为pimpl idiom ,条款 31 详细描述了它。对PrettyMenu而言,典型 写法如下2 struct PMImpl { IlpM工mpl = "Pre即Menu Impl"; std::trl::shared_ptr bgImage; II稍后说明为什么它是个struct int imageChanges; class Prett抖1enu ( private: Mutex mutex; std::trl::shared-rtr pImpl; void PrettyMe口u::changeBackground(std::istrearn&imgSrc) using std::swap; II见条款 25 Lock ml(&mutex); II获得 mutex 的副本数据 std::trl::shared-ptr pNew(new PMImpl(*p工mpl)); pNew->bgImage.reset(new Image(却\gSrc)); /I修改副本 ++pNew->imageChanges; swap(pImpl, pNew); II置换 (swap) 数据,释放mutex Ej知ctive C++中文版,第三版132 5 实现 此例之中我选择让 PMlmpl 成为一个 struct 而不是一个 class. 这是因为 Prett 严伦阳的数据封装性已经由于 '''plmpl 是 private"而获得了保证。如果令 PMlmpl 为一个 class. 虽然一样好,有时候却不太方便(但也保持了面向对象纯度)。如果 你要,也可以将 PMlmpl 嵌套于 Prett 如enu 内,但打包问题( packaging. 例如"独 立撰写异常安全码" )是我们这里所挂虑的事。 "copy-and-swap" 策略是对对象状态做出"全有或全无"改变的一个很好办法, 但一般而言它并不保证整个函数有强烈的异常安全性。为了解原因,让我们考虑 changeBackground 的一个抽象概念: some Func 。它使用 copy-and-swap 策略,但函 数内还包括对另外两个函数目和口的调用 z void someFunc () II对 local 状态做一份副本 fl () ; f2 (); II将修改后的状态置换过来 很显然,如果 fl 或口的异常安全性比"强烈保证"低,就很难让someFunc 成为"强烈异常安全"。举个例子,假设fl 只提供基本保证,那么为了让someFunc 提供强烈保证,我们必须写出代码获得调用fl 之前的整个程序状态、捕捉fl 的所 有可能异常、然后恢复原状态。 如果 fl 和 f2 都是"强烈异常安全",情况并不就此好转。毕竟如果n 圆满 结束,程序状态在任何方面都可能有所改变,因此如果f2 随后抛出异常,程序状 态和 someFunc 被调用前并不相同,甚至当f2 没有改变任何东西时也是如此。 问题出在"连带影响"(side effects) 。如果函数只操作局部性状态 inline const T& std::max(const T& at const T& b) { return a < b ? b : a; } II 明确申请 inline: Ilstd: :max 之前有 II关键字"inline" "max 是个 template" 带出了一项观察结果:我们发现inline 函数和 templates 两者通常都被定义于头文件内。这使得某些程序员以为function templates -定必须 是 inline 。这个结论不但无效而且可能有害,值得深入看一看。 Inline 函数通常一定被置于头文件内,因为大多数建置环境(build environments) 在编译过程中进行 inlining ,而为了将一个"函数调用"替换为"被调用函数的本 体",编译器必须知道那个函数长什么样子。某些建置环境可以在连接期完成 inlining ,少量建置环境如基于 .NET CLI (Common Language Infrastructure; 公共语 言基础设施)的托管环境 (managed environments) 竟可在运行期完成 inlining 。然 而这样的环境毕竟是例外,不是通例。 Inlining 在大多数 C++ 程序中是编译期行为。 Effective C++ 中文版,第三版136 5 实现 Templates 通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现 化,需要知道它长什么样子。(这其实也不是世界一统的准则。某些建置环境可以 在连接期才执行 template 具现化。只不过编译期完成具现化动作比较常见。) Template 的具现化与 inlining 无关。如果你正在写一个 template 而你认为所有 根据此 template 具现出来的函数都应该 inlined ,请将此 template 声明为工 nli口e; 这 就是上述 std: :max 代码的作为。但如果你写的 template 没有理由要求它所具现的 每-个函数都是 inlined ,就应该避免将这个 template 声明为 inline (不论显式或隐 式) 0 Inlining 需要成本,你不会想在没有事先考虑的情况下就招来那些成本吧。 我已经提过 inlining 如何引发代码膨胀(这对 template 作者特别重要,见条款 44) , 但还存在其他成本,稍后再讨论。 现在让我们先结束" inline 是个申请,编译器可加以忽略"的观察。大部分编 译器拒绝将太过复杂(例如带有循环或递归)的函数 inlining ,而所有对 virtual 函 数的调用(除非是最平淡无奇的)也都会使 inlining 落空。这不该令你惊讶,因为 virtual 意味"等待,直到运行期才确定调用哪个函数 'p ,而 inline 意味"执行 前,先将满用动作替换为被调用函数的本体"。如果编译器不知道该调用哪个函数, 你就很难责备它们拒绝将函数本体 inlining 。 这些叙述整合起来的意思就是:一个表面上看似 inline 的函数是否真是 inline , 取决于你的建置环境,主要取决于编译器。幸运的是大多数编译器提供了一个诊断 级别:如果它们无法将你要求的函数 inline 化,会给你→个警告信息(见条款 53) 。 有时候虽然编译器有意愿 inlining 某个函数,还是可能为该函数生成一个函数 本体。举个例子,如果程序要取某个 inline 函数的地址,编译器通常必须为此函数 生成一个 outlined 函数本体。毕竟编译器哪有能力提出一个指针指向并不存在的函 数昵?与此并提的是,编译器通常不对"通过函数指针而进行的调用"实施 inlining , 这意味对 inline 函数的调用有可能被 inlined ,也可能不被 inlined ,取决于该调用的 实施方式 z inline void f( ) {...} II假设编译器有意愿inline "对 f 的调用" void ( * pf )()‘= f; Ilpf 指向 f f ( ); pf(); Effective C++ 中文版,第三版 II 这个调用将被 inlin时,因为它是一个正常调用。 II 这个调用或许不被 inlined ,因为它通过函数指针达成。5 条款 30: 透彻了解 inlining 的里里外外 137 即使你从未使用函数指针, "未被成功 inlined" 的 inline 函数还是有可能缠住 你,因为程序员并非唯一要求函数指针的人。有时候编译器会生成构造函数和析构 函数的 outline 副本,如此一来它们就可以获得指针指向那些函数,在 array 内部元 素的构造和析构过程中使用。 实际上构造函数和析构函数往往是 inlining 的糟糕候选人一一虽然漫不经心的 情况下你不会这么认为。考虑以下De rivedclass 构造函数 z class Base { public: private: std::string brol, brn2; Ilbase 成员 1 和 2 class Der工ved: public Base { public: Derived() {} II De rived 构造函数是空的,哦,是吗? private: std::string drnl , drn2, drn3; Ilderived 成员 1 一 3 这个构造函数看起来是 inlining 的绝佳候选人,因为它根本不含任何代码。但 是你的眼睛可能会欺骗你。 C++ 对于"对象被创建和被销毁时发生什么事"做了各式各样的保证。当你使 用 new. 动态创建的对象被其构造函数自动初始化:当你使用 delete. 对应的析 构函数会被调用。当你创建一个对象,其每一个 base class 及每一个成员变量都会 被自动构造:当你销毁一个对象,反向程序的析构行为亦会自动发生。如果有个异 常在对象构造期间被抛出,该对象己构造好的那一部分会被自动销毁。在这些情况 中 C++ 描述了什么一定会发生,但没有说如何发生。"事情如何发生"是编译器 实现者的权责,不过至少有一点很清楚,那就是它们不可能凭空发生。你的程序内 一定有某些代码让那些事情发生,而那些代码一一由编译器于编译期间代为产生并 安插到你的程序中的代码一一肯定存在于某个地方。有时候就放在你的构造函数和 Effective C++ 中文版,第三版138 5 实现 析构函数内,所以我们可以想象,编译器为稍早说的那个表面上看起来为空的 De rived 构造函数所产生的代码,相当于以下所列: Derived: :Derived() II" 雪白De rived 构造函数"的观念性实现 Base: :Base () ; try { dml.std::string::string(); } catch (...) { Base: :-Base () ; throw; try { dm2.std::string::string(); } catch(...) { dml.std::string::-string( ); Base: : -Base () ; throw; try { dm3.std::str 工口g: : string (); } catch (...) { dm2.std::string::-string( ); dml.std::string::-string( ); Base: : -Base () ; throw; II初始化 "Base 成分" II试图构造 dmlo II如果抛出异常就 II销毁 base class 成分,并 II传播该异常。 II试图构造dm20 II如果抛出异常就 II销毁 dml , II销毁 base class 成分,并 II传播该异常。 II试图构造 dm3 。 II如果抛出异常就 II销毁dm2, II销毁 dml , II销毁 base class 成分,并 II传播该异常。 这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精 致复杂的做法来处理异常。尽管如此,这己能准确反映Derived的空白构造函数必 须提供的行为。不论编译器在其内所做的异常处理多么精致复杂,Derived构造函 数至少一定会陆续调用其成员变量和base class 两者的构造函数,而那些调用(它 们自身也可能被inlined) 会影响编译器是否对此空白函数inliningo 相同理由也适用于Base 构造函数,所以如果它被inlined,所有替换 "Base 构 造函数调用"而插入的代码也都会被插入到"Derived 构造函数调用"内〈因为 Derived构造函数调用了 Base 构造函数)。如果string构造函数恰巧也被inlined, Derived 构造函数将获得五份"string 构造函数代码"副本,每一份副本对应于 Derived对象内的五个字符串(两个来自继承,三个来自自己的声明)之一。现在 或许很清楚了, "是否将 Derived构造函数 inline 化"并非是个轻松的决定。类似 思考也适用于Derived析构函数,在那儿我们必须看到"被Derived构造函数初始 化的所有对象"被一一销毁,无论以哪种方式进行。 El知ctive C++中文版,第二版5 条款 30: 透彻了解 inlining 的里里外外 139 程序库设计者必须评估"将函数声明为 inline" 的冲击: inline 函数无法随着程 序库的升级而升级。换句话说如果 f 是程序库内的一个 inline 函数,客户将 "f 函 数本体"编进其程序中,一旦程序库设计者决定改变 f ,所有用到 f 的客户端程序 都必须重新编译。这往往是大家不愿意见到的。然而如果 f 是 non-inline 函数,一 旦它有任何修改,客户端只需重新连接就好,远比重新编译的负担少很多。如果程 序库采取动态连接,升级版函数甚至可以不知不觉地被应用程序吸纳。 对程序开发而言,将上述所有考虑牢记在心很是重要,但若从纯粹实用观点出 发,有→个事实比其他因素更重要 z 大部分调试器面对 inline 函数都束手无策。这 对你应该不是太大的意外,毕竟你如何在一个并不存在的函数内设立断点 (break point)呢?虽然某些建置环境勉力支持对 inlined 函数的调试,其他许多建置环境 仅仅只能"在调试版程序中禁止发生 inlining" 。 这使我们在决定哪些函数该被声明为 inline 而哪些函数不该时,掌握一个合乎 逻辑的策略。一开始先不要将任何函数声明为 inline ,或至少将 inlining 施行范围局 限在那些"一定成为 inline" (见条款 46) 或"十分平淡无奇" (例如 p.135 Person: : age) 的函数身上。慎重使用 inline 便是对日后使用调试器带来帮助,不 过这么一来也等于把自己推向手工最优化之路。不要忘记 80-20 经验法则 z 平均而 言一个程序往往将 80 奄的执行时间花费在 20 屯的代码上头。这是一个重要的法则, 因为它提醒你,作为一个软件开发者,你的目标是找出这可以有效增进程序整体效 率的 20 毛代码,然后将它 inline 或竭尽所能地将它瘦身。但除非你选对目标,否则 →切都是虚功。 请记住 ·将大多数 inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程 和二进制升级 (binary upgradability) 更容易,也可使潜在的代码膨胀问题最小 化,使程序的速度提升机会最大化。 ·不要只因为 function templates 出现在头文件,就将它们声明为 inlineo El知ctive C++ 中文版,第三版140 条款 31: 将文件阔的编译依存关系降至最低 Minimize compilation dependencies between files. 5 实现 假设你对 C++ 程序的某个 class 实现文件做了些轻微修改。注意,修改的不是 class 接口,而是实现,而且只改private 成分。然后重新建置这个程序,并预计只 花数秒就好。毕竟只有一个class 被修改。你按下 "Build" 按钮或键入 make (或其 他类似命令) ,然后大吃一惊,然后感到窘困,因为你意识到整个世界都被重新编 译和连接了!当这种事情发生,难道你不气恼吗? 问题出在 C++ 并没有把"将接口从实现中分离"这事做得很好。Class 的定义 式不只详细叙述了 class 接口,还包括十足的实现细目。例如: class Person { public: Person(const std::string& name, cpnst Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; private: std::string theName; Date theB工 rthDate; Address theAddress; II实现细目 II实现细目 II实现细目 这里的 class Person无法通过编译一一如果编译器没有取得其实现代码所用到 的 classes string, Date 和 Address 的定义式。这样的定义式通常由#include 指示 符提供,所以 Person 定义文件的最上方很可能存在这样的东西 g #include #include "date.h" #include "address.h" 不幸的是,这么一来便是在Person 定义文件和其含入文件之间形成了一种编 译依存关系 (compilationdependency) 。如果这些头文件中有任何一个被改变,或 这些头文件所倚赖的其他头文件有任何改变,那么每一个含入Person class 的文件 就得重新编译,任何使用Person class 的文件也必须重新编译。这样的连串编译依 存关系(cascading compilation dependencies) 会对许多项目造成难以形容的灾难。 E1知c伽eC++中文版,第三版5 条款 31 :将文件间的编译依存关系降至最低 141 你或许会奇怪,为什么 C++ 坚持将 class 的实现细目置于 class 定义式中?为 什么不这样定义 Person ,将实现细目分开叙述? const Date& birthday, II前置声明(不正确,详下) II II前置声明 II前置声明 class Date; class Address; class Person { public: Person(canst std::string& name, canst Address& addr); std::string name() const; std::string birthDate() canst; std::string address() const; namespace std { class string; 如果可以那么做, Person 的客户就只需要在Person接口被修改过时才重新编 译。 这个想法存在两个问题。第一, string不是个 class,它是个 typedef (定义为 basic string #include class Personlmpl; class Date; class Address; II标准程序库组件不该被前置声明。 II此乃为了 trl:: shared_ptr而含入:详后。 IIPerson实现类的前置声明。 IIPerson接口用到的classes 的前置声明。 class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; private: std::trl::shared ptr plmpl; II指针,指向实现物: II叫.,位1::sh缸ed---.p仕见条款 13. 在这里,main classC Person) 只内含一个指针成员(这里使用 trl: :sharedytr, 见条款 13) ,指向其实现类 C personlmpl) 。这般设计常被称为 pimpl idiom (pimpl R伊ctive C++中文版,第三版5 条款 31 :将文件间的编译依存关系降至最低 143 是 "pointer to implementation" 的缩写)。这种 classes 内的指针名称往往就是 plrnpl ,就像上面代码那样。 这样的设计之下, Person 的客户就完全与 Dates , Addresses 以及 Persons 的实 现细目分离了。那些 classes 的任何实现修改都不需要 Person 客户端重新编译。此 外由于客户无法看到 Person 的实现细目,也就不可能写出什么"取决于那些细目" 的代码。这真正是"接口与实现分离" ! 这个分离的关键在于以"声明的依存性"替换"定义的依存性",那正是编译 依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其 他文件内的声明式(而非定义式)相依。其他每一件事都源自于这个简单的设计策 略: 圃如果使用 object references 或 0时 ect pointers 可以完成任务,就不要使用 。同ects 。你可以只靠一个类型声明式就定义出指向该类型的 references 和 pointers; 但如果定义某类型的 objects ,就需要用到该类型的定义式。 ·如果能够,尽量以 class 声明式替换 class 定义式。注意,当你声明一个函数而 它用到某个 class 时,你并不需要该 class 的定义:纵使函数以 byvalue 方式传 递该类型的参数(或返回值)亦然: class Date; Ilclass 声明式。 Date today(); II没问题一这里并不需要 void clearAppointrnents(Date 由 ; II Date 的定义式。 当然 , pass-by-value 一般而言是个糟糕的主意(见条款 20) ,但如果你发 现因为某种因素被迫使用它,并不能够就此为"非必要之编译依存关系"导入 正当性。 声明 today 函数和 clearAppointrnents 函数而无需定义 Date ,这种能力可 能会令你惊讶,但它并不是真的那么神奇。→旦任何人调用那些函数,调用之 前 Date 定义式一定得先曝光才行。那么或许你会纳闷,何必费心声明一个没人 调用的函数呢?嗯,并非没人调用,而是并非每个人都调用。假设你有一个函 数库内含数百个函数声明,不太可能每个客户叫遍每一个函数。如果能够将"提 供 class 定义式" (通过#include 完成)的义务从"函数声明所在"之头文 件移转到"内含函数调用"之客户文件,便可将"并非真正必要之类型定义" 与客户端之间的编译依存性去除掉。 E1知ctive C++ 中文版,第三版144 5 实现 ·为声明式和定义式提供不同的头文件。为了促进严守上述准则,需要两个头文 件,一个用于声明式,一个用于定义式。当然,这些文件必须保持一致性,如 果有个声明式被改变了,两个文件都得改变。因此程序库客户应该总是 #include 一个声明文件而非前置声明若干函数,程序库作者也应该提供这两个头文件。 举个例子, Date 的客户如果希望声明 today 和 clearAppoir 巾 T 刷出,他们不该 像先前那样以手工方式前置声明 Date ,而是应该#i nclude 适当的、内含声明 式的头文件: #include "datefwd.h" II这个头文件内声明(但未定义)class Date 。 Date today( ); II 同前。 void clearAppointments(Date d); 只含声明式的那个头文件名为"datefwd.h",命名方式取法C++ 标准程序 库头文件(见条款54) 的 o 内含 iostream 各组件的声明式, 其对应定义则分布在若干不同的头文件内,包括, 深具启发意义的另一个原因是,它分外彰显"本条款适用于 templates 也适用于 non-templates" 。虽然条款 30 说过,在许多建置环境 (build environments) 中 template 定义式通常被置于头文件内,但也有某些建置环境允 许 template 定义式放在"非头文件"内,这么一来就可以将"只含声明式"的 头文件提供给 templateso 就是这样→份头文件。 C+十也提供关键字 e碍。此,允许将 template 声明式和 template 定义式分割 于不同的文件内。不幸的是支持这个关键字的编译器目前非常少,因此现实中 使用这个关键字的经验也非常少。目前若要评论 export 在高效 C++ 编程中扮 演什么角色,恐怕言之过早。 像 Person 这样使用 pimpl idiom 的 classes ,往往被称为 Handle classes 。也许 你会纳闷,这样的 classes 如何真正做点事情。办法之一是将它们的所有函数转交给 相应的实现类(implementation classes) 并由后者完成实际工作。例如下面是 Person 两个成员函数的实现 z #include "Person.h" Effective C++ 中文版,第三版 II 我们正在实现 Person cl脯, II 所以必须#i nclude 其 class 定义式。5 条款 31 :将文件间的编译依存关系降至最低 #include "Perso口 Impl.h" II我们也必须#include PersonImpl的 II class 定义式,否则无法调用其成员函数: II注意, PersonImpl 有着和 Person II 完全相同的成员函数,两者接口完全相同。 Person::Person(const std::string& name , const Date& birthday, const Address& addr) : pImpl( 口ew Perso口 Impl(name , birthday, addr)) {} std:;string Person;;name() const return p工mpl->name( ); 145 请注意, Person 构造函数以 new (见条款 16) 调用 PersonImpl 构造函数,以 及 Person: : name 函数内调用 PersonI即 1: :name 。这是重要的,让 Person 变成一 个 Handle class 并不会改变它做的事,只会改变它做事的方法。 另一个制作 Handle class 的办法是,令 Person 成为一种特殊的 abstract base class (抽象基类) ,称为 Interface class. 这种 class 的目的是详细一一描述derived classes 的接口(见条款 34) ,因此它通常不带成员变量,也没有构造函数,只有一 个 virtual 析构函数(见条款 7) 以及一组 pure virtual 函数,用来叙述整个接口。 Interface classes 类似 Java 和 .NET 的 Interfaces,但 C++ 的 Interface classes 并不需要负担Java 和 .NET 的 Interface 所需负担的责任。举个例子,Java 和 .NET 都不允许在 Interfaces 内实现成员变量或成员函数,但C++ 不禁止这两样东西。C++ 这种更为巨大的弹性有其用途,因为→如条款36 所言, "non4i阳al 函数的实现" 对继承体系内所有classes 都应该相同,所以将此等函数实现为Interface class (其 中写有相应声明)的→部分也是合理的。 一个针对 Person 而写的 Interface class 或许看起来像这样: class Person { public: virtual -Person(); virtual std::string name() const = 0; virtual std::string birthDate() canst = 0; virtual std::string address() const = 0; Ej知cfive C++中文版F 第三版146 5 实现 这个 class 的客户必须以 Person 的 pointers 和 references 来撰写应用程序,因 为它不可能针对"内含 pure virtual 函数"的 Person classes 具现出实体。(然而却 有可能对派生自 Person 的 classes 具现出实体,详下。)就像 Handle classes 的客 户一样,除非 Inte白ce class 的接口被修改否则其客户不需重新编译。 Inte 斤'ace class 的客户必须有办法为这种 class 创建新对象。他们通常调用一个 特殊函数,此函数扮横"真正将被具现化"的那个 derived classes 的构造函数角色。 这样的函数通常称为 factory (工厂)函数(见条款 13) 或 vi 民ual 构造函数。它们 返回指针(或更为可取的智能指针,见条款 18) ,指向动态分配所得对象,而该对 象支持 Interface class 的接口。这样的函数又往往在 Inte 巾ce class 内被声明为 static: class Person { public: static std::trl::shared_ptr II返回一个 trl:: sharedytr,指向 create(const std::string& name, II 一个新的 Perso口,并以给定之参数 const Date& birthday, II 初始化。条款 18 告诉你 const Address& addr); II为什么返回的是trl: : shared ptr 客户会这样使用它们: std::string name; Date dateOfBirth; Address address; II创建一个对象,支持Person接口 std::trl::shared_ptr PP(Person::create(name, dateOfBirth, address)) ; std::cout « pp->n缸ne () «" was born on " « pp->birthDate() «" and now lives at " « pp->address( ); II通过 Perso口的接口使用这个对象 II 当 pp 离开作用域, II对象会被自动删除, II见条款 13 。 当然,支持 Interface class 接口的那个具象类 (concrete class巳s) 必须被定义出 来,而且真正的构造函数必须被调用。二切都在vi同ual 构造函数实现码所在的文件 El知ctive C++中文版F 第三版5 条款 31: 将文件间的编译依存关系降至最低 147 内秘密发生。假设 Interface class Person有个具象的 derived class RealPerson,后 者提供继承而来的virtual 函数的实现z class RealPerson: public Person { public: RealPerson(const std::string& name, const Date& birthday, const Address& addr) theName(name) , theBirthDate(birthday) , theAddress(addr) {} virtual -RealPerson() {) std::string name() const; std::string birthDate() const; std::string address() const; private: std::string theName; Date theBirthDate; Address theAddress; II这些函数的实现码并不显示于此, II但它们很容易想象。 有了 RealPerson之后,写出 Person::create就真的-点也不稀奇了z std::trl::shared ptr Person::create(const std::string& name, canst Date& birthday, const Address& addr) return std::trl::shared-ptr(口ew RealPerson(name, birthday, addr)) ; 一个更现实的 Person::create 实现代码会创建不同类型的 derived class 对象, 取决于诸如额外参数值、读自文件或数据库的数据、环境变量等等。 RealPerson 示范实现 Inte巾ce class 的两个最常见机制之一:从 Interface class (Person) 继承接口规格,然后实现出接口所覆盖的函数。Inte巾ce class 的第二 个实现法涉及多重继承,那是条款40 探索的主题。 Handle classes 和 Inte巾ce classes 解除了接口和实现之间的藕合关系,从而降 低文件间的编译依存性(ωmpilation dependencies) 。如果你是犬儒学派(译注2 犬儒学派希望过一种符合自然的简朴生活,槟弃一切社会习俗和人为引导的种种欲 望) ,我知道你正等着我有义务给出的旁注。"所有这些戏法得付出多少代价?" 你咕哝着。答案是计算器科学中通常需要付出的那些:它使你在运行期丧失若干速 度,又让你为每个对象超额付出若干内存。 Effective C++ 中文版 F 第三版148 5 实现 在 Handle classes 身上,成员函数必须通过 implementation pointer 取得对象数 据。那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加 implementation ppinter 的大小。最后. implementation pointer 必须初始化(在 Handle class 构造函数内) ,指向一个动态分配得来的 implementation object,所以你将蒙 受因动态内存分配(及其后的释放动作)而来的额外开销,以及遭遇bad alloe 异 常(内存不足)的可能性。 至于 Interface classes ,由于每个函数都是 virtual ,所以你必须为每次函数调用 付出一个间接跳跃 (indirect jump) 成本(见条款 7) 。此外 Interface class 派生的 对象必须内含一个 vp位 (virtual table pointer. 再次见条款 7) .这个指针可能会增 加存放对象所需的内存数量一一实际取决于这个对象除了 Interface class 之外是否 还有其他 virtual 函数来源。 最后,不论 Handle classes 或 Interface classes. 一旦脱离 inline 函数都无法有 太大作为。条款 30 解释过为什么函数本体为了被 inlined 必须(恨典型地)置于头 文件内,但 Handle classes 和 Interface classes 正是特别被设计用来隐藏实现细节如 函数本体。 然而,如果只因为若干额外成本便不考虑 Handle classes 和 Interface classes, 将是严重的错误。 Vi阳al 函数不也带来成本吗?你并不会想要弃绝它们对不对? (如果是的话,那你读错书了。)你应该考虑以渐进方式使用这些技术。在程序发 展过程中使用 Handle classes 和 Interface classes 以求实现码有所变化时对其客户带 来最小冲击。而当它们导致速度和/或大小差异过于重大以至于 classes 之间的稿合 相形之下不成为关键时,就以具象类 (concrete classes) 替换 Handle classes 和 Interface classes 。 请记住 ·支持"编译依存性最小化"的→般构想是:相依于声明式,不要相依于定义式。 基于此构想的两个手段是 Handle classes 和 Interface classes 。 ·程序库头文件应该以"完全且仅有声明式" (full and declaration-only forms) 的 形式存在。这种做法不论是否涉及templates 都适用。 Effective C++ 中文版,第三版6 条款 31: 将文件间的编译依存关系降至最低 149 6 继承与面向对象设计 Inheritance and 0问ect-OrientedDesign 面向对象编程 (OOP) 几乎已经风靡两个年代了,所以关于继承、派生、vi阳al 函数等等,可能你已经有了一些经验。纵使你过去只以C 编写程序,如今肯定也无 法逃脱 OOP 的笼罩。 尽管如此, C++ 的 OOP 有可能和你原本习惯的OOP 稍有不同: "继承"可以 是单一继承或多重继承,每一个继承连接(link) 可以是 public, protected 或 priva旬, 也可以是 virtual 或 non-virtual。然后是成员函数的各个选项:virtual? non-virtual? pure virtual? 以及成员函数和其他语言特性的交互影响:缺省参数值与 virtual 函数 有什么交互影响?继承如何影响 C++ 的名称查找规则?设计选项有哪些?如果 class 的行为需要修改, virtual 函数是最佳选择吗? 本章对这些题目全面宣战。此外我也解释 C++ 各种不同特性的真正意义,也 就是当你使用某个特定构件你真正想要表达的意思。例如"public 继承"意味 "is-a" , 如果你尝试让它带着其他意义,你会惹祸上身。同样道理, v挝ual 函数意味"接口 必须被继承" , non-virtual 函数意味"接口和实现都必须被继承"。如果不能区分 这些意义,会造成 C++ 程序员大量的苦恼。 如果你了解 C++ 各种特性的意义,你会发现,你对 OOP 的看法改变了。它不 再是→项用来划分语言特性的仪典,而是可以让你通过它说出你对软件系统的想 法。一旦你知道该通过它说些什么,移转至 C++ 世界也就不再是可怕的高要求了。 Effective C++ 中文版,第三版150 6 继承与面向对象设计 条款 32: 确定懈的 public 继承塑模出 is-a 关系 Make sure public inheritance models "is-a." 在 «Some Must Watch l-怖 ile Some Must Sleep» (W. H. Freeman and Company, 1974) 这本书中,作者 William Dement 说了一个故事,谈到他曾经试图让学生记下 课程中最重要的一些教导。书上说,他告诉他的班级,一般英国学生对于发生在 1066 年的黑斯廷斯 (Hastings) 战役所知不多。如果有学生记得多一些, Dement 强调, 无非也只是记得 1066 这个数字而己。然后 Dement 继续其课程,其中只有少数重要 信息,包括"安眠药反而造成失眠症"这类有趣的事情。他一再要求学生,纵使忘 了课程中的其他每一件事,也要记住这些数量不多的重要事情。 Dement 在整个学 期中不断耳提面命这样的话。 课程结束后,期末考的最后一道题目是: "写下你从本课程获得的一件永生不 忘的事"。当 Dement 批改试卷,他目瞪口呆。几乎每一个人都写下 "1066" 。 这就是为什么现在我要戒慎恐惧地对你声明,以 C++ 进行面向对象编程,最 重要的一个规则是: public inheritance (公开继承)意味 "is-a" (是一种)的关系。 把这个规则牢牢地烙印在你的心中吧! 如果你令 class D ("Derived门以 public 形式继承 class B ("Base") ,你便是告 诉 C++ 编译器(以及你的代码读者)说,每一个类型为 D 的对象同时也是一个类 型为 B 的对象,反之不成立。你的意思是 B 比 D 表现出更一般化的概念,而 D 比 8 表现出更特殊化的概念。你主张 "B 对象可派上用场的任何地方, D 对象一样可以 派上用场" (译注:此即所谓Liskov Substitution Principle) ,因为每一个 D 对象都 是一种(是一个) B 对象。反之如果你需要一个 D 对象, B 对象无法效劳,因为虽 然每个 D 对象都是一个 B 对象,反之井不成立。 C++ 对于 "public 继承"严格奉行上述见解。考虑以下例子: class Person { ... }; class Student: public Person { ... }; 根据生活经验我们知道,每个学生都是人,但并非每个人都是学生。这便是这 个继承体系的主张。我们预期,对人可以成立的每一件事一一例如每个人都有生 日一一对学生也都成立。但我们并不预期对学生可成立的每一件事一一例如他或她 Effective C++ 中文版,第三版6 条款 32: 确定你的 public 继承塑模出 is-a 关系 151 注册于某所学校一一对人也成立。人的概念比学生更一般化,学生是人的一种特殊 形式。 于是,承上所述,在 C++ 领域中,任何函数如果期望获得一个类型为 Person (或 pointer-to- Person 或 reference-to- Person) 的实参,都也愿意接受一个 Student 对象(或 pointer-to-Student 或 reference-to-Student) : void eat(const Person& p); void study(const Student& s); Person p; Student s; eat (p); eat (s) ; study(s) ; study(p) ; II任何人都会吃 II 只有学生才到校学习 lip 是人 lis 是学生 II 没问题, p 是人 也 II 没问题, s 是学生,而学生也是Cis-a) 人 II 没问题, s 是个学生 II错误 !p 不是个学生 这个论点只对 public 继承才成立。只有当 Student ~ public 形式继承 Person , C+十的行为才会如我所描述。 private 继承的意义与此完全不同(见条款 39) ,至于 protected 继承,那是-种其意义至今仍然困惑我的东西。 public 继承和 is-a 之间的等价关系听起来颇为简单,但有时候你的直觉可能会 误导你。举个例子,企鸪 (pen伊in) 是→种鸟,这是事实。鸟可以飞,这也是事实。 如果我们天真地以 C++ 描述这层关系,结果如下: class Bird { public: virtual void fly(); class Penguin: public Bird { II 鸟可以飞 II 企鹅是一种鸟 突然间我们遇上了乱流,因为这个继承体系说企鹅可以飞,而我们知道那不是 真的。怎么回事? 在这个例子中,我们成了不严谨语言(英语)下的牺牲品。当我们说鸟会飞的 时候,我们真正的意思并不是说所有的鸟都会飞,我们要说的只是一般的鸟都有飞 行能力。如果谨慎二点,我们应该承认一个事实:有数种鸟不会飞。我们来到以下 El知ctive C++ 中文版F 第三版152 6 继承与面向对象设计 继承关系,它塑模出较佳的真实性: class Bird { II 没有声明 fly 函数 {dr·-BC·工-i. , 1(ph 」r-L Mdl·lBoqdv口114ya--u「ι-·· ← L iCES--Els14V JM 由 }CP class Penguin: public Bird { II 没有声明 fly 函数 这样的继承体系比原先的设计更能忠实反映我们真正的意思。 即使如此,此刻我们仍然未能完全处理好这些鸟事,因为对某些软件系统而言, 可能不需要区分会飞的鸟和不会飞的鸟。如果你的程序忙着处理鸟啄和鸟翅,完全 不在乎飞行,原先的"双 classes 继承体系"或许就相当令人满足了。这反映出一个 事实,世界上并不存在一个"适用于所有软件"的完美设计。所谓最佳设计,取决 于系统希望做什么事,包括现在与未来。如果你的程序对飞行一无所知,而且也不 打算未来对飞行"有所知",那么不去区分会飞的鸟和不会飞的鸟,不失为一个完 美而有效的设计。实际上它可能比"对两者做出区隔"更受欢迎,因为这样的区隔 在你企图塑模的世界中并不存在。 另有一种思想派别处理我所谓"所有的鸟都会飞,企鹅是鸟,但是企鹅不会飞, 喔欧"的问题,就是为企鹅重新定义 fly 函数,令它产生一个运行期错误: void error{const std::string& msg); II定义于另外某处 class Penguin: public Bird { public: virtual void fly () { error ("Attempt to II旧ke a penguin fly!"); ) Effective C++ 中文版,第三版6 条款 32: 确定你的 public 继承塑模出 is-a 关系 153 很重要的是,你必须认知这里所说的某些东西可能和你所想的不同。这里并不 是说"企鹅不会飞",而是说"企鹅会飞,但尝试那么做是→种错误"。 如何描述其间的差异?从错误被侦测出来的时间点观之, "企鹅不会飞"这一 限制可由编译期强制实施,但若违反"企鹅尝试飞行,是→种错误"这一条规则, 只有运行期才能检测出来。 为了表现"企鹅不会飞,就这样"的限制,你不可以为 Penguin 定义 fly 函数: class Bird { II 没有声明 fly 函数 class Penguin: public Bird { II 没有声明 fly 函数 现在,如果你试图让企鹅飞,编译器会对你的背信加以谴责: Penguin p; p.fly( ); II错误! 这和采取"令程序于运行期发生错误"的解法极为不同。若以那种做法,编译 器不会对 p.fly 调用式发出任何抱怨。条款18 说过:好的接口可以防止无效的代 码通过编译,因此你应该宁可采取"在编译期拒绝企鹅飞行"的设计,而不是"只 在运行期才能侦测它们"的设计。 或许你承认你对鸟类缺乏直觉,但基础几何学得不错。喔,是吗?那么我请问, 正方形和矩形之间可能有多么复杂? 好,请回答这个简单的问题: class Square 应该以 public 形式继承 class Rectangle吗? Effective C++ 中文版,第三版154 6 继承与面向对象设计 "咄! "你说, "当然应该如此!每个人都知道正方形是一种矩形,反之则不 一定",这是真理,至少学校是这么教的。但是我不认为我们还在象牙塔内。 考虑这段代码: class Rectaηgle { public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height( ) const; II返回当前值 virtual int width() const; void makeBigger(Rectangle& r) II这个函数用以增加r 的面积 int oldHeight = r.height( ); r.setWidth(r.width() + 10); II为 r 的宽度加 10 assert(r.height( ) == oldHeight); II判断主的高度是否未曾改变 显然,上述的 assert 结果永远为真。因为makeBigger只改变 r 的宽度; r 的 高度从未被更改。 现在考虑这段代码,其中使用public 继承,允许正方形被视为一种矩形z class Square: public Rectangle { Square s; assert(s.width() == s.height(»; makeBigger (s) ; assert(s.width() == s.height(»; II这对所有正方形一定为真。 II 由于继承, s 是一种draw( ); Shape* ps2 = new Ellipse; ps2->draw () ; psl->Shape::draw( ); ps2->Shape::draw( ); II错误! Shape 是抽象的 II没问题 II调用 Rectangle::draw II没问题 II调用 Ellipse: :draw II调用 Shape: :draw II调用 Shape: :draw 163 除了能够帮助你在鸡尾酒派对上留给大师级程序员一个深刻的印象,→般而言 这项性质用途有限。但是一如稍后你将看到,它可以实现二种机制,为简朴的(非 纯) impure virtual 函数提供更平常更安全的缺省实现。 简朴的 impure virtual 函数背后的故事和pure vi阳al 函数有点不同。一如往常, derived classes 继承其函数接口,但impure virtual 函数会提供→份实现代码,derived classes 可能覆写 (override) 它。稍加思索,你就会明白: ·声明简朴的(非纯) impure virtual 函数的目的,是让derived classes 继承该函数 的接口和缺省实现。 考虑 Shape: : error 这个例子: class Shape { public: virtual void error(const std::string& msg); 其接口表示,每个 class 都必须支持一个"当遇上错误时可调用"的函数,但 每个 class 可自由处理错误。如果某个class 不想针对错误做出任何特殊行为,它可 以返回到 Shape class 提供的缺省错误处理行为。也就是说Shape: : error 的声明式 告诉 derived classes 的设计者, "你必须支持一个 error 函数,但如果你不想自己 写一个,可以使用 Shape class 提供的缺省版本"。 但是,允许 impure virtual 函数同时指定函数声明和函数缺省行为,却有可能造 成危险。欲探讨原因,让我们考虑 XYZ 航空公司设计的飞机继承体系。该公司只 有 A 型和 B 型两种飞机,两者都以相同方式飞行。因此 XYZ 设计出这样的继承体 系: Effective C++ 中文版,第三版164 6 继承与面向对象设计 class Airport { ... }; I I用以表现机场 class Airplane { public: virtual void fly(const Airport& destination); void Airplane::fly(const Airport& destination) { 缺省代码,将飞机飞至指定的目的地 class ModelA: public Airplane { }; class ModelB: public Airplane { }; 为了表示所有飞机都一定能飞,并阐明"不同型飞机原则上需要不同的fly 实 现" , Ai 叩 lane: :fly 被声明为 vi阳alo 然而为了避免在 ModelA 和 ModelB 中撰写 相同代码,缺省飞行行为由 Airplane: : fly 提供,它同时被 ModelA 和 ModelB 继承。 这是个典型的面向对象设计。两个 classes 共享一份相同性质(也就是它们实现 fly 的方式) ,所以共同性质被搬到 base class 中,然后被这两个 classes 继承。这 个设计突显出共同性质,避免代码重复,并提升未来的强化能力,减缓长期维护所 需的成本。所有这些都是面向对象技术如此受到欢迎的原因。 XYZ 航空公司应该感 到骄傲。 现在,假设 XYZ 盈余大增,决定购买一种新式 C 型飞机。 C 型和 A 型以及 B 型有某些不同。更明确地说,它的飞行方式不同。 XYZ 公司的程序员在继承体系中针对 C 型飞机添加了一个 class ,但由于他们 急着让新飞机上线服务,竟忘了重新定义其 fly 函数: class ModelC: public Airplane { II 未声明 fly 函数 然后代码中有一些诸如此类的动作: Airport PDX(...); Airplane* pa = new ModelC; pa->fly (PDX) ; Effective C++ 中文版,第三版 IIPDX 是我家附近的机场 II 调用 Airplane: : fly6 条款 34: 区分接口继承和实现继承 165 这将酿成大灾难:这个程序试图以 ModelA 或 ModelB 的飞行方式来飞 ModelC 。 这不是一个可以公开鼓舞旅游信心的行为。 问题不在 Airplane: : fly 有缺省行为,而在于 ModelC 在未明白说出"我要" 的情况下就继承了该缺省行为。幸运的是我们可以轻易做到"提供缺省实现给 derived classes,但除非它们明白要求否则免谈"。此间技俩在于切断"virtual 函数 接口"和其"缺省实现"之间的连接。下面是一种做法: class Airplane { public: virtual void fly(const Airport& destination) = 0; protected: void defaultFly(const Airport& destination); void Airplane::defaultFly(const Airport& destination) 缺省行为,将飞机飞至指定的目的地。 请注意, Airplane: :fly 己被改为一个pure virtual 函数,只提供飞行接口。其 缺省行为也出现在Airplane class 中,但此次系以独立函数defaultFly的姿态出 现。若想使用缺省实现(例如ModelA和 ModelB),可以在其 fly 函数中对defaultFly 做一个 inline 调用(但请注意条款30 所言, inline 函数和 virtual 函数之间的交互关 系) : class ModelA: public Airplane { public: virtual void fly(const Airport& destination) { defaultFly(destination); } class ModelB: public Airplane { public: virtual void fly(const Airport& dest工nation) { defaultFly(destination); } El作ctive C++ 中文版,第三版166 6 继承与面向对象设计 现在 ModelC class 不可能意外继承不正确的 fly 实现代码了,因为 Airplane 中的 pure virtual 函数迫使 ModelC 必须提供自己的 fly 版本: class ModelC: public Airplane { public: virtual void fly(const Airport& destination}; }; void ModelC::fly(const Airport& destination} { 将 C 型飞机飞至指定的目的地 这个方案并非安全无虞,程序员还是可能因为剪贴 (copy-and-paste) 代码而招 来麻烦,但它的确比原先的设计值得倚赖。至于 Airplane::defaultFly ,请注意 它现在成了 protected ,因为它是 Airplane 及其 derived classes 的实现细目。乘客应 该只在意飞机能不能飞,不在意它们怎么飞。 Airplane::defaultFly 是个 non-virtual 函数,这一点也很重要。因为没有任何 一个 derived class 应该重新定义此函数(见条款 36) 。如果 default Fl y 是 virtual 函数,就会出现一个循环问题:万一某些 derived class 忘记重新定义 defaultFly , 会怎样? 有些人反对以不同的函数分别提供接口和缺省实现,像上述的 fly 和 defaultFly 那样。他们关心因过度雷同的函数名称而引起的 class 命名空间污染问 题。但是他们也同意,接口和缺省实现应该分开。这个表面上看起来的矛盾该如何 解决?晤,我们可以利用 "pure virtual 函数必须在 derived classes 中重新声明,但 它们也可以拥有自己的实现"这一事实。下面便是 Airplane 继承体系如何给 pure virtual 函数→份定义: class Airplane { public: virtual void fly(const Airport& destination} = 0; £1知ctive C++中文版,第三版6 条款 34: 区分接口继承和实现继承 167 飞loid Airplane::fly(co 口 st Airport& destinatio口) / /pure virtual 函数实现 缺省行为,将飞机飞至指定的目的地 class ModelA: public Airplane { public: virtual void fly(const Airport& destination) { Airplane::fly(destination); } class ModelB: public Airplane { public: virtual void fly(const Airport& destination) { Airplane::fly(destination); } class ModelC: public Airplane { public: virtual void fly(const Airport& destination); }; void ModelC::fly(const Airport& destination) 将 C 型飞机飞至指定的目的地 这几乎和前一个设计一模一样,只不过pure virtual 函数 Airplane: : fly 替换了 独立函数 Airplane::defaultFly。本质上,现在的 fly 被分割为两个基本要素: 其声明部分表现的是接口(那是 derived classes 必须使用的) .其定义部分则表现 出缺省行为(那是 derived classes 可能使用的,但只有在它们明确提出申请时才是)。 如果合并 fly 和 defaultFly. 就丧失了"让两个函数享有不同保护级别"的机会: 习惯上被设为 protected 的函数 CdefaultFly) 如今成了 publicC 因为它在 fly 之中)。 最后,让我们看看 Shape 的 non-virtual 函数 objectI D: class Shape { public: int objectID( ) const; Effective C++ 中文版,第三版168 6 继承与面向对象设计 如果成员函数是个 non-virtual 函数,意味是它并不打算在 derived classes 中有 不同的行为。实际上一个 non-virtual 成员函数所表现的不变性 Cinvarian t) 凌驾其 特异性 (specialization) , 因为它表示不论 derived class 变得多么特异化,它的行为 都不可以改变。就其自身而言: ·声明 non-virtual 函数的目的是为了令 derived classes 继承函数的接口及一份强制 性实现。 你可以把 Shape: :obj ectID 的声明想做是: "每个 Shape 对象都有一个用来产 生对象识别码的函数:此识别码总是采用相同计算方法,该方法由 Shape::objectID的定义式决定,任何derived class 都不应该尝试改变其行为"。 由于 non-virtual 函数代表的意义是不变性Cinvariant) 凌驾特异性 (specialization) , 所以它绝不该在 derived class 中被重新定义。这也是条款 36 所讨论的一个重点。 pure virtual 函数、 simple (impure) virtual 函数、 non-virtual 函数之间的差异,使 你得以精确指定你想要 derived classes 继承的东西:只继承接口,或是继承接口和 一份缺省实现,或是继承接口和一份强制实现。由于这些不同类型的声明意味根本 意义并不相同的事情,当你声明你的成员函数时,必须谨慎选择。如果你确实履行, 应该能够避免经验不足的 class 设计者最常犯的两个错误。 第一个错误是将所有函数声明为 non-virtual 。这使得 derived classes 没有余裕空 间进行特化工作。 non-virtu剖析构函数尤其会带来问题(见条款7)。当然啦,设 计一个并不想成为 base class 的 class 是绝对合理的,既然这样,将其所有成员函数 都声明为 non-virtual 也很适当。但这种声明如果不是忽略了 virtual 和 non-virtual 函 -数之间的差异,就是过度担心 virtual 函数的效率成本。实际上任何 class 如果打算 被用来当做一个 base class ,都会拥有若干 virtual 函数(再次见条款7)。 如果你关心 VI阳al 函数的成本,请容许我介绍所谓的 80-20 法则(也可见条 款 30) 。这个法则说,一个典型的程序有 80% 的执行时间花费在 20 毛的代码身上。 此一法则十分重要,因为它意味,平均而言你的函数调用中可以有 80 毛是 virtual 而不冲击程序的大体效率。所以当你担心是否有能力负担 virtual 函数的成本之前, 请先将心力放在那举足轻重的 20 宅代码上头,它才是真正的关键。 El知ctive C++ 中文版,第三版6 条款 35: 考虑 virtual 函数以外的其他选择 169 另一个常见错误是将所有成员函数声明为 virtual 。有时候这样做是正确的,例 如条款 31 的 Interface classes 。然而这也可能是 class 设计者缺乏坚定立场的前兆。 某些函数就是不该在 derived class 中被重新定义,果真如此你应该将那些函数声明 为 non-virtual 。没有人有权利妄称你的 class 适用于任何人任何事任何物而他们只需 花点时间重新定义你的函数就可以享受一切。如果你的不变性( invariant) 凌驾特 异性 (specialization) , 别害怕说出来。 请记住 ·接口继承和实现继承不同。在 public 继承之下, derived classes 总是继承 base class 的接口。 • p町e virtual 函数只具体指定接口继承。 ·简朴的(非纯) impure virtual 函数具体指定接口继承及缺省实现继承。 • non-virtual 函数具体指定接口继承以及强制性实现继承。 条款 35: 弩虑 virtual 函数以外的真他选择 Consider alternatives to virtual functions. 假设你正在写→个视频游戏软件,你打算为游戏内的人物设计一个继承体系。 你的游戏属于暴力砍杀类型,剧中人物被伤害或因其他因素而降低健康状态的情况 并不罕见。你因此决定提供一个成员函数 healthValue ,它会返回一个整数,表示 人物的健康程度。由于不同的人物可能以不同的方式计算他们的健康指数,将 healthValue 声明为 virtual 似乎是再明白不过的做法: class GameCharacter { public: virtual int healthValue() canst; II返回人物的健康指数: II derived classes 可重新定义它。 healthValue 并未被声明为 pure virtual ,这暗示我们将会有个计算健康指数的 缺省算法(见条款 34) 。 Effective C++ 中文版,第三版170 6 继承与面向对象设计 这的确是再明白不过的设计,但是从某个角度说却反而成了它的弱点。由于这 个设计如此明显,你可能因此没有认真考虑其他替代方案。为了帮助你跳脱面向对 象设计路上的常轨,让我们考虑其他→些解法。 藉由 Non-Virtual Interface 手法实现 Template Method 模式 我们将从一个有趣的思想流派开始,这个流派主张 virtual 函数应该几乎总是 private 。这个流派的拥护者建议,较好的设计是保留 healthValue 为 public 成员函 数,但让它成为 non-virtual ,并调用一个 private virtual 函数(例如 doHealthValue) 进行实际工作: class GameCharacter { public: int healthValue() const int retVal = doHealthValue(); return retVal; private: virtual int doHealthValue() const II derived classes 不重新定义它, II 见条款 36 。 II 做一些事前工作,详下。 II 做真正的工作。 II 做一些事后工作,详下。 II derived classes 可重新定义它。 II缺省算法,计算健康指数。 在这段(以及本条款其余的)代码中,我直接在 class 定义式内呈现成员函数 本体。一如条款 30 所言,那也就让它们全都暗自成了 inline 。但其实我以这种方 式呈现代码只是为了让你比较容易阅读。我所描述的设计与 inlining 其实没有关联, 所以请不要认为成员函数在这里被定义于 classes 内有特殊用意。不,它没有。 这一基本设计,也就是"令客户通过 public non-virtual 成员函数间接调用 private virtual 函数",称为 non-virtual inte价ce (NY!) 手法。它是所谓 Template Method 设计模式(与 C++ templates 并无关联)的一个独特表现形式。我把这个non-vi血泊l 函数 (healthValue) 称为 virtual 函数的外覆器 (wrapper) 。 El知ctive C++中文版,第三版6 条款 35: 考虑 virtual 函数以外的其他选择 171 NYI 手法的一个优点隐身在上述代码注释"做-些事前工作"和"做一些事后 工作"之中。那些注释用来告诉你当时的代码保证在 "virtual 函数进行真正工作之 前和之后"被调用。这意味外覆器 (wrapper) 确保得以在一个 virtual 函数被调用 之前设定好适当场景,并在调用结束之后清理场景。"事前工作"可以包括锁定互 斥器 (locking a mutex) 、制造运转日志记录项 (log en位y) 、验证 class 约束条件、 验证函数先决条件等等。"事后工作"可以包括互斥器解除锁定( unlocking a mutex) 、 验证函数的事后条件、再次验证 class 约束条件等等。如果你让客户直接调用 virtual 函数,就没有任何好办法可以做这些事。 有件事实或许会妨碍你跃跃欲试的心: NVI 手法涉及在 derived classes 内重新 定义 private virtual 函数。啊,重新定义若干个 derived classes 并不调用的函数!这 里并不存在矛盾。"重新定义 virtual 函数"表示某些事"如何"被完成, "调用 virtual 函数"则表示它"何时"被完成。这些事情都是各自独立互不相干的。 NVI 手法允许 derived classes 重新定义 virtual 函数,从而赋予它们"如何实现机能"的 控制能力,但 base class 保留诉说"函数何时被调用"的权利。-开始这些昕起来 似乎诡异,但 C++ 的这种" derived classes 可重新定义继承而来的 private vi阳al 函 数"的规则完全合情合理。 在 NVI 手法下其实没有必要让 vi血泪1 函数二定得是 private 。某些 class 继承体 系要求 derived class 在 VI阳al 函数的实现内必须调用其 base class 的对应兄弟(例如 p .l 20 的程序) ,而为了让这样的调用合法, vi阳al 函数必须是 protected ,不能是 private. 有时候 virtual 函数甚至一定得是 public (例如具备多态性质的 base classes 的析构函数一见条款 7) ,这么一来就不能实施 NYI 手法了。 藉由 Function Pointers 实现 Strategy 模式 NYI 手法对 public virtual 函数而言是一个有趣的替代方案,但从某种设计角度 观之,它只比窗饰花样更强→些而已。毕竟我们还是使用 virtual 函数来计算每个人 物的健康指数。另一个更戏剧性的设计主张"人物健康指数的计算与人物类型无 关",这样的计算完全不需要"人物"这个成分。例如我们可能会要求每个人物的 构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际 计算: Effective C++ 中文版,第三版172 6 继承与面向对象设计 class GameCharacter; II前置声明(岛阳缸d declaration) II 以下函数是计算健康指数的缺省算法。 int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typedef int (*HealthCalcFunc) (const GameCharacter&); explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {) int healthValue() const { return healthFunc(*this); ) private: HealthCalcFunc healthFunc; 这个做法是常见的 Strategy 设计模式的简单应用。拿它和"植基于 GameCharacter继承体系内之virtual 函数"的做法比较,它提供了某些有趣弹性: ·同一人物类型之不同实体可以有不同的健康计算函数。例如: class EvilBadGuy: public GameCharacter { public: explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) GameCharacter(hcf) { ... } int loseHealthQuickly(const GameCharacter&); II健康指数计算函数l int loseHealthSlowly(const GameCharacter&); II健康指数计算函数2 EvilBadGuyebgl(loseHealthQuickly); II相同类型的人物搭配 EvilBadGuyebg2(loseHealthSlowly); II 不同的健康计算方式 ·某己知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供 一个成员函数 setHealthCalculator,用来替换当前的健康指数计算函数。 换句话说,"健康指数计算函数不再是GameCharacter继承体系内的成员函数" 这一事实意味,这些计算函数并未特别访问"即将被计算健康指数"的那个对象的 内部成分。例如defaultHealthCalc并未访问 EvilBadGuy的 non-public成分。 EfJ告ctive C++中文版,第三版6 条款 35: 考虑 virtual 函数以外的其他选择 173 如果人物的健康可纯粹根据该人物 public 接口得来的信息加以计算,这就没有 问题,但如果需要 non-public 信息进行精确计算,就有问题了。实际上任何时候当 你将 class 内的某个机能(也许取道自某个成员函数)替换为 class 外部的某个等价 机能(也许取道自某个 non-member non- 企iend 函数或另一个 class 的 non-friend 成员 函数) ,这都是潜在争议点。这个争议将持续至本条款其余篇幅,因为我们即将考 虑的所有替代设计也都涉及使用 GameCharacter 继承体系外的函数。 一般而言,唯一能够解决"需要以 non-member 函数访问 class 的 non-public 成 分"的办法就是:弱化 class 的封装。例如 class 可声明那个 non-member 函数为 friends ,或是为其实现的某一部分提供 public 访问函数(其他部分则宁可隐藏起 来)。运用函数指针替换 virtual 函数,其优点(像是"每个对象可各自拥有自己的 健康计算函数"和"可在运行期改变计算函数" )是否足以弥补缺点(例如可能必 须降低 GameCharacter 封装性) ,是你必须根据每个设计情况的不同而抉择的。 藉由 trl: :function 完成 Strategy 模式 一旦习惯了 templates 以及它们对隐式接口(见条款 4 1)的使用,基于函数指 针的做法看起来便过分苛刻而死板了。为什么要求"健康指数之计算"必须是个函 数,而不能是某种"像函数的东西" (例如函数对象)呢?如果一定得是函数,为 什么不能够是个成员函数?为什么一定得返回 int 而不是任何可被转换为 int 的类 型呢? 如果我们不再使用函数指针(如前例的 heal thFunc) ,而是改用一个类型为 trl::function的对象,这些约束就全都挥发不见了。就像条款54 所说,这样的 对象可持有(保存)任何可调用物(callable entity, 也就是函数指针、函数对象、 或成员函数指针) ,只要其签名式兼容于需求端。以下将刚才的设计改为使用 trl::function: class GameCharacter; II如前 int defaultHealthCalc(const GameCharacter& gc); II如前 class GameCharacter { public: IIHealthCalcFunc可以是任何"可调用物" (callable entity) ,可被调用并接受 II任何兼容于 GameCharacter 之物,返回任何兼容于 int 的东西。详下。 typedef std: :trl: :function夏plicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) healthFunc (hcf ) {} int healthValue() const [ return healthFunc( * this); } private: HealthCalcFunc healthFunc; 如你所见, HealthCalcFunc 是个 typedef,用来表现 trl::function的某个具 现体,意味该具现体的行为像一般的函数指针。现在我们靠近一点瞧瞧 HealthCalcFunc 是个什么样的typedef: std::trl::functioncalc(*this); } private: HealthCalcFunc* pHealthCalc; £1知ctive C++中文版,第三版6 条款 35: 考虑 virtual 函数以外的其他选择 177 这个解法的吸引力在于,熟悉标准 Strategy 模式的人很容易辨认它,而且它还 提供"将一个既有的健康算法纳入使用"的可能性一一只要为 HealthCalc Func 继 承体系添加一个 derived class 即可。 摘要 本条款的根本忠告是,当你为解决问题而寻找某个设计方法时,不姑考虑 VI阳al 函数的替代方案。下面快速重点复习我们验证过的几个替代方案: ·使用 non-virtual interface (NY!) 手法,那是 Template Method 设计模式的一种 特殊形式。它以 public non-virtual 成员函数包裹较低访问性 (private 或 protected) 的 vi阳al 函数。 ·将 virtual 函数替换为"函数指针成员变量",这是 Strategy 设计模式的一种分 解表现形式。 ·以 trl::function 成员变量替换 virtual 函数,因而允许使用任何可调用物 (callable entity) 搭配一个兼容于需求的签名式。这也是Strategy 设计模式的 某种形式。 ·将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数。这是 Strategy 设计模式的传统实现手法。 以上并未彻底而详尽地列出 virtual 函数的所有替换方案,但应该足够让你知道 的确有不少替换方案。此外,它们各有其相对的优点和缺点,你应该把它们全部列 入考虑。 为避免陆入面向对象设计路上因常规而形成的凹洞中,偶而我们需要对着车轮 猛推二把。这个世界还有其他许多道路,值得我们花时间加以研究。 请记住 • virtual 函数的替代方案包括 NYl 手法及 Strategy 设计模式的多种形式。 NYI 于 法自身是一个特殊形式的 Template Method 设计模式。 ·将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访 问 class 的 non-public 成员。 • trl::function 对象的行为就像→般函数指针。这样的对象可接纳"与给定之 目标签名式 (target signature) 兼容"的所有可调用物 (callable entities) 。 Effective C++ 中文版,第三版178 6 继承与面向对象设计 条款 36: 绝不重新定义继承而来的 non-virtual 函数 Never redefine an inherited non-virtual function. 假设我告诉你. class D 系由 class B ~ public 形式派生而来. class B 定义有一个 public 成员函数 mf。由于 mf 的参数和返回值都不重要,所以我假设两者皆为void。 换句话说我的意思是: class B{ public: void mf (); class D: public B{... }; 虽然我们对民D 和 mf 一无所知,但面对一个类型为D 的对象 x: D x; 如果以下行为: B* pB = &x; pB->mf (); 异于以下行为: D* pD = &x; pD->mf () ; Ilx 是一一个类型为 D 的对象 II获得-个指针指向 x II经由该指针调用 mf II 获得)个指针指向 x II经由该指针调用 mf 你可能会相当惊讶。毕竟两者都通过对象 x 调用成员函数 mf。由于两者所调用 的函数都相同,凭借的对象也相同,所以行为也应该相同,是吗? 是的,理应如此,但事实可能不是如此。更明确地说,如果 mf 是个 non-virtual 函数而 D 定义有自己的时版本,那就不是如此: class D: public B{ public: void mf(); II遮掩 (hides) 了 B: :mf; 见条款 33 pB->mf(); II调用 B: :mf pD->mf(); II调用 D: :mf 造成此一两面行为的原因是. non-virtual 函数如 B: :mf 和 D: :mf 都是静态绑定 ( statically bound. 见条款 37) 。这意思是,由于 pB 被声明为一个 pointer-to-B. 通 £1知ctive C++ 中文版,第三版6 条款 36: 绝不重新定义继承而来的 non-virtual 函数 179 过 pB 调用的 non-virtual 函数永远是 B 所定义的版本,即使 pB 指向一个类型为 "B 派生之 class" 的对象,→如本例。 但另一方面, virtual 函数却是动态绑定 (dynamically bound ,见条款 37) ,所 以它们不受这个问题之苦。如果 mf 是个 vi阳al 函数,不论是通过 pB 或 pD 调用时, 都会导致调用 D- .时,因为 pB 和 pD 真正指的都是一个类型为 D 的对象。 如果你正在编写 class D 并重新定义继承自 class B 的 non呐rtual 函数 mf , D 对象 很可能展现出精神分裂的不一致行径。更明确地说,当时被调用,任何一个 D 对 象都可能表现出 B 或 D 的行为:决定因素不在对象自身,而在于"指向该对象之指 针"当初的声明类型。 References 也会展现和指针一样难以理解的行径。 但那只是实务面上的讨论。我知道你真正想要的是理论层面的理由(关于"绝 不重新定义继承而来的 non-virtual 函数"这回事)。我很乐意为你服务。 条款 32 己经说过,所谓 public 继承意味 is-a (是一种)的关系。条款 34 则描 述为什么在 class 内声明一个 non-virtual 函数会为该 class 建立起一个不变性 (invariant> ,凌驾其特异性( specialization) 。如果你将这两个观点施行于两个 classes B 和 D 以及 non-virtual 成员函数 B: :mf 身上,那么 z ·适用于 B 对象的每一件事,也适用于 D 对象,因为每个 D 对象都是一个 B 对象: • B 的 derived classes 一定会继承时的接口和实现,因为 mf 是 B 的一个 non-virtual 函数。 现在,如果曾重新定义时,你的设计便出现矛盾。如果 D 真有必要实现出与 B 不同的时,并且如果每一个 B 对象一一不管多么特化一一真的必须使用 B 所提供的 时实现码,那么"每个 D 都是一个 B" 就不为真。既然如此 D 就不该!j public 形式 继承 B。另一方面,如果 D 真的必须以 public 方式继承 B ,并且如果 D 真有需要实 现出与 B 不同的时,那么时就无法为 B 反映出"不变性凌驾特异性"的性质。既 然这样 mf 应该声明为 virtual 函数。最后,如果每个 D 真的是一个 B ,并且如果 mf 真的为 B 反映出"不变性凌驾特异性"的性质,那么 D 便不需要重新定义时,而且 它也不应该尝试这样做。 不论哪一个观点,结论都相同:任何情况下都不该重新定义一个继承而来的 non-virtual 函数。 El知ctive C++ 中文版,第三版180 6 继承与面向对象设计 如果此条款使你感到枯燥乏味,或许是因为你己经读过条款 7 ,该条款解释为 什么多态性 (polymorphic) base classes 内的析构函数应该是 virtual 。如果你违反那 个准则(也就是说如果你在polymorphic base class 内声明一个 non-virtual 析构函数), 你也就违反了本条款,因为 derived classes 绝对不该重新定义一个继承而来的 non-vI阳al 函数(此处指的是 base class 析构函数)。即使没有声明析构函数,此亦 为真,因为条款 5 说,析构函数是"如果你没有为自己声明一个,编译器会为你生 成一个"的数种成员函数之一。就本质而言,条款7 只不过是本条款的一个特殊案 例,尽管它也足够重要到单独成为一个条款。 请记住 ·绝对不要重新定义继承而来的non-virtual 函数。 条款 37: 绝不重新定义继承而来的缺省参数值 Never redefine a function's inherited default parameter value. 让我们一开始就将讨论简化。你只能继承两种函数:virtual 和 non-virtual 函数。 然而重新定义一个继承而来的non-virtual 函数永远是错误的(见条款36) ,所以我 们可以安全地将本条款的讨论局限于"继承一个带有缺省参数值的virtual 函数"。 这种情况下,本条款成立的理由就非常直接而明确了: virtual 函数系动态绑定 (dynamically bound) ,而缺省参数值却是静态绑定(statically bound) 。 那是什么意思?你说你那负荷过重的脑袋早己忘记静态绑定和动态绑定之间 的差异? (为了正式记录在案,容我再说一次,静态绑定又各前期绑定, early binding; 动态绑定又名后期绑定, late binding 0 )现在让我们来一趟复习之旅吧! 对象的所谓静态类型( static type) ,就是它在程序中被声明时所采用的类型。 考虑以下的 class 继承体系: II一个用以描述几何形状的 class class Shape { public: enum ShapeColor { Red, Green, Blue ); II所有形状都必须提供一个函数,用来绘出自己 virtual 飞loid draw(ShapeColor color = Red) const = 0; E1知ctive C++中文版,第三版6 条款 37: 绝不重新定义继承而来的缺省参数值. class Rectangle: public Shape { public: II注意,赋予不同的缺省参数值。这真糟糕! virtual void draw(ShapeColor color = Green) const; class Circle: public Shape { public: virtual void draw(ShapeColor color) canst; II译注:请注意,以上这么写则当客户以对象调用此函数,一定要指定参数值。 II 因为静态绑定下这个函数并不从其base 继承缺省参数值。 II 但若以指针(或reference) 调用此函数,可以不指定参数值, II 因为动态绑定下这个函数会从其base 继承缺省参数值。 这个继承体系图示如下z 现在考虑这些指针: 181 Shape* ps; Shape* pc = new Circle; Shape* pr = new Rectangle; II静态类型为 Shape* II静态类型为 Shape* II静态类型为 Shape女 本例中 ps , pc 和 pr 都被声明为 pointer-to-Shape类型,所以它们都以它为静态 类型。注意,不论它们真正指向什么,它们的静态类型都是Shape飞 对象的所谓动态类型 (dynamic type) 则是指"目前所指对象的类型"。也就 是说,动态类型可以表现出一个对象将会有什么行为。以上例而言,pc 的动态类型 是 Circle*, pr 的动态类型是Rectangle飞 ps 没有动态类型,因为它尚未指向任 何对象。 动态类型一如其名称所示,可在程序执行过程中改变(通常是经由赋值动作): ps = pc; ps = pr; lips 的动态类型如今是Circle* lips 的动态类型如今是Rectangle* Virtual 函数系动态绑定而来,意思是调用一个virtual 函数时,究竟调用哪一份 函数实现代码,取决于发出调用的那个对象的动态类型: Effective C++ 中文版,第三版182 pc->draw(Shape: :Red); pr->draw(Shape::Red); 6 继承与面向对象设计 II调用 Circle::draw(Shape::Red) II调用 Rectangle::draw(Shape::Red) 我知道这些都是老调重弹:是的,你当然己经了解virtual 函数。但是当你考虑 带有缺省参数值的virtual 函数,花样来了,因为就如我稍早所说,virtual 函数是动 态绑定,而缺省参数值却是静态绑定。意思是你可能会在"调用一个定义于derived class 内的 virtual 函数"的同时,却使用base class 为它所指定的缺省参数值z pr->draw( ); II调用 Rectangle::draw(Shape::Red)! 此例之中, pr 的动态类型是 Rectangle女,所以调用的是 Rectangle 的 virtual 函数,一如你所预期。 Rectangle: :draw 函数的缺省参数值应该是 G阻剧,但由于 pr 的静态类型是 Shape 食,所以此一调用的缺省参数值来自 Shape class 而非 Rectangle class! 结局是这个函数调用有着奇怪并且几乎绝对没人预料得到的组 合,由 Shape class 和 Rectangleclass 的 draw 声明式各出一半力。 以上事实不只局限于"ps, pc 和 pr 都是指针"的情况:即使把指针换成references 问题仍然存在。重点在于draw是个 virtual 函数,而它有个缺省参数值在derived class 中被重新定义了。 为什么 C++ 坚持以这种乖张的方式来运作呢?答案在于运行期效率。如果缺 省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual 函数决定适当的 参数缺省值。这比目前实行的"在编译期决定"的机制更慢而且更复杂。为了程序 的执行速度和编译器实现上的简易度,C++ 做了这样的取舍,其结果就是你如今所 享受的执行效率。但如果你没有注意本条款所揭示的忠告,很容易发生混淆。 这一切都很好,但如果你试着遵守这条规则,并且同时提供缺省参数值给base 和 derived classes 的用户,又会发生什么事呢? class Shape ( public: enum ShapeColor { Red, Green, Blue }; virtual void draw(ShapeColor color = Red) const = 0; Effective C++ 中文版,第三版6 条款 37: 绝不重新定义继承而来的缺省参数值 class Rectangle: public Shape { public: virtual void draw(ShapeColor color = Red) const; 183 喔欧,代码重复。更糟的是,代码重复又带着相依性 (wi仕1 dependencies) : 如果 Shape 内的缺省参数值改变了,所有"重复给定缺省参数值"的那些 derived classes 也必须改变,否则它们最终会导致"重复定义一个继承而来的缺省参数值"。 怎么办? 当你想令 virtual 函数表现出你所想要的行为但却遭遇麻烦,聪明的做法是考虑 替代设计。条款 35 列了不少 virtual 函数的替代设计,其中之一是 NVI (non-virtual inte功'ce) 手法:令 base class 内的一个 public non-virtual 函数调用 private vi血泊l 函 数,后者可被 derived classes 重新定义。这里我们可以让 non-virtual 函数指定缺省 参数,而 private virtual 函数负责真正的工作: class Shape { public: enum ShapeColor { Red, Gree口, Blue }; void draw(ShapeColor color = Red) const doDraw (color) ; II如今它是 non-讨血泪I II调用一个世血.Jal private: virtual void doDraw(ShapeColor color) const = 0; II真正的工作 II在此处完成 class Rectangle: public Shape { public: private: virtual void doDraw(ShapeColor color) const; II注意,习专酣旨定 II缺省参数值。 由于 non-virtual 函数应该绝对不被derived classes 覆写(见条款36) ,这个设 计很清楚地使得draw 函数的 color缺省参数值总是为Redo 请记住 ·绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定, 而 vi阳al 函数一一你唯一应该覆写的东西一一却是动态绑定。 Effie伽eC++中文版,第三版184 6 继承与面向对象设计 条款 38: 通过复合塑模出 has-a 或"根据某物实现出" Model "has-a" or "is-implemented-in-tenns-of' through composition. 复合 (composition) 是类型之间的一种关系,当某种类型的对象内含它种类型 的对象,便是这种关系。例如: class Address { ... }; class PhoneNumber { ... }; class Person { public: private: std::string name; Address address; PhoneNumber voiceNumber; PhoneNumber faxNumber; II某人的住址 II合成成分物(∞mposed object) II 同上 II 同上 II 同上 本例之中 Person 对象由 stri 呵, Address, PhoneNumber 构成。在程序员之间 复合( composition) 这个术语有许多同义词,包括 l可ering (分层人 containment (内含) , aggregation (聚合)和 embedding (内嵌)。 条款 32 曾说, "public 继承"带有 is -a (是一种)的意义。复合也有它自己的 意义。实际上它有两个意义。复合意味 has-a( 有一个)或 is-implemented-in也rms-of (根据某物实现出)。那是因为你正打算在你的软件中处理两个不同的领域 (domains) 。程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、 汽车、一张张视频画面等等。这样的对象属于应用域 (application domain) 部分。 其他对象则纯粹是实现细节上的人工制品,像是缓冲区(bu的rs) 、互斥器 (mutexes) 、 查找树 (search trees) 等等。这些对象相当于你的软件的实现域(implementation domain) 。当复合发生于应用域内的对象之间,表现出 has-a 的关系:当它发生于 ·实现域内则是表现 is-implemented-in-terms-of 的关系。 上述的 Person class 示范 has-a 关系。 Person 有一个名称,一个地址,以及语 音和传真两笔电话号码。你不会说"人是一个名称"或"人是一个地址",你会说 "人有→个名称"和"人有一个地址"。大多数人接受此一区别毫无困难,所以很 少人会对 is-a 和 has-a 感到困惑。 比较麻烦的是区分 is唱(是一种)和 is-implemented-in-terms-of (根据某物实 现出)这两种对象关系。假设你需要一个 template ,希望制造出一组 classes 用来表 现由不重复对象组成的 sets 。由于复用 (reuse) 是件美妙无比的事情,你的第一个 E庐ctive C++ 中文版,第三版6 条款 38: 通过复合塑模出 has-a 或"根据某物实现出" 185 直觉是采用标准程序库提供的 set template 。是的,如果他人所写的 template 合乎 需求,我们何必另写一个呢? 不幸的是 set 的实现往往招致"每个元素耗用三个指针"的额外开销。因为 sets 通常以平衡查找树 (balanced search 位ees) 实现而成,使它们在查找、安插、移除 元素时保证拥有对数时间 (logarithmic-time) 效率。当速度比空间重要,这是个通 情达理的设计,但如果你的程序却是空间比速度重要呢?那么标准程序库的 set 提 供给你的是个错误决定下的取舍。似乎你终究还得写个自己的 templateo 但是容我再说一次,复用 (reuse) 是件美好的事。如果你是一位数据结构专家, 你就会知道,实现 sets 的方法太多了,其中一种便是在底层采用 linked lists 。而你 又刚好知道,标准程序库有一个 list template ,于是你决定复用它。 更明确地说,你决定让你那个萌芽中的 Set template 继承 std: :list 。也就是 让 Set 继承 list 。毕竟在你的实现理念中 Set 对象其实是个 list 对象。你 于是声明 Set template 如下: template class Set: public std::list { II将 list 应用于 Set 。错误做法。 每件事看起来都很好,但实际上有些东西完全错误。一如条款 32 所说,如果 D 是一种 B ,对 B 为真的每一件事情对 D 也都应该为真。但 list 可以内含重复元素, 如果数值 3051 被安插到 list 两次,那个 list 将内含两笔 30510 Set 不可以 内含重复元素,如果数值 3051 被安插到 Set 两次,这个 set 只内含一笔 3051 。 因此 "Set 是一种 list" 并不为真,因为对 list 对象为真的某些事情对 Set 对象 并不为真。 由于这两个 classes 之间并非 is唱的关系,所以 public 继承不适合用来塑模它 们。正确的做法是,你应当了解, Set 对象可根据一个 list 对象实现出来 z template II将 list 应用于 Set 。正确做法。 class Set ( public: bool member(const T& item) const; void insert(const T& item); void remove(const T& item); std::size t size() const; private: std::list rep; II用来表述Set 的数据 Effective C++ 中文版,第三版186 6 继承与面向对象设计 Set 成员函数可大量倚赖 list 及标准程序库其他部分提供的机能来完成,所以 其实现很直观也很简单,只要你熟悉以 STL 编写程序: template bool Set::member(const T& item) const { return std::find(rep.begin() , rep.end() , item) != rep.end( ); template void Set::insert(const T& item) if (!member(item» rep.push back(item); template void Set::remove(const T& item) typename std::list::iterator it = std::find(rep.begin() , rep.end() , item); if (it != rep.end(» rep.erase(it); template std::size t Set::size( ) const { return rep.size( ); II见条款42 对 II 这些函数如此简单,因此都适合成为inlining 候选人。但请记住,在做出任何 与 inlining 有关的决定之前,应该先看看条款30 。 也许有人主张,如果Set 接口遵循 STL 容器的协议,就更符合条款18 对设计 接口的警告: "让它容易被正确使用,不易被误用"。但是这儿如果要遵循那些协 议,需得为 Set 添加许多东西,那将模糊了它和list 之间的关系。由于Set 和 list 之间的关系是本条款的重点,所以我们以教学清澈度交换STL 兼容性。此外, Set 接口也不该造成"对Set 而言无可置辩的权利"黯然失色,那个权利是指它和list 间的关系。这关系并非is-a (虽然最初似乎是) ,而是 is-implemented-in-terms-ot。 请记住 ·复合(composition) 的意义和 public 继承完全不同。 ·在应用域 (application domain) ,复含意味 has-a (有一个)。在实现域 (implementation domain) ,复合意味 is-implemented-in-terms-of(根据某物实 现出)。 Ej作ctive C++中文版F 第三版6 条款 39: 明智而审慎地使用 private 继承 条款 39: 明智而审慎地使用 private 继承 Use private inheritance judiciously. 187 条款 32 曾经论证过 C++ 如何将 public 继承视为 is-a 关系。在那个例子中我们 有个继承体系,其中 class Student l::J. public 形式继承 class Perso 口,于是编译器在 必要时刻(为了让函数调用成功)将 Students 暗自转换为 Persons 。现在我再重复 该例的一部分,并以 private 继承替换 public 继承: class Person { ... }; class Student: pr工vate Person { ... }; void eat(const Person& pi; void study(const Student& s); Person p; Student s; eat (p) ; eat (s) ; II这次改用 private 继承 II 任何人都会吃 II 只有学生才在校学习 lip 是人 lis 是学生 II 没问题.p 是人,会吃。 II错误!吓,难道学生不是人?! 显然 private 继承并不意味 is唱关系。那么它意味什么? "哇喔! "你说,"在我们探讨其意义之前,可否先搞清楚其行为。到底 private 继承的行为如何呢? "晤,统御 private 继承的首要规则你刚才已经见过了:如果 classes 之间的继承关系是 private. 编译器不会自动将一个 derived class 对象(例如 Student) 转换为)个 base class 对象(例如 Person) 。这和 public 继承的情况不 同。这也就是为什么通过 s 调用 eat 会失败的原因。第二条规则是,由 private base class 继承而来的所有成员,在 derived class 中都会变成 private 属性,纵使它们在 base class 中原本是 protected 或 public 属性。 够了,现在让我们开始讨论其意义。 Private 继承意味 implemented-in-terms-of (根据某物实现出)。如果你让 class D 以 private 形式继承 class B ,你的用意是为 了采用 class B 内已经备妥的某些特性,不是因为 B 对象和 D 对象存在有任何观念上 的关系。 private 继承纯粹只是一种实现技术(这就是为什么继承自一个private base class 的每样东西在你的 class 内都是 private: 因为它们都只是实现枝节而已)。借 用条款 34 提出的术语. pnvate 继承意味只有实现部分被继承,接口部分应略去。 如果 D 以 private 形式继承 B. 意思是 D 对象根据 B 对象实现而得,再没有其他意涵 了。 Private 继承在软件"设计"层面上没有意义,其意义只及于软件实现层面。 Effective C++ 中文版,第三版188 6 继承与面向对象设计 Private 继承意味 is-implemented-in-terms-ot (根据某物实现出) ,这个事实有 点令人不安,因为条款 38 才刚指出复合( composition) 的意义也是这样。你如何 在两者之间取舍?答案很简单:尽可能使用复合,必要时才使用 private 继承。何时 才是必要?主要是当 protected 成员和/或 vi阳 al 函数牵扯进来的时候。其实还有一 种激进情况,那是当空间方面的利害关系足以踢翻 private 继承的支柱时。稍后我们 再来操这个心,毕竟它只是→种激进情况。 假设我们的程序涉及 Widgets ,而我们决定应该较好地了解如何使用 Widgets 。 例如我们不只想要知道 Widget 成员函数多么频繁地被调用,也想、知道经过一段时 间后调用比例如何变化。要知道,带有多个执行阶段( execution phases) 的程序, 可能在不同阶段拥有不同的行为轮廓 (behavioral profiles) 。例如编译器在解析 (parsing) 阶段所用的函数,大大不同于在最优化(optimization) 和代码生成 (code generation) 阶段所使用的函数。 我们决定修改 Widget class ,让它记录每个成员函数的被调用次数。运行期间 我们将周期性地审查那份信息,也许再加上每个Widget 的值,以及我们需要评估 的任何其他数据。为完成这项工作,我们需要设定某种定时器,使我们知道收集统 计数据的时候是否到了。 我们宁可复用既有代码,尽量少写新代码,所以在自己的工具百宝箱中翻箱倒 柜,并且很开心地发现了这个class: class Timer { public: explicit Timer (int tickFrequency); virtual void onTick() const; II定时器每滴答一次, II此函数就被自动调用一次。 这就是我们找到的东西。一个Timer 对象,可调整为以我们需要的任何频率滴 答前进,每次滴答就调用某个virtual 函数。我们可以重新定义那个virtual 函数,让 后者取出 Widget 的当时状态。完美! 为了让 Widget 重新定义 Timer 内的 virtual 函数, Widget 必须继承自 Timer。 但 public 继承在此例并不适当,因为Widget 并不是个 Timer。是呀, Widget客户 总不该能够对着一个Widget 调用 onTick吧,因为观念上那并不是Widget接口的 一部分。如果允许那样的调用动作,很容易造成客户不正确地使用Widget 接口, El知ctive C++中文版F 第三版6 条款 39: 明智而审慎地使用 private 继承 189 那会违反条款 18 的忠告: "让接口容易被正确使用,不易被误用"。在这里, public 继承不是个好策略。 我们必须以 private 形式继承 Timer: class Widget: private Timer { private: virtual void onTick() const; II查看 Widget 的数据...等等。 藉由 private 继承, Timer 的 public onTick 函数在 Widget 内变成 private,而我 们重新声明(定义)时仍然把它留在那儿。再说一次,把onTick放进 public 接口 内会误导客户端以为他们可以调用它,那就违反了条款18 。 这是个好设计,但不值几文钱,因为private 继承并非绝对必要。如果我们决定 以复合(composition)取而代之,是可以的。只要在Widget 内声明一个嵌套式private class,后者以 public 形式继承 Timer 并重新定义 onTick,然后放一个这种类型的 对象于 Widget 内。下面是这种解法的草样: class Widget { private: class W工dgetTimer: public Timer ( public: virtual void onTick() const; }; WidgetT工mer timer; 这个设计比只使用pnvate 继承要复杂一些些,因为它同时涉及public 继承和复 合,并导入一个新 class (WidgetTimer) 。坦白说我展示它主要是为了提醒你,解 决一个设计问题的方法不只一种,而训练自己思考多种做法是值得的(看看条款 35) 。 尽管如此,我可以想出两个理由,为什么你可能愿意(或说应该)选择这 样的 public 继承加复合,而不是选择原先的private 继承设计。 首先,你或许会想设计Widget使它得以拥有derived classes,但同时你可能会 想阻止 derived classes 重新定义 onTick。如果 Widget继承自 Timer,上面的想法就 不可能实现,即使是private 继承也不可能。(还记得吗,条款35 曾说 derived classes 可以重新定义 virtual 函数,即使它们不得调用它。 J 但如果 WidgetTimer 是 Widget E1知ctive C++ 中文版,第三版190 6 继承与面向对象设计 内部的一个 private 成员并继承 Timer , Widget 的 derived classes 将无法取用 WidgetTimer ,因此无法继承它或重新定义它的 virtual 函数。如果你曾经以 Java 或 C# 编程并怀念"阻止 derived classes 重新定义 virtual 函数"的能力(也就是 Java 的 final 和创的 sealed) ,现在你知道怎么在 C++ 中模拟它了。 第二,你或许会想要将 Widget 的编译依存性降至最低。如果 Widget 继承 Timer ,当 Widget 被编译时 Timer 的定义必须可见,所以定义 Widget 的那个文件 恐怕必须#i nclude Time r. h 。但如果 WidgetTimer 移出 Widget 之外而 Widget 内含 指针指向一个 WidgetTimer , Widget 可以只带着一个简单的 WidgetTimer 声明式, 不再需要#i nclude 任何与 Timer 有关的东西。对大型系统而言,如此的解糯 (decouplings) 可能是重要的措施。关于编译依存性的最小化,详见条款 31 。 稍早我曾谈到, private 继承主要用于"当一个意欲成为 derived class 者想访问 一个意欲成为 base class 者的 protected 成分,或为了重新定义一或多个 virtual 函数飞 但这时候两个 classes 之间的概念关系其实是 is-implemented-in-terms-of (根据某物 实现出)而非 is唱。然而我也说过,有一种激进情况涉及空间最优化,可能会促使 你选择 "private 继承"而不是"继承加复合"。 这个激进情况真是有够激进,只适用于你所处理的 class 不带任何数据时。这 样的 classes 没有 non-static 成员变量,没有 virtual 函数(因为这种函数的存在会为 每个对象带来一个 vp口,见条款 7) ,也没有 vi阳al base classes (因为这样的 base classes 也会招致体积上的额外开销,见条款40) 。于是这种所谓的 empty classes 对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而由于技术上的 理由, C++ 裁定凡是独立(非附属)对象都必须有非零大小,所以如果你这样做 z class Empty { }; II没有数据,所以其对象应该不使用任何内存 class HoldsAnlnt ( pr~飞Tate: int x; Empty e; II应该只需要一个int 空间 II应该不需要任何内存 你会发现 sizeof(HoldsAnlnt) > sizeof(int); 喔欧,一个Empty 成员变量竟 然要求内存。在大多数编译器中 sizeof( 由lpty) 获得1,因为面对"大小为零之独 Effective C++ 中文版,第二版6 条款 39: 明智而审慎地使用 private 继承 191 立(非附属)对象",通常 C++ 官方勒令默默安插一个 char 到空对象内。然而齐 位需求( alignment ,见条款 SO) 可能造成编译器为类似 Holds An lnt 这样的 class 加上一些衬垫 (padding) .所以有可能 Holds An lnt 对象不只获得一个 char 大小, 也许实际上被放大到足够又存放一个 into 在我试过的所有编译器中,的确有这种 情况发生。 但或许你注意到了,我很小心地说"独立(非附属) "对象的大小一定不为零。 也就是说,这个约束不适用于 derived class 对象内的 base class 成分,因为它们并非 独立(非附属)。如果你继承Em pty ,而不是内含一个那种类型的对象: class HoldsAnlnt: private Empty { private: int X; 几乎可以确定 sizeof(HoldsAnlnt)==sizeof(川的。这是所谓的EBa (empty base optimization; 空白基类最优化) .我试过的所有编译器都有这样的结果。如果 你是一个程序库开发人员,而你的客户非常在意空间,那么值得注意EBa 。另外还 值得知道的是. EBa 一般只在单一继承(而非多重继承)下才可行,统治C++对象 布局的那些规则通常表示 EBa 无法被施行于"拥有多个 base" 的 derived classes 身 上。 现实中的 "empty" classes 并不真的是 empty。虽然它们从未拥有 non-static 成员 变量,却往往内含 typedefs, enums, static 成员变量,或 non-virtual 函数。 STL 就有 许多技术用途的 empty classes ,其中内含有用的成员(通常是 typedefs) ,包括 base classes unary_function 和 binary_function,这些是"用户自定义之函数对象" 通常会继承的 classeso 感谢 EBa 的广泛实践,使这样的继承很少增加 derived classes 的大小。 尽管如此,让我们回到根本。大多数 classes 并非 empty ,所以 EBa 很少成为 private 继承的正当理由。更进一步说,大多数继承相当于 is唱,这是指 public 继承, 不是 private 继承。复合和 private 继承都意味 is-implemented-in-terms-of,但复合比 较容易理解,所以无论什么时候,只要可以,你还是应该选择复合。 当你面对"并不存在 is-a 关系"的两个 classes. 其中一个需要访问另一个的 protected 成员,或需要重新定义其一或多个 virtual 函数, private 继承极有可能成为 正统设计策略。即使如此你也己经看到,一个混合了 public 继承和复合的设计,往 往能够释出你要的行为,尽管这样的设计有较大的复杂度。"明智而审慎地使用 £1知 cfive c++ 中文版,第三版192 6 继承与面向对象设计 private 继承"意味,在考虑过所有其他方案之后,如果仍然认为 private 继承是"表 现程序内两个 classes 之间的关系"的最佳办法,这才用它。 请记住 • Private 继承意味 is-implemented-in-terms of (根据某物实现出)。它通常比复合 (composition) 的级别低。但是当 derived class 需要访问 prot饵ted base class 的 成员,或需要重新定义继承而来的virtual 函数时,这么设计是合理的。 ·和复合(composition) 不同, private 继承可以造成 empty base 最优化。这对致 力于"对象只寸最小化"的程序库开发者而言,可能很重要。 条款 40: 明智而审慎地使用多重继承 Use multiple inheritance judiciously. -旦涉及多重继承 (multiple inheritance: MI) , C++ 社群便分为两个基本阵 营。其中之一认为如果单一继承 (single inheritance: SO 是好的,多重继承一定更 好。另一派阵营则主张,单一继承是好的,但多重继承不值得拥有(或使用)。本 条款的主要目的是带领大家了解多重继承的两个观点。 最先需要认清的一件事是,当 MI 进入设计景框,程序有可能从一个以上的 base classes 继承相同名称(如函数、 typedef 等等〉。那会导致较多的歧义(ambiguity) 机会。例如: class Borrowableltem { public: void checkOut( ); class ElectronicGadget { private: bool checkOut( ) const; class MP3Player: publ工 c Borrowableltem, public ElectronicGadget { ... ); MP3Player mp; mp.checkOut( ); Effective C++ 中文版,第三版 II 图书馆允许你借某些东西 II 离开时进行检查 II 执行自我检测,返回是否测试成功 II 注意这里的多重继承 II(某些图书馆愿意借出 MP3 播放器) II 这里, class 的定义不是我们的关心重点 II歧义!调用的是哪个 checkl∞ t?6 条款 40: 明智而审慎地使用多重继承 193 注意此例之中对 checkωt 的调用是歧义(模棱两可)的,即使两个函数之中 只有一个可取用 (Borrowableltem 内的 checkOut 是 public , ElectronicGadget 内的却是 private) 。这与 C++ 用来解析 (resolving) 重载函数调用的规则相符z 在 看到是否有个函数可取用之前,C++ 首先确认这个函数对此调用之言是最佳匹配。 找出最佳匹配函数后才检验其可取用性。本例的两个checkωts有相同的匹配程度 (译注:因此才造成歧义),没有所谓最佳匹配。因此ElectronicGadget::checkOut 的可取用性也就从未被编译器审查。 为了解决这个歧义,你必须明白指出你要调用哪一个base class 内的函数z mp.Borrowableltem::checkOut( ); II哎呀,原来是这个checkOut.. . 你当然也可以尝试明确调用 ElectronicGadget::check∞t ,但然后你会获得 一个"尝试调用 private 成员函数"的错误。 多重继承的意思是继承一个以上的 base classes ,但这些 base classes 并不常在 继承体系中又有更高级的 base classes ,因为那会导致要命的"钻石型多重继承" : .,}...{ class File { ... ); class I口putFile: public File { .,. ); class OutputFile: public File { ... ); class IOFile: public InputFile, public OutputFile 任何时候如果你有一个继承体系而其中某个base class 和某个 derived class 之间 有→条以上的相通路线(就像上述的File 和 IOFile 之间有两条路径,分别穿越 InputFile和 OutputFile) ,你就必须面对这样一个问题:是否打算让base class 内的成员变量经由每一条路径被复制?假设File class 有个成员变量 fileName,那 么 IOFile 内该有多少笔这个名称的数据呢?从某个角度说,IOFile 从其每一个 base class 继承一份,所以其对象内应该有两份fileName成员变量。但从另→个角 度说,简单的逻辑告诉我们,IOFile 对象只该有一个文件名称,所以它继承自两个 base classes 而来的 fileName不该重复。 C忡在这场辩论中并没有倾斜立场:两个方案它都支持一一虽然其缺省做法是 执行复制(也就是上一段所说的第一个做法)。如果那不是你要的,你必须令那个 带有此数据的 class (也就是 File) 成为一个 virtual base class。为了这样做,你必 Ej知ctive C++中文版,第三版194 6 继承与面向对象设计 须令所有直接继承自它的 classes 采用 "vi阳 al 继承": .,}...{ class File { ... }; class I口putFile: virtual public File { }; class OutputFile: virtual public F工 le { }; class IOFile: public InputFile, public OutputFile C++ 标准程序库内含一个多重继承体系,其结构 就如右图那样,只不过其classes 其实是 class templates,名称分别是 basic_ios, basic istream, basic ostream和 basic iostream,而非这里的 File, InputFile, OutputFile 和 IOFile 。 从正确行为的观点看, public 继承应该总是 virtual 。如果这是唯一一个观点, 规则很简单:任何时候当你使用 public 继承,请改用 virtual public 继承。但是,啊 呀,正确性并不是唯一观点。为避免继承得来的成员变量重复,编译器必须提供若 干幕后戏法,而其后果是:使用 virtual 继承的那些 classes 所产生的对象往往比使 用 non-virtual 继承的兄弟们体积大,访问 virtual base classes 的成员变量时,也比访 问 non-virtual base classes 的成员变量速度慢。种种细节因编译器不同而异,但基本 重点很清楚 z 你得为 virtual 继承付出代价。 vi阳 al 继承的成本还包括其他方面。支配 "virtual base classes 初始化"的规则 比起 non-virtual bases 的情况远为复杂且不直观。virtual base 的初始化责任是由继承 体系中的最低层 (most derived) class 负责,这暗示(1) classes 若派生自 virtual bases 而需要初始化,必须认知其 virtual bases一一不论那些 bases 距离多远, (勾当一个 新的 derived class 加入继承体系中,它必须承担其 virtual bases (不论直接或间接) 的初始化责任。 我对 virtual base classes (亦相当于对 virtual 继承〉的忠告很简单。第一,非必 要不使用 virtual bases 。平常请使用 non-virtual 继承。第二,如果你必须使用 vi血ml base classes ,尽可能避免在其中放置数据。这么一来你就不需担心这些classes 身上 的初始化(和赋值)所带来的诡异事情了。 Java 和 .NET 的 Interfaces 值得注意, 它在许多方面兼容于 C++ 的 virtual base classes ,而且也不允许含有任何数据。 现在让我们看看下面这个用来塑模"人"的 C++ Inte巾ce class (见条款 3 1) : Effective C++ 中文版,第三版6 条款 40: 明智而审慎地使用多重继承 class IPerson { public: virtual -IPerson(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; 195 IPerson 的客户必须以 Iperson 的 pointers 和 references 来编写程序,因为抽象 classes 无法被实体化创建对象。为了创建一些可被当做 IPerson 来使用的对象, IPerson 的客户使用 factory functions (工厂函数,见条款3 1)将"派生自 IPerson 的具象 classes" 实体化: Ilfacω,ry 也nction (工厂函数) ,根据→个独一无二的数据库B 创建一个 Person对象。 II条款 18 告诉你为什么返回类型不是原始指针。 std::trl::shared-ptr makePerson(DatabaseID personldentifier); II这个函数从使用者手上取得一个数据库B DatabaseID askUserForDatabaseID(); DatabaseID id(askUserForDatabaseID(»); std::trl::shared-ptr pp(makePerson(id)); II创建一个对象支持Iperson接口。 II藉由 Iperson 成员函数处理*pp. 但是 makePerson如何创建对象并返回-个指针指向它呢?无疑地一定有某些 派生自工Perso日的具象 class,在其中 makePerson可以创建对象。 假设这个 class 名为 CPerson。就像具象 class 一样, CPerson必须提供"继承 自工Perso口"的 pure virtual 函数的实现代码。我们可以从无到有写出这些东西,但 更好的是利用既有组件,后者做了大部分或所有必要事情。例如,假设有个既有的 数据库相关 class,名为 Personlnfo,提供 CPerson所需要的实质东西t class Personlnfo { public: explicit Personlnfo(DatabaseID pid); virtual -Personlnfo(); virtual const char* theName() const; virtual const char* theBirthDate( ) const; private: virtual const char* valueDe lirr郎en() const; II详下 virtual const char* valueDelimClose() const; Effective C++ 中文版,第三版196 6 继承与面向对象设计 你可以说这是个旧式 class ,因为其成员函数返回 const char* 而不是 string 对象。尽管如此,如果鞋子合脚,干嘛不穿它?这个 class 的成员函数的名称已经 暗示我们其结果有可能很令人满意。 你会发现, PersonInfo 被设计用来协助以各种格式打印数据库字段,每个字 段值的起始点和结束点以特殊字符串为界。缺省的头尾界限符号是方括号(中括 号) ,所以(例如)字段值"Ri ng-tailed Lemur" 将被格式化为: [Ring-tailed Lemur] 但由于方括号并非放之四海人人喜爱的界限符号,所以两个virtual 函数 valueDelimOpen 和 valueDelimClose允许 derived classes 设定它们自己的头尾界限 符号。 PersonInfo 成员函数将调用这些virtual 函数,把适当的界限符号添加到它 们的返回值上。以PersonInfo::theName为例,代码看起来像这样: const char* PersonInfo::valueDelimOpen() const { return "["; II缺省的起始符号 const char* Person工 nfo::valueDelimClose() const return "]"; II缺省的结尾符号 const char* PersonInfo::theName() const { II保留缓冲区给返回值使用:由于缓冲区是 static ,因此会被自动初始化为"全部是 0" static char value[Max Formatted Field Value Length]; II写入起始符号 std::strcpy(value, valueDelimOpen()); 现在,将 value 内的字符串添附到这个对象的name 成员变量中 (小心,避免缓冲区超限) II写入结尾符号 std::strcat(value, valueDelimClose()); return value; 或许有人质疑 PersonInfo::t由heN怡恼a缸me 的 static 缓冲区,那将充斥超限问题和线程问题,见条款2 1) ,但是不妨暂时把这 样的疑问放两旁,把以下焦点摆中间:theName 调用 valueDelimOp en 产生字符串 起始符号,然后产生name 值,然后调用 valueDelimClose。由于 valueDelirr句en Efj告ctive C++中文版,第三版6 条款 40: 明智而审慎地使用多重继承 197 和 valueDelimClose 都是 vim泊l 函数, theName 返回的结果不仅取决于 PersonInfo 也取决于从 PersonInfo 派生下去的 classeso 身为 CPerson 实现者,这是个好消息,因为仔细阅读 IPerson 文档后,你发现 name 和 birthDate 两函数必须返回未经装饰(不带起始符号和结尾符号)的值。也 就是说如果有人名为 Homer ,调用其 name 函数理应获得 "Homer" 而不是 "[Homer]" 。 CPerson 和 PersonInfo 的关系是, PersonInfo 刚好有若干函数可帮助 CPerson 比较容易实现出来。就这样。它们的关系因此是 is-implemented-in-terms-of (根据 某物实现出) ,而我们知道这种关系可以两种技术实现:复合(见条款 38) 和 private 继承(见条款 39) 。条款 39 指出复合通常是较受欢迎的做法,但如果需要重新定 义 vi阳al 函数,那么继承是必要的。本例之中 CPerson 需要重新定义 value De limOpen 和 value De limClose ,所以单纯的复合无法应付。最直接的解法 就是令 CPerson 以 private 形式继承 PersonInfo ,虽然条款 39 也说过,只要多加一 点工作, CPerson 也可以结合"复合+继承"技术以求有效重新定义 PersonInfo 的 virtual 函数。此处我将使用 private 继承。 但 CPerson 也必须实现工 Person 接口,那需得以 public 继承才能完成。这导致 多重继承的一个通情达理的应用:将 "public 继承自某接口"和 "private 继承自某 实现"结合在一起: class IPerson { II 这个 class 指出需实现的接口 public: virtual -IPerson(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; II稍后被使用:细节不重要。 II这个 class 有若干有用函数, II可用以实现 IPerson接口。 class PersonInfo { public: explicit PersonInfo(DatabaseID pid); virtual -PersonInfo(); virtual const char* theName() const; virtual const char* theBirthDate( ) const; virtual const char* valueDelimOpe口() const; virtual const char* valueDelimClose() const; class DatabaseID { ... }; Effective C++ 中文版,第三版198 6 继承与面向对象设计 class CPerson: public IPerson, private Perso旧nfo { I I注意,多重继承 public: explicit CPerson(DatabaseID pid): Personlnfo(pid) {} virtual std::string name() const II实现启耍的IPerson成员函数 { return Personlnfo::theName(); } virtual std::string birthDate() const II剪处且要的工Person成员函数 { return Personlnfo::theBirthDate(); } private: const char* valueDelimOpen() const { return ""; } const char* valueDelimClose() const { return ""; } 在 UML 图中这个设计看起来像这样: 这个例子告诉我们,多重继承也有它的合理用途。 II 重新定义 II 继承而来的 II virtual II" 界限函数" 故事结束前,请容我说,多重继承只是面向对象工具箱里的一个工具而已。和 单一继承比较,它通常比较复杂,使用上也比较难以理解,所以如果你有个单一继 承的设计方案,而它大约等价于一个多重继承设计方案,那么单一继承设计方案几 乎一定比较受欢迎。如果你唯一能够提出的设计方案涉及多重继承,你应该更努力 想一想一一几乎可以说一定会有某些方案让单一继承行得通。然而多重继承有时候 的确是完成任务之最简洁、最易维护、最合理的做法,果真如此就别害怕使用它。 只要确定,你的确是在明智而审慎的情况下使用它。 请记住 ·多重继承比单→继承复杂。它可能导致新的歧义性,以及对 virtual 继承的需要。 • virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果 virtual base classes 不带任何数据,将是最具实用价值的情况。 ·多重继承的确有正当用途。其中一个情节涉及"public 继承某个 Interface class" 和 "private 继承某个协助实现的class" 的两相组合。 E1知ctive C++中文版,第三版7 条款 41 :了解隐式接口和编译期多态 199 7 模板与泛型编程 Templates and Generic Programming C++ templates 的最初发展动机很直接:让我们得以建立"类型安全"( type-safe) 的容器如 vector, list 和 map。然而当愈多人用上 templates ,他们发现 templates 有能力完成愈多可能的变化。容器当然很好,但泛型编程(generic programming)一一­ 写出的代码和其所处理的对象类型彼此独立 更好。 STL 算法如 for each, find 和 merge 就是这一类编程的成果。最终人们发现,C++ template 机制自身是一部完 整的图灵机 (Turing-complete) :它可以被用来计算任何可计算的值。于是导出了 模板元编程 (template metaprogramming) ,创造出"在 C++ 编译器内执行并于编 译完成时停止执行"的程序。近来这些日子,容器反倒只成为C++ template 馅饼上 的一小部分。然而尽管 template 的应用如此宽广,有一组核心观念一直支撑着所有 基于 template 的编程。那些观念便是本章焦点。 本章无法使你变成一个专家级的 template 程序员,但可以使你成为一个比较好 的 template 程序员。本章也会给你必要信息,使你能够扩展你的template 编程,到 达你所渴望的境界。 条款 41: 了解隐式接口和编译期多态 Understand implicit interfaces and compile-time pol归no甲hism. 面向对象编程世界总是以显式接口 (explicit interfaces) 和运行期多态 (runtime polymorphism) 解决问题。举个例子,给定这样(无甚意义)的class: class Widget { public: Widget(); virtual -Widget(); Effective C++ 中文版F 第三版200 virtual std::size t size( ) const; virtual void normalize(); void swap(Widget& other); 和这样(也是无甚意义)的函数: void doProcessing(Widget& w) { II 见条款 25 7 模板与泛型编程 if (w.size() > 10 && w != someNastyWidget) { Widget temp (w) ; temp.normalize(); temp. swap (w) ; 我们可以这样说 doProcessing 内的 w: ·由于 w 的类型被声明为 Widget ,所以 w 必须支持 Widget 接口。我们可以在源 码中找出这个接口(例如在 Widget 的 .h 文件中) ,看看它是什么样子,所以我 称此为一个显式接口 ( explicit inte价ce) , 也就是它在源码中明确可见。 ·由于 Widget 的某些成员函数是 virtual , w 对那些函数的调用将表现出运行期多 态 (runtime polymorphism) , 也就是说将于运行期根据w 的动态类型(见条款 37) 决定究竟调用哪一个函数。 Templates 及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式 接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit inte功ces) 和编译期多态 (compile-timepolymorphism) 移到前头了。若想知道那是什么,看看 当我们将 doProcessing从函数转变成函数模板 (function template) 时发生什么事: template void doProcessing(T& w) if (w.size() > 10 && w != someNastyWidget) { T temp (w); temp.no盯阻lize (); temp.swap(w) ; 现在我们怎么说 doProcessing 内的 w 呢? • w 必须支持哪一种接口,系由 template 中执行于 w 身上的操作来决定。本例看来 w 的类型 T 好像必须支持 size , normalize 和 swap 成员函数、 copy 构造函数(用 £1知ctive C++ 中文版,第三版7 条款 41 :了解隐式接口和编译期多态 201 以建立 temp) 、不等比较 (inequality comparison ,用来比较 someNasty-Widget) 。 我们很快会看到这并非完全正确,但对目前而言足够真实。重要的是,这一组 表达式(对此 template 而言必须有效编译)便是 T 必须支持的一组隐式接口 (implicit inte功ce) 。 ·凡涉及 w 的任何函数调用,例如 operator>和 operator! =,有可能造成 template 具现化 (instantiated) ,使这些调用得以成功。这样的具现行为发生在编译期。 "以不同的 template 参数具现化 function templates" 会导致调用不同的函数,这 便是所谓的编译期多态 ( compile-time polymorphism) 。 纵使你从未用过 templates ,应该不陌生"运行期多态"和"编译期多态"之间 的差异,因为它类似于"哪→个重载函数该被调用" (发生在编译期)和"哪一个 virtual 函数该被绑定" (发生在运行期)之间的差异。显式接口和隐式接口的差异 就比较新颖,需要更多更贴近的说明和解释。 通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。 例如 Widget class: class Widget ( public: Widget(); virtual -Widget(); virtual std::size t size( ) const; virtual void normalize(); 飞loid swap(Widget& other); 其 public 接口由一个构造函数、一个析构函数、函数size, normalize, swap 及 其参数类型、返回类型、常量性(constnesses )构成。当然也包括编译器产生的 copy 构造函数和 copy assignment操作符(见条款们。另外也可以包括 typedefs ,以及 如果你大胆违反条款 22 (令成员变量为 private) 而出现的 public 成员变量。 隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式(valid expressions) 组成。再次看看 dOProcessingtemplate 一开始的条件z Effective C++ 中文版第三版202 template void doProcessing( T& w) if (w.size() > 10 && w != someNastyWidget) ( T (w 的类型)的隐式接口看来好像有这些约束: 7 模板与泛型编程 ·它必须提供一个名为 size 的成员函数,该函数返罔一个整数值。 ·它必须支持一个 operator!= 函数,用来比较两个 T 对象。这里我们假设 someNastyWidget 的类型为 T 。 真要感谢操作符重载 (operator overloading) 带来的可能性,这两个约束都不 需要满足。是的, T 必须支持 size 成员函数,然而这个函数也可能从 base class 继 承而得。这个成员函数不需返回一个整数值,甚至不需返回一个数值类型。就此而 言,它甚至不需要返回一个定义有 operator> 的类型!它唯一需要做的是返回一个 类型为 x 的对象,而 X 对象加上一个 int( 10 的类型)必须能够调用一个 operator> 。 这个 operator> 不需要非得取得一个类型为 x 的参数不可,因为它也可以取得类 型 y 的参数,只要存在一个隐式转换能够将类型 x 的对象转换为类型 y 的对象! 同样道理, T 并不需要支持 operator! =,因为以下这样也是可以的 :operator!= 接受一个类型为 x 的对象和一个类型为 Y 的对象, T 可被转换为 x 而 someNastyWidget 的类型可被转换为 y ,这样就可以杳效调用 operator!= 。 (偷偷告诉你,以上分析并未考虑这样的可能性: operator&& 被重载,从一 个连接词改变为或许完全不同的某种东西,从而改变上述表达式的意义。) 当人们第一次以此种方式思考隐式接口,大多数的他们会感到头疼。但真的不 需要阿司匹林来镇痛。隐式接口仅仅是由一组有效表达式构成,表达式自身可能看 起来很复杂,但它们要求的约束条件一般而言相当直接又明确。例如以下条件式: if (w.size() > 10 && w != someNastyWidget) 关于函数 size, oper-ator>, operator&&或 operator!= 身上的约束条件,我 们很难就此说得太多,但整体确认表达式约束条件却很容易。 if 语旬的条件式必须 Effective C++ 中文版,第三版7 条款 42: 了解 typename 的双重意义 203 是个布尔表达式,所以无论涉及什么实际类型,无论 "w.size() > 10 && w != someNastyW idget"导致什么,它都必须与bool 兼容。这是template doProcessing 加诸于其类型参数(type parameter) T 的隐式接口的一部分。 doProcessing要求的 其他隐式接口 : copy 构造函数、 no四日lize 和 swap 也都必须对 T 型对象有效。 加诸于 template 参数身上的隐式接口,就像加诸于 class 对象身上的显式接口 一样真实,而且两者都在编译期完成检查。就像你无法以一种"与class 提供之显 式接口矛盾"的方式来使用对象(代码将通不过编译).你也无法在 template 中使 用"不支持 template 所要求之隐式接口"的对象(代码一样通不过编译)。 请记住 • classes 和 templates 都支持接口( interfaces) 和多态 (polymorphism) 。 ·对 classes 而言接口是显式的 (explicit) .以函数签名为中心。多态则是通过 virtual 函数发生于运行期。 ·对 template 参数而言,接口是隐式的(implicit) .奠基于有效表达式。多态则 是通过 template 具现化和函数重载解析( function overloading resolution) 发生于 编译期。 条款 42: 了解 typename 的双重意义 Understand the two meanings oftypename. 提一个问题z 以下 template 声明式中. class 和 typename template class Widget; template class Widget; II使用 "class" II使用 "typename" 答案:没有不同。当我们声明template 类型参数. class 和 typename 的意义 完全相同。某些程序员始终比较喜欢class. 因为可以少打几个字。其他人(包括 我〉比较喜欢 typename. 因为它暗示参数并非一定得是个class 类型。少数开发人 员在接受任何类型时使用 typename. 而在只接受用户自定义类型时保留旧式的 class。然而从 C++ 的角度来看,声明template 参数时,不论使用关键字class 或 typename. 意义完全相同。 然而 C++ 并不总是把 class 和 typename 视为等价。有时候你一定得使用 typename。为了解其时机,我们必须先谈谈你可以在template 内指涉 (referto) 的 两种名称。 Effective C++ 中文版,第三版204 7 模板与泛型编程 假设我们有个 template function. 接受一个 STL 兼容容器为参数,容器内持有 的对象可被赋值为 ints 。进→步假设这个函数仅仅只是打印其第二元素值。这是一 个无聊的函数,以无聊的方式实现,而且如稍后所言,它甚至不能通过编译。但请 暂时漠视那些事,下面是实践这个愚蠢想法的→种方式: template void print2nd(const C& container) II打印容器内的第二元素 II也意这不是有效的C++代码 if (container.size() >= 2) { C::const iterator iter(container.begin());11取得第一元素的迭代器 ++iter; II将 iter 移往第二元素 int value =吐出口 II将该元素复制到某个int. std::cout «value; II打印那个 int. 我在代码中特别强调两个 local 变量 iter 和 value 0 iter 的类型是 C::const iterator. 实际是什么必须取决于template 参数 Co template 内出现的名 称如果相依于某个template 参数,称之为从属名称 (dependentnames) 。如果从属 名称在 class 内呈嵌套状,我们称它为嵌套从属名称(nested dependent name) 。 C::const iterator就是这样一个名称。实际上它还是个嵌套从属类型名称(nested d写pendent type name) • 也就是个嵌套从属名称并且指涉某类型。 print2nd 内的另一个 local 变量 value. 其类型是 into int 是一个并不倚赖任 何 template 参数的名称。这样的名称是谓非从属名称(non-dependent names) 。我 不知道为什么不叫做独立名称 (independent names) 。如果你和我一样认为术语 "non-dependent" 令人憎恶,你我之间起了共鸣。但毕竟 "non-dependent" 己被定为 这→类名称的术语,所以请和我一样,眨眨眼睛然后顺从它吧。 嵌套从属名称有可能导致解析(parsing) 困难。举个例子,假设我们令print2nd 更愚蠢些,这样起头z template void print2nd(const C& container) { C::const iterator* x; 看起来好像我们声明 x 为一个 local 变量,它是个指针,指向-个 C::const iteratoro 但它之所以被那么认为,只因为我们"己经知道" C::const iterator 是个类型。如果 C::const iterator 不是个类型呢?如果 C 有个 static 成员变量而碰巧被命名为 const iterator. 或如果 x 碰巧是个 global E1作ctive C++中文版,第三版7 条款 42: 了解 typename 的双重意义 205 if (container.size() >= 2) ( C::const iterator iter(container.begin()); 变量名称呢?那样的话上述代码就不再是声明一个local 变量,而是一个相乘动作z C::const iterator乘以 x。当然啦,这昕起来有点疯狂,但却是可能的,而撰写 C++ 解析器的人必须操心所有可能的输入,甚至是这么疯狂的输入。 在我们知道 C 是什么之前,没有任何办法可以知道C::const iterator是否为 一个类型。而当编译器开始解析template print2nd时,尚未确知 C 是什么东西。 C++ 有个规则可以解析 (resolve) 此一歧义状态:如果解析器在template 中遭遇一 个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下 嵌套从属名称不是类型。此规则有个例外,稍后我会提到。 把这些记在心上,现在再次看看print2nd起始处: template void print2nd(const C& container) { II 这个名称被 II 假设为非类型 现在应该很清楚为什么这不是有效的 C++ 代码了吧。 iter 声明式只有在 C::const iterator是个类型时才合理,但我们并没有告诉C++ 说它是,于是C++ 假设它不是。若要矫正这个形势,我们必须告诉C++ 说 C::const iterator是个 类型。只要紧临它之前放置关键字typename 即可 2 template II 这是合法的 C++代码 void print2nd(const C& conta 工 ner) if (container.size() >= 2) ( typen缸ne C::const iterator iter(container.begin()); 一般性规则很简单:任何时候当你想要在template 中指涉一个嵌套从属类型名 称,就必须在紧临它的前一个位置放上关键字typenameo (再提醒一次,很快我 会谈到一个例外。) typename只被用来验明嵌套从属类型名称:其他名称不该有它存在。例如下面 这个 function template,接受一个容器和一个"指向该容器"的迭代器: template II 允许使用 "typenarne" (或"c1 as唱 II) void f(const C& container, II 不允许使用可严name" typename C::iterator iter); II一定要使用 "ty阳naIlle" Effective C++ 中文版,第二版206 7 模板与泛型编程 上述的 C 并不是嵌套从属类型名称(它并非嵌套于任何"取决于 template 参数" 的东西内) ,所以声明 container 时并不需要以 typenarne 为前导,但 c: :iterator 是个嵌套从属类型名称,所以必须以typenarne 为前导。 " typenarne 必须作为嵌套从属类型名称的前缀词"这一规则的例外是, typenarne 不可以出现在 base classes list 内的嵌套从属类型名称之前,也不可在 member initialization list (成员初值列)中作为base class 修饰符。例如: I Ibase class list 中 II 不允许 lit ypenarne". Ilmem. init. list 中 II 不允许 "typenarne". typenarne Base::Nested temp; II嵌套从属类型名称, II既不在 base class list 中也不在 memo init. list 中, II 作为一个 baseclass 修饰符需加上 typename. template class Derived: public Base::Nested { public: e>夏plicit Derived(int x) Base::Nested(x) 这样的不一致性真令人恼恨,但一旦你有了一些经验,勉勉强强还能接受它。 让我们看看最后一个typenarne 例子,那是你将在真实程序中看到的代表性例 子。假设我们正在撰写一个function tempI耐,它接受一个迭代器,而我们打算为该 迭代器指涉的对象做一份local 复件(副本) temp。我们可以这么写: template void workWithlterator(IterT iter) type口arne std:: 工 terator_traits::value_typetemp(*iter); 别让 std::iterator traits::value type 惊吓了你,那只不过是标 准阳its class (见条款 47) 的一种运用,相当于说"类型为工terT 之对象所指之物 的类型"。这个语句声明一个local 变量 (tern肘,使用工terT 对象所指物的相同 类型,并将 temp 初始化为 iter 所指物。如果工terT 是 vector::iterator, temp 的类型就是 into 如果 IterT 是 list::iterator, t四p 的类型就是 string。由于 std::iterator_traits::value_type是个嵌套从属类型名 称 (value type 被嵌套于 iterator traits 之内而工terT 是个 template 参数) ,所以我们必须在它之前放置typenarne。 El作ctive C++中文版,第三版7 条款 43: 学习处理模板化基类内的名称 207 如果你认为 std::iterator traits::value type 读起来不畅快,想 象一下打那么长的字又是什么光景。如果你像大多数程序员一样,认为多打几次这 些字实在很恐怖,那么你应该会想建立一个typedef。对于 traits 成员名称如 value_type (再次请看条款47 提供的回its 信息) ,普遍的习惯是设定typedef名 称用以代表某个traits 成员名称,于是常常可以看到类似这样的local typedef: template void workWithlterator(IterT iter) { typedef type口a口e std::iterator_traits::value_type value_type; value type temp(*iter); 许多程序员最初认为把 "typedef typenarne" 并列颇不合谐,但它实在是指涉 "嵌套从属类型名称"的一个合理附带结果。你很快会习惯它,毕竟你有强烈的动 机一一你希望多打几次 typen回巴 std::iterator_traits::value_type 吗? 作为结语,我应该提到, typenarne相关规则在不同的编译器上有不同的实践。 某些编译器接受的代码原本该有typenarne却遗漏了:原本不该有typenarne却出现 了:还有少数编译器(通常是较旧版本)根本就拒绝type口 arne。这意味 typenarne 和"嵌套从属类型名称"之间的互动,也许会在移植性方面带给你某种温和的头疼。 请记住 ·声明 template 参数时,前缀关键字class 和 typenarne可互换。 ·请使用关键字typenarne标识嵌套从属类型名称:但不得在base class lists (基类 列〉或 member initialization list (成员初值列〉内以它作为 base class 修饰符。 条款 43: 学习处理模板化墓类内的名称 Know how to access names in templatized base classes. 假设我们需要撰写一个程序,它能够传送信息到若干不同的公司去。信息要不 译成密码,要不就是未经加工的文字。如果编译期间我们有足够信息来决定哪一个 信息传至哪一家公司,就可以来用基于template 的解法: Effective C++ 中文版,第三版208 class CompanyA ( public: void sendCleartext{const std::string& msg); void sendEncrypted{const std::stri口g& msg); class CompanyB ( public: 飞roid sendCleartext(const std::string& msg); void sendEncrypted(const std::string& msg); 7 模板与泛型编程 class Msglnfo { ... }; template class MsgSender ( public: II针对其他公司设计的classes. II这个 class 用来保存信息,以备将来产生信息 II构造函数、析构函数等等。 void sendClear(const Msglnfo& info) std: : string msg; 在这儿,根据info 产生信息J Company c; c.sendCleartext(msg); void sendSecret(const Msglnfo& info) II类似 sendClear,唯一不同是 { ... } I I这里调用 c. sendEncrypted 这个做法行得通。但假设我们有时候想要在每次送出信息时志记(log) 某些信 息。 derived class 可轻易加上这样的生产力,那似乎是个合情合理的解法z template class LoggingMsgSender: public MsgSender ( public: II构造函数、析构函数等等. void sendClearMsg(const Msg工nfo& info) 将"传送前"的信息写至log; sendClear(info); 将"传送后"的信息写至log; Effective C++ 中文版,第三版 II 调用 base class 函数:这段码无法通过编译。7 条款 43: 学习处理模板化基类内的名称 209 注意这个 derived class 的信息传送函数有一个不同的名称( sendClearMsg) • 与其 base class 内的名称 (sendClear) 不同。那是个好设计,因为它避免遮掩"继 承而得的名称" (见条款 33 ),也避免重新定义一个继承而得的 non-virtual 函数(见 条款 36) 。但上述代码无法通过编译,至少对严守规律的编译器而言。这样的编译 器会抱怨 sendClear 不存在。我们的眼睛可以看到 sendClear 的确在 base class 内, 编译器却看不到它们。为什么? 问题在于,当编译器遭遇 class template LoggingMsgSender定义式时,并不知 道它继承什么样的classo 当然它继承的是MsgSender.但其中的 Company 是个 template 参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它 是什么。而如果不知道Company是什么,就无法知道class MsgSender看 起来像什么一一更明确地说是没办法知道它是否有个sendClear函数。 为了让问题更具体化,假设我们有个class CompanyZ坚持使用加密通讯z class Comp~nyZ { II 这个 class 不提供 public: IlsendCleartext函数 void sendEncrypted(const std::string& msg); 一般性的MsgSender template 对 CompanyZ并不合适,因为那个template 提供 了一个 sendClear 函数(真中针对其类型参数Company调用了 sendCleartext函 数) ,而这对 CompanyZ对象并不合理。欲矫正这个问题,我们可以针对Co呻anyZ 产生一个 MsgSender特化版z {>η白VAnapmo户」 S < qJ estM··aclsips­ma-hue-utcp II 一个全特化的 IIMsgSender; 它和一般 te呻 late 相同, II 差别只在于它删掉了 sendClear 。 void sendSecret(const Msglnfo& info) { ... } 注意 class 定义式最前头的" temp late<>" 语法象征这既不是 template 也不是 标准 class. 而是个特化版的 MsgSender template. 在 template 实参是 CompanyZ 时 被使用。这是所谓的模板全特化 (total template specialization) : template MsgSender 针对类型 CompanyZ 特化了,而且其特化是全面性的,也就是说一旦类型参数被定 义为 CompanyZ. 再没有其他template 参数可供变化。 El知ctive C++中文版,第三版210 7 模板与泛型编程 现在, MsgSender 针对 CompanyZ 进行了全特化,让我们再次考虑 derived class LoggingMsgSender: template class LoggingMsgSender: public MsgSender ( public: void sendClearMsg{const MsgInfo& i口 fo) 将"传送前"的信息写至 log; sendClear(工nfo) ; II如果 Company == CompanyZ,这个函数不存在。 将"传送后"的信息写至log; 正如注释所言,当base class 被指定为 MsgSender 时这段代码不合 法,因为那个class 并未提供 sendClear函数!那就是为什么C++ 拒绝这个调用的 原因:它知道base class templates 有可能被特化,而那个特化版本可能不提供和一 般性 template 相同的接口。因此它往往拒绝在templatizedbase classes (模板化基类, 本例的 MsgSender": template class LoggingMsgSender: public MsgSender ( public: void sendClearMsg{const Msg工nfo& info) 将"传送前"的信息写至log; this->sendClear(info); 将"传送后"的信息写至log; £1作ctive C++中文版,第三版 II成立,假设sendClear将被继承。7 条款 43: 学习处理模板化基类内的名称 211 第二是使用 using 声明式。如果你己读过条款 33 ,这个解法应该会令你感到熟 悉。条款 33 描述 using 声明式如何将"被掩盖的 base class 名称"带入一个 derived class 作用域内。我们可以这样写下 sendClearMsg: template class LoggingMsgSender: public MsgSender { public: us工ng MsgSender class LoggingMsgSender: public MsgSender { public: void sendClearMsg(const Msglnfo& info) { MsgSender::sendClear(info); IIOK, 假设 sendClear II将被继承下来。 但这往往是最不让人满意的→个解法,因为如果被调用的是virtual 函数,上述 的明确资格修饰 (explicit qualification) 会关闭"virtual 绑定行为"。 从名称可视点(visibility point) 的角度出发,上述每一个解法做的事情都相同: 对编译器承诺 "base class template 的任何特化版本都将支持其一般(泛化〉版本所 提供的接口"。这样一个承诺是编译器在解析(parse) 像 LoggingMsgSender 这样 的 derived class template 时需要的。但如果这个承诺最终未被实践出来,往后的编 EJ.知ctive C++中文版,第二版212 7 模板与泛型编程 译最终还是会还给事实一个公道。举个例子,如果稍后的源码内含这个: LoggingMsgSe 口der zMsgSender; Msglnfo msgData; II在 msgData 内放置信息。 zMsgSender.sendClearMsg(msgData); II错误!无法通过编译。 其中对 sendClearMsg 的调用动作将无法通过编译,因为在那个点上,编译器 知道 base class 是个 template 特化版本 MsgSender,而且它们知道那个 class 不提供 sendClear函数,而后者却是sendClearMsg尝试调用的函数。 根本而言,本条款探讨的是,面对"指涉base class members" 之无效 references , 编译器的诊断时间可能发生在早期(当解析derived class template 的定义式时) , 也可能发生在晚期(当那些 templates 被特定之 template 实参具现化时) 0 C++ 的 政策是宁愿较早诊断,这就是为什么"当 base classes 从 templates 中被具现化时" 它假设它对那些 base classes 的内容毫无所悉的缘故。 请记住 ·可在 derived class templates 内通过"this->" 指涉 base class templates 内的成员 名称,或藉由一个明白写出的 "base class 资格修饰符"完成。 条款 44: 将与参数无关的代码捆离 templates Factor parameter-independent code out oftemplates. Templates 是节省时间和避免代码重复的一个奇方妙法。不再需要键入20 个类 似的 classes 而每一个带有 15 个成员函数,你只需键入一个class template,留给编 译器去具现化那20 个你需要的相关classes 和 300 个函数。 (class templates 的成员 函数只有在被使用时才被暗中具现化,所以只有在这300 个函数的每一个都被使用, 你才会获得这300 个函数。) Function templates 有类似的诉求。替换写许多函数, 你只需要写一个 function template ,然后让编译器做剩余的事情。技术是不是很崇高 伟大,呵呵。 是的,晤……有时候啦。如果你不小心,使用 templates 可能会导致代码膨胀 (code bloat) : 其二进制码带着重复(或几乎重复)的代码、数据,或两者。其结 果有可能源码看起来合身而整齐,但目标码(object code) 却不是那么回事。肥胖 不结实很难被视为时尚,所以你需要知道如何避免这样的二进制浮夸。 你的主要工具有个气势恢宏的名称:共性与变性分析(commonality and variability ana加 is) ,但其概念十分平民化。纵使你从未写过一个 template ,你始 £1知ctive C++ 中文版,第三版7 条款 44: 将与参数无关的代码抽离 templates 终做着这样的分析。 213 当你编写某个函数,而你明白其中某些部分的实现码和另→个函数的实现码实 质相同,你会很单纯地重复这些码吗?当然不。你会抽出两个函数的共同部分,把 它们放进第三个函数中,然后令原先两个函数调用这个新函数。也就是说,你分析 了两个函数,找出共同的部分和变化的部分,把共同部分搬到一个新函数去,保留 变化的部分在原函数中不动。同样道理,如果你正在编写某个 class ,而你明白其中 某些部分和另一个 class 的某些部分相同,你也不会重复这共同的部分。取而代之 的是你会把共同部分搬移到新 class 去,然后使用继承或复合(见条款 32 , 38 , 39) , 令原先的 classes 取用这共同特性。而原 classes 的互异部分(变异部分)仍然留在 原位置不动。 编写 templates 时,也是做相同的分析,以相同的方式避免重复,但其中有个 窍门。在 non-template 代码中,重复十分明确:你可以"看"到两个函数或两个 classes 之间有所重复。然而在 template 代码中,重复是隐晦的:毕竟只存在一份 template 源码,所以你必须训练自己去感受当 template 被具现化多次时可能发生的重复。 举个例子,假设你想为固定尺寸的正方矩阵编写一个 templateo 该矩阵的性质 之一是支持逆矩阵运算 (matrix inversion) 。 template class SquareMatrix ( public: void invert( ); Iltemplate支持 n x n 矩阵,元素是 II类型为 T 的 objects; 见以下 II 关于 size t 参数的信息 II 求逆矩阵 这个 template 接受一个类型参数 T ,除此之外还接受一个类型为 size t 的参数, 那是个非类型参数 (non-type parameter) 。这种参数和类型参数比起来较不常见, 但它们完全合法,而且就像本例一样,相当自然。 现在,考虑这些代码: SquareMatrix sml; sml.invert( ); II调用 Squar~但trix::invert Square~但trix 5m2; sm2 .invert( ); II调用 SquareMatrix::invert El知ctive C++中文版,第三版214 7 模板与泛型编程 这会具现化两份 invert 。这些函数并非完完全全相同,因为其中一个操作的 是 5 巧矩阵而另一个操作的是 10*10 矩阵,但除了常量 5 和 10 ,两个函数的其他部 分完全相同。这是 template 引出代码膨胀的一个典型例子。 如果你看到两个函数完全相同,只除了一个使用 5 而另一个使用 10 ,你会怎么 做?你的本能会为它们建立一个带数值参数的函数,然后以 5 和 10 来调用这个带 参数的函数,而不重复代码。你的本能很好,下面是对 SquareMatrix 的第一次修 改 z template II 与尺寸无关的 baseclass , class SquareMatrixBase { II 用于正方矩阵 protected: void invert(std::size t matrixSize); II 以给定的尺寸求逆矩阵 template class SquareMatrix: private SquareMatrixBase { private: using SquareMatrixBase::invert; II避免遮掩base 版的 Ilinvert; 见条款 33 public: void invert( ){ this->invert(n); } II制造一个 inline 调用,调用 Ilbase class 版的 invert。稍后 II 说明为什么这儿出现this-> 就如你所看到,带参数的 invert 位于 base class SquareMatrixBase 中。和 SquareMatrix 一样, SquareMa trixBase 也是个 template,不同的是它只对"矩阵 元素对象的类型"参数化,不对矩阵的尺寸参数化。因此对于某给定之元素对象类 型,所有矩阵共享同一个(也是唯一一个)SquareMatrixBase class。它们也将因 此共享这唯一一个class 内的 invert。 SquareMa trixBase::invert只是企图成为"避免derived classes 代码重复"的 一种方法,所以它以protected替换 public。调用它而造成的额外成本应该是0 , 因为 derived classes 的工nverts 调用 base class 版本时用的是 inline 调用(这里的 inline 是隐晦的,见条款30) 。这些函数使用 "this->" 记号,因为若不这样做, 便如条款 43 所说,模板化基类 (templatized base classes ,例如 SquareMatrixBase class SquareMatrixBase { protected: SquareMatrixBase(std::size_t η , T女 pMem) size(n) , pData(pMem) {) void setDataPtr(T* ptr) { pData = ptr; } private: std::size t size; T* pData; II存储矩阵大小和一个 II指针,指向矩阵数值。 II重新赋值给pData。 II矩阵的大小。 II指针,指向矩阵内容。 这允许 derived classes 决定内存分配方式。某些实现版本也许会决定将矩阵数 据存储在 SquareMatrix对象内部z template class SquareMatrix: private SquareMatrixBase { public: SquareMatrix( ) II送出矩阵大小和 : SquareMatrixBase(n, data) {} II数据指针给base class。 private: T data[n勺1] ; Effective C++ 中文版,第三版216 7 模板与泛型编程 这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一种做法是 把每→个矩阵的数据放进heap (也就是通过 new 来分配内存) : template class SquareMatrix: private SquareMatrixBase { public: SquareMatrix( ) : SquareMatr 工 xBase (n, 0) , pData(new T[口六口] ) { this->setDataPtr(pData.get()); II将 base class 的数据指针设为null , II为矩阵内容分配内存, II将指向该内存的指针存储起来, } II 然后将它的→个副本交给 base class .,a←」aDDF>T和 SquareMa trix 成员函 数,我们也没机会传递一个 SguareMatrix 对象到一个期望获得 SquareMatrix 的函数去。很棒,对吗? 是的,很棒,但必须付出代价。硬是绑着矩阵尺寸的那个 invert 版本,有可 能生成比共享版本(其中尺寸乃以函数参数传递或存储在对象内)更佳的代码。例 如在尺寸专属版中,尺寸是个编译期常量,因此可以藉由常量的广传达到最优化, 包括把它们折进被生成指令中成为直接操作数。这在"与尺寸无关"的版本中是无 法办到的。 从另一个角度看,不同大小的矩阵只拥有单一版本的 invert ,可减少执行文 件大小,也就因此降低程序的 working set (译注:见下说明)大小,并强化指令高 速缓存区内的引用集中化(locality of reference) 。这些都可能使程序执行得更快速, 超越"尺寸专属版"invert 的最优化效果。哪一个影响占主要地位?欲知答案,唯 一的办法是两者都尝试并观察你的平台的行为以及面对代表性数据组时的行为。 译注:所谓 working set 是指对一个在"虚内存环境"下执行的进程(process) 而言, 其所使用的那一组内存页 (pages) 。 另一个效能评比所关心的主题是对象大小。如果你不介意,可将前述"与矩阵 大小无关的函数版本"搬至base class 内,这会增加每一个对象的大小。例如在我 R萨ctive C++中文版,第三版7 条款 44: 将与参数无关的代码抽离 templates 217 刚才展示的例子中,每一个 Square Matrix 对象都有→个指针指向 SquareMatrixBase class 内的数据。虽然每个derived class 已经有)种取得数据的 办法,这会对每一个SquareMatrix对象增加至少一个指针那么大。当然也可以修 改设计,拿掉这些指针,但是再一次,这其中需要若干取舍。例如令base class 贮 存一个 protected指针指向矩阵数据,会导致丧失封装性,如条款22 所言。也可 能导致资源管理上的混乱和复杂:是的,如果base class 存储一个指针指向矩阵数 据,那些数据空间也许是动态分配获得,也许存储于derived class 对象内(如稍早 所见) ,如何判断这个指针该不该被删除昵?这样的问题有其答案,但你愈是尝试 精密的做法,事情变得愈是复杂。从某个角度看,一点点代码重复反倒看起来有点 幸运了。 这个条款只讨论由non-type template parameters( 非类型模板参数)带来的膨胀, 其实 type parameters (类型参数)也会导致膨胀。例如在许多平台上int 和 long 有 相同的二进制表述,所以像 vector 和飞rector 的成员函数有可能完全 相同一一-这正是膨胀的最佳定义。某些连接器 (linkers) 会合并完全相同的函数实 现码,但有些不会,后者意味某些 templates 被具现化为 int 和 long 两个版本,并 因此造成代码膨胀(在某些环境下)。类似情况,在大多数平台上,所有指针类型 都有相同的二进制表述,因此凡 templates 持有指针者(例如 list, list, list勺等等)往往应该对每一个成员 函数使用唯一一份底层实现。这很具代表性地意味,如果你实现某些成员函数而它 们操作强型指针 (strongly typed pointe时,即 T 仆,你应该令它们调用另一个操作 无类型指针 (untypedpointers , 即 void*) 的函数,由后者完成实际工作。某些 C++ 标准程序库实现版本的确为飞rector, deque 和 list 等 templates 做了这件事。如果 你关心你的 templates 可能出现代码膨胀,也许你会想让你的 templates 也做相同的 事情。 请记住 • Templates 生成多个 classes 和多个函数,所以任何 template 代码都不该与某个造 成膨胀的 template 参数产生相依关系。 ·因非类型模板参数 (non-type template parameters) 而造成的代码膨胀,往往可 消除,做法是以函数参数或 class 成员变量替换 template 参数。 ·因类型参数(type parameters) 而造成的代码膨胀,往往可降低,做法是让带有 完全相同二进制表述 (binary representations) 的具现类型 (instantiation types) 共享实现码。 Effective C++ 中文版,第三版218 7 模板与泛型编程 条款 45: 运用成员函数模板接受所青兼容类型 Use member function templates to accept "all compatible types." 所谓智能指针 (Smartpointers) 是"行为像指针"的对象,并提供指针没有的 机能。例如条款13 曾经提及 std: :auto-ptr 和 trl: : shared-ptr 如何能够被用来 在正确时机自动删除 heap-based 资源。 STL 容器的迭代器几乎总是智能指针:无疑 地你不会奢望使用 11++" 将一个内置指针从 linked list 的某个节点移到另一个节点, 但这在 list: : iterators 身上办得到。 真实指针做得很好的-件事是,支持隐式转换 (implicit conversions) 0 Derived class 指针可以隐式转换为base class 指针, "指向 non-const对象"的指针可以转 换为"指向 const 对象"……等等。下面是可能发生于三层继承体系的一些转换: class Top { ... }; class Middle: public Top { ... }; class Bottom: public Middle { .., }; Top* ptl = new Middle; Top* pt2 = new Bottαn; const Top* pct2 = ptl; II将 Middle* 转换为 Top* II将 Bottom* 转换为 Top* II将 Top* 转换为 const Top* 但如果想在用户自定的智能指针中模拟上述转换,稍稍有点麻烦。我们希望以 下代码通过编译: template class SrnartPtr { public: explicit SmartPtr(T* realPtr); SmartPtr ptl = SmartPtr(new Middle); SmartPtr pt2 = SmartPtr (new Bottom); SmartPtr pct2 = ptl; II智能指针通常 II 以内置(原始)指针完成初始化 II将臼artPtr 转换为 II SrnartPtr II将SmartPtr转换为 II SrnartPtr II将臼artPtr 转换为 II SmartPtr 但是,同一个 template 的不同具现体(instantiations) 之间并不存在什么与生俱 来的固有关系(译注:这里意指如果以带有base-derived 关系的民 D 两类型分别具 现化某个 template. 产生出来的两个具现体并不带有base-derived 关系) ,所以编译 器视缸自rtptr 和部血rtPtr 为完全不同的 classes,它们之间的关 系并不比……晤……并不比vector 和 Widget 更密切,呵呵。为了获得我 们希望获得的SrnartPtr classes 之间的转换能力,我们必须将它们明确地编写出来。 Effective C++ 中文版,第二版7 条款 45: 运用成员函数模板接受所有兼容类型 219 Templates 和泛型编程( Generic Programming) 在上述智能指针实例中,每一个语句创建了一个新式智能指针对象,所以现在 我们应该关注如何编写智能指针的构造函数,使其行为能够满足我们的转型需要。 一个很具关键的观察结果是:我们永远无法写出我们需要的所有构造函数。在上述 继承体系中,我们根据一个Smartptr 或一个 SmartPtr构造出 一个 SmartPtr 对象又 必须能够根据其他智能指针构造自己。假设日后添加了: class BelowBottom: public Bottom { ... }; 我们因此必须令 SmartPtr对象得以生成 SmartPtr 对 象,但我们当然不希望-再修改SmartPtr template 以满足此类需求。 就原理而言,此例中我们需要的构造函数数量没有止尽,因为一个template 可 被无限量具现化,以致生成无限量函数。因此,似乎我们需要的不是为SmartPtr 写一个构造函数,而是为它写一个构造模板。这样的模板(templates)是所谓 member 户nction templates (常简称为 member templates) , 其作用是为 class 生成函数: template class SmartPtr { public: template SmartPtr(const SmartPtr& other); Ilmember template, II 为了生成 copy 构造函数 以上代码的意思是,对任何类型 T 和任何类型 U ,这里可以根据 SmartPtr 生 成一个 SmartPtr 一一因为 Smartptr 有个构造函数接受一个 SmartPtr 参数。这一类构造函数根据对象 U 创建对象 t (例如根据 SmartPtr 创建一俨 SmartPtr创建一个 SmartPtr,却不希望根据一个SmartPtr创建一个 SmartPtr 创建一个 Srr旧 tPtr ,因为现实中并没有"将 int* 转换为 double 户的对应隐式转换行为。是的,我们必须从某方面对这一 member template 所创建的成员函数群进行拣选或筛除。 假设 Smartptr 遵循 auto ptr 和 trl::shared ptr 所提供的榜样,也提供一 个 get 成员函数,返回智能指针对象(见条款15) 所持有的那个原始指针的副本, 那么我们可以在"构造模板"实现代码中约束转换行为,使它符合我们的期望: template class SmartPtr ( public: template SmartPtr(const SmartPtr& other) II 以 other 的 heldPtr : heldPtr(other.get()) ( ...) II 初始化 this 的 heldptr T女 get() const ( return heldptr; ) pr 工 vate: T* heldPtr; ) ; II 这个 SmartPtr 持有的内置(原始)指针 我使用成员初值列 (member initialization list) 来初始化 Smartptr 之内类 型为?的成员变量,并以类型为U女的指针(由 SmartPtr 持有)作为初值。 这个行为只有当"存在某个隐式转换可将一个v 指针转为一个?指针"时才能通 过编译,而那正是我们想要的。最终效益是SmartPtr 现在有了一个泛化 copy 构造函数,这个构造函数只在其所获得的实参隶属适当(兼容)类型时才通过编译。 member function templates (成员函数模板)的效用不限于构造函数,它们常扮 演的另一个角色是支持赋值操作。例如TRl 的 sharedytr (见条款 13) 支持所有 "来自兼容之内置指针、 trl: : sharedytrs、 autoytrs和 t r1 :: weakytrs (见条 款 54) "的构造行为,以及所有来自上述各物(trl::weakytrs 除外)的赋值操 作。下面是 TRl 规范中关于 trl:: shared_ptr 的一份摘录,其中强烈倾向声明 template 参数时采用关键字class 而不采用 type口ame(条款 42 曾说过,两者的意 义在此语境下完全相同)。 template class sharedytr ( public: template II 构造,来自任何兼容的 explicit shared ptr(Y* p); II 内置指针、 template shared_ptr(shared_ptr ∞,nst& r); I I 或 shared_ptr 、 template explicit shared_ptr(weakytr∞nst& r); I I或 weakytr 、 template e葛plicit sharedytr(auto-ptr 〈Y〉&r);// 或 autoytr. Effective C++ 中文版 F 第三版7 条款 45: 运用成员函数模板接受所有兼容类型 221 Ilcopy 构造函数. II 泛化 copy 构造函数. template II 赋值,来自倒可兼容的 shared-ptr& operator=(shared_ptr∞,nst& r); Ilshared-ptr、 template II 或 auto ptr. shared-ptr& operator=(auto_ptr& r); 上述所有构造函数都是e}φlicit,惟有"泛化 copy构造函数"除外。那意味 从某个 shared-ptr 类型隐式转换至另一个shared二ptr 类型是被允许的,但从某个 内置指针或从其他智能指针类型进行隐式转换则不被认可(如果是显式转换如cast 强制转型动作倒是可以)。另一个趣味点是传递给trl::shared ptr 构造函数和 assignment 操作符的 auto_ptrs 并未被声明为 const ,与之形成对比的则是 trl: : shared ptrs 和 trl: :weak ptrs 都以 const 传递。这是因为条款13 说过, 当你复制-个auto_ptrs,它们其实被改动了。 member function templates (成员函数模板)是个奇妙的东西,但它们并不改变 语言基本规则。条款5 说过,编译器可能为我们产生四个成员函数,其中两个是copy 构造函数和 copyassignment操作符。现在, trl: : shared_ptr 声明了一个泛化 copy 构造函数,而显然一旦类型 T 和 Y 相同,泛化 copy 构造函数会被具现化为"正常的" copy 构造函数。那么究竟编译器会暗自为 trl:: shared-ptr 生成一个 copy 构造函 数呢?或当某个 trl: : shared ptr 对象根据另一个同型的trl: : shared_ptr 对象 展开构造行为时,编译器会将"泛化copy构造函数模板"具现化呢? 一如我所说, member templates 并不改变语言规则,而语言规则说,如果程序 需要→个 copy 构造函数,你却没有声明它,编译器会为你暗自生成一个。在class 内声明泛化 copy构造函数(是个member template) 并不会阻止编译器生成它们自 己的 copy构造函数(一个non-template) ,所以如果你想要控制copy构造的方方面 面,你必须同时声明泛化copy 构造函数和"正常的" copy 构造函数。相同规则也 适用于赋值 ( assignment) 操作。下面是 trl::shared-ptr 的→份定义摘要,例证 上述所言: template class shared ptr ( public: shared ptr(shared ptr const& r); template shared ptr(shared ptr const& r); shared-ptr& operator=(shared_ptr const& r); Ilcopyass切nment. template II 泛化 copyassignmen t. shared-ptr& operator=(shared_ptr const& r); E1作ctive C++中文版,第二版222 7 模板与泛型编程 请记住 ·请使用 member function templates (成员函数模板〉生成"可接受所有兼容类型" 的函数。 .如果你声明 membertemplates 用于"泛化 copy构i造宦"或"泛化 as.挝5够nment忧t 操作" 你还是需要声明正常的copy构造函数和 Cωopya臼ssignmen附7忧t 操作符。 条款 46: 需要类型转换时请为模板定义非成员函数 Defme non-member functions inside templates when type conversions are desired. 条款 24 讨论过为什么惟有non-member函数才有能力"在所有实参身上实施隐 式类型转换",该条款并以Rational class 的 operato俨函数为例。我强烈建议你 继续看下去之前先让自己熟捻那个例子,因为本条款首先以一个看似无害的改动扩 充条款 24 的讨论:本条款将Rational和 operato俨模板化了3 template class Rational { public: Rational(const T& numerator = 0, const T& denominator = 1); const T numerator() const; const T denominator() const; II条款 20 告诉你为什么参数以 II passed by reference 方式传递。 II条款 28 告诉你为什么返回值 II 以 passed by value 方式传递。 II条款 3 告诉你为什么它们是∞nst. template const Rational operator* (const Rational& lhs, const Rational& rhs)}...{ 就像条款 24 一样,我们希望支持混合式 (mixed-mode) 算术运算,所以我们 希望以下代码顺利通过编译。我们也预期它会,因为它正是条款 24 所列的同一份 代码,唯一不同的是 Rational 和 operator* 如今都成了 templates: Rational oneHalf(l, 2); II这个例子米白条款24 , II唯一不同是Rational 改为 template。 Rational吐吐> result = oneHalf * 2; II错误!无法通过编译。 上述失败给我们的启示是,模板化的Rational 内的某些东西似乎和其 non-template版本不同。事实的确如此。在条款24 内,编译器知道我们尝试调用什 么函数(就是接受两个Rationals 参数的那个 operator*啦) ,但这里编译器不知 道我们想要调用哪个函数。取而代之的是,它们试图想出什么函数被名为operator* Ej作cfive C++中文版,第三版7 条款 46: 需要类型转换时请为模板定义非成员函数 223 的 template 具现化(产生)出来。它们知道它们应该可以具现化某个"名为 operator* 并接受两个Ra tional 参数"的函数,但为完成这一具现化行动,必须先算出 T 是什么。问题是它们没有这个能耐。 为了推导 T ,它们看了看 operatoρ 调用动作中的实参类型。本例中那些类型 分别是Ra tional (oneHalf 的类型)和 int (2 的类型)。每个参数分开考虑。 以 oneHalf 进行推导,过程并不困难。 operator 女的第一参数被声明为 Rational ,而传递给 operator女的第二实参(oneHalf) 的类型是 Rational<坷。,所以 T 一定是 into 其他参数的推导则没有这么顺利。operator舍 的第二参数被声明为Rational,但传递给 operator* 的第二实参 (2) 类型是 int。编译器如何根据这个推算出T? 你或许会期盼编译器使用Rational 的 non-explicit构造函数将 2 转换为 Ratio口al,进而将 T 推导为 int ,但它们 不那么做,因为在template 实参推导过程中从不将隐式类型转换函数纳入考虑。绝 不!这样的转换在函数调用过程中的确被使用,但在能够调用-个函数之前,首先 必须知道那个函数存在。而为了知道它,必须先为相关的function template 推导出 参数类型(然后才可将适当的函数具现化出来)。然而template 实参推导过程中并 不考虑采纳"通过构造函数丽发生的"隐式类型转换。条款24 不涉及 templates, 所以 template 实参推导不成为讨论议题。现在我们却是处在template pa忧 of C++( 见 条款I)领域内, template 实参推导是我们的重大议题。 只要利用一个事实,我们就可以缓和编译器在template 实参推导方面受到的挑 战: template class 内的 frier叫声明式可以指涉某个特定函数。那意味 class Rational可以声明 operator* 是它的一个 friend 函数。 Class templates 并不倚 赖 template 实参推导(后者只施行于 function templates 身上) ,所以编译器总是能 够在 class Rational 具现化时得知 To 因此,令 Rational class 声明适当的 operator食为其仕iend 函数,可简化整个问题: template class Rational { public: Effective C++ 中文版,第三版224 fr 工 end const Rational operator*(const Rational& lhs, const Rational& rhs); 7 模板与泛型编程 II声明 110阳ator* 函数, II细节详 fo template II 定义 const Rational operato沪 (const Rational& lhs, Ilo~tor* 函数。 const Rat工onal& rhs) { ... } 现在对 operator* 的混合式调用可以通过编译了,因为当对象。neHalf 被声 明为一个 Rational于是被具现化出来,而作为过程的 一部分,台iend 函数 operator* (接受 Rational参数)也就被自动声明出来。 后者身为一个函数而非函数模板(function temphlte) ,因此编译器可在调用它时使 用隐式转换函数(例如Rational 的 non-e即licit 构造函数) ,而这便是混合式调 用之所以成功的原因。 但是,此情境下的"成功"是个有趣的字眼,因为虽然这段代码通过编译,却 无法连接。稍后我马上回来处理这个问题,首先我要谈谈在Rational 内声明 operator* 的语法。 在一个 class template 内, template 名称可被用来作为 "template 和其参数"的 简略表达方式,所以在 Rational 内我们可以只写Rational 而不必写 Rational。本例中这只节省我们少打几个字,但若出现许多参数,或参数名称 很长,这可以节省我们的时间,也可以让代码比较干净。我谈这个是因为,本例中 的 operator* 被声明为接受井返回 Rationals (而非Rationals) 。如果它被 声明如下,一样有效: template class Rational { public: friend const Rational operator*(const Rational& lhs, const Rational& rhs); 然而使用简略表达方式(速记式)比较轻松也比较普遍。 现在回头想想我们的问题。混合式代码通过了编译,因为编译器知道我们要调 用哪个函数(就是接受一个Rational 以及又一个 Rational 的那个 £1知ctive C++中文版,第三版7 条款 46: 需要类型转换时请为模板定义非成员函数 225 operator 叶,但那个函数只被声明于Ra tional 内,并没有被定义出来。我们意图 令此 class 外部的 operator 女 template 提供定义式,但是行不通一一如果我们自己 声明了二个函数(那正是Ra tional template 内的作为) ,就有责任定义那个函数。 既然我们没有提供定义式,连接器当然找不到它! 或许最简单的可行办法就是将 operata 俨函数本体合并至其声明式内: template class Rational ( public: friend const Rational operator*(const Rational& lhs, const Rational& rhs) return Rational(lhs.numerator() * rhs.numerator( ), II实现码与 lhs.denominator() * rhs.denominator(»; II条款 24 同 这便如同我们所期望地正常运作了起来:对operator*的混合式调用现在可编译连 接并执行。万岁! 这项技术的一个趣味点是,我们虽然使用合iend,却与合iend 的传统用途"访 问 class 的 non-public 成分"毫不相干。为了让类型转换可能发生于所有实参身上, 我们需要一个non-member函数(条款 24) ;为了令这个函数被自动具现化,我们 需要将它声明在class 内部:而在 class 内部声明 non-member函数的唯一办法就是: 令它成为一个台iend。因此我们就这样做了。不习惯?是的。有效吗?不必怀疑。 一如条款 30 所说,定义于class 内的函数都暗自成为inline,包括像 operator* 这样的企iend 函数。你可以将这样的inline 声明所带来的冲击最小化,做法是令 operator*不做任何事情,只调用(个定义于class 外部的辅助函数。在本条款的例 子中,这样做并没有太大意义,因为operato俨已经是个单行函数,但对更复杂的 函数而言,那么做也许就有价值。"令仕iend 函数调用辅助函数"的做法的确值得 细究一番。 "Rational 是个 template"这→事实意味上述的辅助函数通常也是个template, 所以定义了Rational 的头文件代码,很典型地长这个样子: template class Rational; II声明Rational template EfJ告ctive C++ 中文版,第三版226 7 模板与泛型编程 template II 声明 helper tempi 耐 const Rational doMultiply(const Rational& lhs, const Rational& rhs); template class Rational ( public: friend const Rational operator*(const Rational& lhs, const Rational& rhs) ( return doMultiply(lhs, rhs); j II 令台iend 调用 helper 许多编译器实质上会强迫你把所有 template 定义式放进头文件内,所以你或许 需要在头文件内定义 doMultiply (一如条款 30 所言,这样的 templates 不需非得是 inline 不可) ,看起来像这样: template const Rational doMultiply(const Rational& lhs, const Rational& rhs) II若有必要, II在头文件内定义 Ilhelper旬mplate return Rational(lhs.numerator() * rhs.numerator() , lhs.denominator() * rhs.denominator()j; 作为一个 template, doMultiply当然不支持混合式乘法,但它其实也不需要。 它只被 operator* 调用,而 operator* 支持了混合式操作!本质上operator* 支 持了类型转换所需的任何东西,确保两个Rational 对象能够相乘,然后它将这两 个对象传给一个适当的doMultiply template 具现体,完成实际的乘法操作。协作 为成功之本,不是吗? 请记住 ·当我们编写一个class template,而它所提供之"与此template 相关的"函数支 持"所有参数之隐式类型转换"时,请将那些函数定义为"class template 内部 的企iend 函数"。 条款 47: 请使用 traits classes 表现类型信息 Use traits classes for information about types. STL 主要由"用以表现容器、选代器和算法"的templates 构成,但也覆盖若 干工具性 templates,其中一个名为 advance,用来将某个迭代器移动某个给定距离: R萨ctive C++中文版,第三版7 条款 47: 请使用 traits classes 表现类型信息 227 template II将迭代器向前移动d 单位。 void advance(IterT& iter, DistT d); II如果 d < 0 则向后移动。 观念上 advance 只是做 iter += d 动作,但其实不可以全然那么实践,因为只 有 random access (随机访问)迭代器才支持+=操作。面对其他威力不那么强大的 迭代器种类, advance 必须反复施行++或一,共 d 次。 日思,你不记得你的 STL 选代器分类 (categories) 了吗?没关系,让我们来一 次迷你回顾。 STL 共有 5 种选代器分类,对应于它们支持的操作。如 put 迭代器只 能向前移动,一次一步,客户只可读取(不能涂写)它们所指的东西,而且只能读 取一次。它们模仿指向输入文件的阅读指针 (read pointer) ; C++ 程序库中的 istream iterators 是这→分类的代表。Output迭代器a情况类似,但一切只为输出: 它们只向前移动,一次-步,客户只可涂写它们所指的东西,而且只能涂写一次。 它们模仿指向输出文件的涂写指针(write pointer) ; ostream_ i terators 是这一分 类的代表。这是威力最小的两个选代器分类。由于这两类都只能向前移动,而且只 能读或写其所指物最多一次,所以它们只适合"→次性操作算法" (one-pass algorithms) 。 另一个威力比较强大的分类是forward法代器。这种选代器可以做前述两种分 类所能做的每一件事,而且可以读或写其所指物一次以上。这使得它们可施行于多 次性操作算法 (multi-pass algorithms) 0 STL 并未提供单向 lipked list ,但某些程序 库有(通常名为 slis t) ,而指入这种容器的迭代器就是属于 forward 迭代器。指 入 TRI hashed 容器(见条款 54) 的也可能是这-分类。(译注:这里说"可能" 是因为 hashed 容器的迭代器可为单向也可为双向,取决于实现版本。) Bidirectional 选代器比上一个分类威力更大 z 它除了可以向前移动,还可以向后 移动。 STL 的 list 选代器就属于这一分类, set, multiset, map 和 multimap 的法 代器也都是这一分类。 最有威力的迭代器当属 random access 迭代器。这种迭代器比上一个分类威力 更大的地方在于它可以执行"迭代器算术",也就是它可以在常量时间内向前或向 后跳跃任意距离。这样的算术很类似指针算术,那并不令人惊讶,因为 random access 迭代器正是以内置(原始〉指针为榜样,而内置指针也可被当做 random access 法 代器使用。 vector , deque 和 stri 呵提供的选代器都是这一分类。 对于这 5 种分类, C十+标准程序库分别提供专属的卷标结构 (tag struct)加以 确认: Effective C++ 中文版,第三版228 7 模板与泛型编程 struct input_iterator_tag {I; struct output iterator tag {I; struct forward_iterator_tag: public input_iterator_tag { }; struct bidirectional_iterator_tag: public forward_iterator_tag { }; struct random_access_iterator_tag: public bidirectional iterator_tag {I; 这些 structs 之间的继承关系是有效的is-a 关系(见条款32): 是的,所有 forward 迭代器都是 input选代器,依此类推。很快我们会看到这个继承关系的效力。 现在回到 advance 函数。我们己经知道 STL 迭代器有着不同的能力,实现 advance 的策略之一是采用"最低但最普及"的选代器能力,以循环反复递增或递 减迭代器。然而这种做法耗费线性时间。我们知道random access 选代器支持选代 器算术运算,只耗费常量时间,因此如果面对这种选代器,我们希望运用其优势。 我们真正希望的是以这种方式实现advance: template void advance(IterT& iter, DistT d) if (iter is a random access iterator) { iter += d; II针对 random access 迭代器使用途代器算术运算 else { if (d >= 0) { while (d--) ++iter; } else { while (d++) - -iter; } II 针对其他迭代器分类 II 反复调用++或一 这种做法首先必须判断 iter 是否为 random access 迭代器,也就是说需要知道 类型 IterT 是否为 random access 选代器分类。换句话说我们需要取得类型的某些 信息。那就是 traits 让你得以进行的事:它们允许你在编译期间取得某些类型信息。 Traits 并不是 C++ 关键字或一个预先定义好的构件:它们是一种技术,也是一 个 C++ 程序员共同遵守的协议。这个技术的要求之一是,它对内置 (built-in) 类 型和用户自定义 (user-defined) 类型的表现必须一样好。举个例子,如果上述 advance 收到的实参是一个指针(例如 const char 叶和一个 int ,上述 advance 仍然必须 有效运作,那意味 traits 技术必须也能够施行于内置类型如指针身上。 " traits 必须能够施行于内置类型"意味"类型内的嵌套信息( nesting information) "这种东西出局了,因为我们无法将信息嵌套于原始指针内。因此类 型的回iω 信息必须位于类型自身之外。标准技术是把它放进一个template 及其一 或多个特化版本中。这样的templates 在标准程序库中有若干个,其中针对选代器 者被命名为 iterator traits: El如clive C++中文版,第三版7 条款 47: 请使用回 its classes 表现类型信息 229 template struct iterator traits; liter叩late ,用来处理 II遥代器分类的相关信息 如你所见, iterator traits 是个 struct。是的,习惯上回its 总是被实现为 struc饵,但它们却又往往被称为traits classes。 iterator traits 的运作方式是,针对每一个类型Ite凹,在 struct i terator traits 内一定声明某个 typedef 名为 iterator_category。这个 typedef 用来确认 IterT 的法代器分类。 iterator traits 以两个部分实现上述所言。首先它要求每一个"用户自定义 的选代器类型"必须嵌套-个typedef,名为 iterator_catego巧,用来确认适当的 卷标结构(tag struct>。例如 deque 的迭代器可随机访问,所以一个针对deque 选 代器而设计的 class 看起来会是这样子: template < ... > II 略而未写阳叩late 参数 class deque { public: class iterator { - public: typedef random access iterator tag iterator_category; list 的迭代器可双向行进,所以它们应该是这样: template < ... > class list { public: class iterator { public: typedef bidirectional iterator tag iterator_category; 至于 iterator traits,只是鹦鹉学舌般地响应iterator class 的嵌套式 typOOef: II类型 Ite凹的 i阳百ω,r_category其实就是用来表现"IterT说它自己是什么"。 II关于 "typedeftypename" 的运用,见条款42 。 template struct iterator traits { typedef type口arne IterT::iterator_category iterator_category; Effective C++ 中文版,第三版230 7 模板与泛型编程 这对用户自定义类型行得通,但对指针(也是一种选代器)行不通,因为指针 不可能嵌套 ~edefo iterator traits 的第二部分如下,专门用来对付指针。 为了支持指针迭代器, iterator traits 特别针对指针类塑提供→个偏特化 版本 (partial template specialization) 。由于指针的行径与 random access 选代器类 似,所以 iterator traits 为指针指定的选代器类型是: template struct iterator traits lltemplate偏特化 II针对内置指针 typedef random access iterator tag iterator category; 现在,你应该知道如何设计并实现一个traits class 了: ·确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将 来可取得其分类 (category)。 ·为该信息选择一个名称(例如iterator category) 。 ·提供一个 template 和一组特化版本(例如稍早说的iterator traits) ,内含 你希望支持的类型相关信息。 好,现在有了 iterator traits (实际上是 std::iterator traits,因为它 是 C++标准程序库的一部分),我们可以对 advance 实践先前的伪码(pseudocode) : template void advance(IterT& iter, DistT d) if (typeid(typename std: :iterator_traits<工 terT>::iterator_category) ==type工d(std::random_access_iterator_tag)) 虽然这看起来前景光明,其实并非我们想耍。首先它会导致编译问题,但我将 在条款 48 才探讨这一点,此刻有更根本的问题要考虑。 IterT 类型在编译期间获知, 所以 iterator_traits::iterator_category 也可在编译期间确定。但 if 语句却是在运行期才会核定。为什么将可在编译期完成的事延到运行期才做呢?这 不仅浪费时间,也造成可执行文件膨胀。 我们真正想要的是一个条件式(也就是一个 i f. ..else 语句)判断"编译期核 定成功"之类型。恰巧 C++ 有一个取得这种行为的办法,那就是重载( overloading) 。 E庐'dive C++ 中文版,第三版7 条款 47: 请使用 traits classes 表现类型信息 231 当你重载某个民数 f ,你必须详细叙述各个重载件的参数类型。当你调用 f , 编译器便根据传来的实参选择最适当的重载件。编译器的态度是"如果这个重载件 最匹配传递过来的实参,就调用这个 f; 如果那个重载件最匹配,就调用那个 f; 如果第三个 f 最匹配,就调用第三个 f! "依此类推。看到了吗,这正是一个针对 类型而发生的"编译期条件句"。为了让 advance 的行为如我们所期望,我们需要 做的是产生两版重载函数,内含 advance 的本质内容,但各自接受不同类型的 iterator category对象。我将这两个函数取名为doAdvance: template void doAdvance(工 terT& iter, DistT d , std::random access iterator tag) iter += d; II这份实现用于 IIrandom access II 迭代器 template II这份实现用于 void doAdvance(IterT& iter, DistT d , Ilbidireetional std::bidirectional iterator tag) II迭代器 if (d >= 0) { while (d--) ++iter; } else { while (d++) --iter; } template void doAdvance(IterT& iter, DistT d, std::input iterator tag) II这份实现用于 Ilinput法代器 if (d < 0 ) { throw std: lout of range ("Negative distance"); I I 详下 } while (d--) ++iter; 由于 forward_iterator_tag 继承自 input_ i terator_tag ,所以上述 doAdvance 的 input_iterator_tag 版本也能够处理 forward 选代器。这是 iterator tag structs 继承关系带来的一项红利。实际上这也是public 继承带来的 部分好处:针对base class 编写的代码用于derived class 身上也行得通。 advance 函数规范说,如果面对的是random access和 bidirectional:i!代器,则 接受正距离和负距离:但如果面对的是forward或 input选代器,则移动负距离会导 致不明确(未定义)行为。我所检验过的实现码都假设d 不为负,于是直接进入一 个冗长的循环法代,等待计数器降为0 。上述代码中我以抛出异常取而代之。两种 做法都有根据,但"无法预言发生何事"是"不明确行为"之祸源所在。 El如ctive C++中文版,第三版232 7 模板与泛型编程 有了这些 doAdvance 重载版本, advance 需要做的只是调用它们并额外传递一 个对象,后者必须带有适当的迭代器分类。于是编译器运用重载解析机制 (overloading resolution) 调用适当的实现代码: template void advance{IterT& iter, DistT d) { doAdvance{ II调用的 doAdvance版本 iter, d, I I对 iter 之迭代器分类而言 typename I I 必须是适当的。 std::iterator tra工ts<工 terT>::iterator category{) 现在我们可以总结如何使用一个traits class 了: ·建立一组重载函数(身份像劳工)或函数模板(例如doAdvance) ,彼此间的 差异只在于各自的回its 参数。令每个函数实现码与其接受之traits 信息相应和。 ·建立一个控制函数(身份像工头)或函数模板(例如advance) ,它调用上述 那些"劳工函数"并传递traits class 所提供的信息。 Traits 广泛用于标准程序库。其中当然有上述讨论的iterator traits,除了 供应 iterator_category 还供应另四份迭代器相关信息(其中最有用的是 value_type,见条款 42) 。此外还有 char traits 用来保存字符类型的相关信息, 以及 numeric limits 用来保存数值类型的相关信息,例如某数值类型可表现之最 小值和最大值等等:命名为numeric limits 杳点让人惊讶,因为traits classes 的名 称常以 "traits" 结束,但 numeric limits 却没有遵守这种风格。 TRl (条款 54 )导入许多新的 traits classes 用以提供类型信息,包括 is_fundamental (判断 T 是否为内置类型) , is_array (判断 T 是否为 数组类型),以及 is base of(Tl 和 T2 相同,抑或 Tl 是凹的 base class) 。 总计 TRl 一共为标准C++ 添加了 50 个以上的 traits classes。 请记住 • Traits classes 使得"类型相关信息"在编译期可用。它们以templates 和 "templates 特化"完成实现。 ·整合重载技术(overloading) 后, traits classes 有可能在编译期对类型执行 if...else 测试。 R庐ctive C++中文版,第三版7 条款 48: 认识 template 元编程 条款 48: 认识 template 元编程 Be aware oftemplate metaprogramming. 233 Template metaprogramming (TMP ,模板元编程)是编写 template-based C++ 程 序并执行于编译期的过程。花一分钟想想这个:所谓 template metaprogram (模板元 程序)是以 C++ 写成、执行于 C++ 编译器内的程序。一旦 TMP 程序结束执行, 其输出,也就是从 templates 具现出来的若干 C++ 源码,便会一如往常地被编译。 如果这没有带给你异乎寻常的印象,你一定是没有足够认真地思考它。 C++ 并非是为 template metaprogramming 而设计,但自从 TMP 于 1990s 初期 被发现以后,由于日渐被证明十分有用,其延伸部分很可能加入语言和标准程序库 内,使 τMP 更容易进行。是的, τMP 是被发现而不是被发明出来的。当 templates 加入 C++ 时 TMP 底层特性也就被引进了。对某些人而言唯一需要注意的是如何以 熟练巧妙而意想不到的方式使用 TMP 。 τ'MP有两个伟大的效力。第一,它让某些事情更容易。如果没有它,那些事情 将是困难的,甚至不可能的。第二,由于 template metaprograms 执行于 C++ 编译 期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通 常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP 的 C++ 程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需 求。然而将工作从运行期移转至编译期的另一个结果是,编译时间变长了。是的, 程序如果使用 TMP ,其编译时间可能远长于不使用 TMP 的对应版本。 考虑 p.228 导入的 STLadvance 伪码(位于条款 47 。或许你会想现在就阅读它, 因为本条款中我假设你己经熟悉条款 47 的内容)。就像 p.228 所示,我特别强调那 段代码的伪码部分 (pseudo part) : tercplate void advance(工 terT& iter, DistT d) { if (iter isarandomaccessiterator) { iter += d; II针对 random access 迭代器使用迭代器算术运算 } else { if (d >= O) { while (d--) ++iter; } else { while (d++) - -iter; } II 针对其他迭代器类型 /1 反复调用++或一 Effective C++ 中文版,第三版234 7 模板与泛型编程 advance(iter, 10); 我们可以使用 typeid让其中的伪码成真,取得C++ 对此问题的一个"正常" 解决方案一一所有工作都在运行期进行: template void advance(IterT& iter, DistT d) if (typeid(typename std::iterator traits::iterator category) == typeid(std::random access iterator tag)) { iter += d; II针对 random access迭代器,使用法代器算术运算。 else { if (d >= 0) { while (d一) ++iter;} II针对其他迭代器分类 else { while (d++) - -iter; } II 反复调用++或一 条款 47 指出,这个 typeid-based 解法的效率比 traits 解法低,因为在此方案中, (1) 类型测试发生于运行期而非编译期, (2)" 运行期类型测试"代码会出现在(或 说被连接于)可执行文件中。实际上这个例子正可彰显 TMP 如何能够比"正常的" C++ 程序更高效,因为 traits 解法就是 TMP。别忘了,位aits 引发"编译期发生于 类型身上的 if...else 计算"。 稍早我曾谈到,某些东西在 TMP 比在"正常的 "C++ 容易,对此 advance 也 提供了一个好例子。条款 47 曾经提过 advance 的 typeid-based 实现方式可能导致 编译期问题,下面就是个例子: std::list::iterator iter; II移动 iter 向前走 10 个元素: II上述实现无法通过编译。 下面这一版 advance 便是针对上述调用而产生的。将template 参数 IterT 和 DistT 分别替换为 iter 和 10 的类型之后,我们得到这些z void advance (std: :list: :iterator& iter, int d) { if (typeid(std::iterator tra工 ts::iterator>::iteratorcategory) == typeid(std::random access iterator tag}) { iter += d; II错误! else { if (d >= 0) { while (d--) ++iter; else { while (d++) --iter; E1知ctive C++中文版,第三版7 条款 48: 认识 template 元编程 235 问题出在我所强调的那一行代码使用了+=操作符,那便是尝试在一个 list::iterator 身上使用卡,但 list< 扣。 ::iterator 是 bidirectional 迭代 器(见条款 47) ,并不支持+=。只有 random access 迭代器才支持+=。此刻我们 知道绝不会执行起+=那一行,因为测试 typeid 的那一行总是会因为 list::iterators 而失败,但编译器必须确保所有源码都有效,纵使是不会 执行起来的代码!而当 iter 不是 random access 迭代器时 "iter += d" 无效。与 此对比的是 traits-based TMP 解法,其针对不同类型而进行的代码,被拆分为不同 的函数,每个函数所使用的操作(操作符)都可施行于该函数所对付的类型。 TMP 已被证明是个"图灵完全" (Turing-complete) 机器,意思是它的威力大 到足以计算任何事物。使用 TMP 你可以声明变量、执行循环、编写及调用函数… 但这般构件相对于"正常的 "C++ 对应物看起来很是不同,例如条款 47 展示的 TMP i f. ..else 条件句是藉由 templates 和其特化体表现出来。不过那毕竟是汇编语言层 级的 TMP 。针对 TMP 而设计的程序库(例如 Boost's MPL ,见条款 55) 提供更高 层级的语法一一尽管目前还不足以让你误以为那是"正常的" C忡。 为了再次浮光掠影地认识一下"事物在 TMP 中如何运作",让我们看看循环。 TMP 并没有真正的循环构件,所以循环效果系藉由递归 (recursion) 完成。如果你 对递归不太适应,恐怕必须在大胆投入 TMP 之前先解决它。 TMP 主要是个"函数 式语言" ( functionallanguage ) ,而递归之于这类语言就像电视之于美国通俗文化 一样地无法分割。 TMP 的递归甚至不是正常种类,因为 TMP 循环并不涉及递归函 数调用,而是涉及"递归模板具现化" (recursive template instantiation) 。 TMP 的起手程序是在编译期计算阶乘(factorial) 。这不是个令人特别兴奋的 程序,但 "hello world" 程序也不是,而两者对于语言的导入都很有帮助。 TMP 的 阶乘运算示范如何通过"递归模板具现化" (recursive template instantiation) 实现 循环,以及如何在 TMP 中创建和使用变量: t回~late II 一般情况: Factorial 的值是 struct Factorial { II n 乘以 Factorial 的值。 enurn { value =口* Factorial::value }; template<> struct Factorial { enurn { value = 1 }; II特殊情况: IIFactorial 的值是 1 Effective C++ 中文版,第三版236 7 模板与泛型编程 有了这个 template metaprogram (其实只是个单一的 template metafunction Factorial) ,只要你指涉 Factorial::value就可以得到n 阶乘值。 循环发生在 template 具现体 Factorial内部指涉另一个 template 具现体 Factorial之时。和所有良好递归一样,我们需要一个特殊情况造成递归结 束。这里的特殊情况是template 特化体 Factorial。 每个 Factorial template 具现体都是一个struct,每个 struct 都使用 enum hack (见条款 2) 声明一个名为 value 的 TMP 变量, value 用来保存当前计算所得的阶 乘值。如果 TMP 拥有真正的循环构件, value 应该在每次循环内获得更新。但由 于 TMP 系以"递归模板具现化" (recursive template instantiation) 取代循环,每个 具现体有自己的一份 value ,而每个 value 有其循环内的适当值。 你可以这样使用 Factorial: int main () std::cout « Factorial<5>::value; std::cout « Factorial::value; II 印出 120 /1 印出 36288∞ 如果你认为这比冬天吃冰漠淋还醋,你就是取得了成为二个template metaprogrammer的必要条件。如果templates和其特化版本,以及递归具现化和enum hacks,以及键入 Factorial::value这样的东西会使你汗毛直坚,晤……你 是个十分"正常化"的C++ 程序员。 当然, Factorial示范 TMP 的用途就只是像 "hello world" 示范任何传统语言 的用途一样。为求领悟TMP 之所以值得学习,很重要的一点是先对它能够达成什 么目标有一个比较好的理解。下面举出三个例子z ·确保量度单位正确。在科学和工程应用程序中,确保量度单位(例如质量、距 离、时间……等等)正确结合是绝对必要的。举个例子,将一个质量变量赋值 给一个速度变量是错误的,但是将一个距离变量除以一个时间变量并将结果赋 值给一个速度变量则成立。如果使用TMP,就可以确保(在编译期)程序中所 有量度单位的组合都正确,不论其计算多么复杂。这也就是为什么TMP 可被用 来进行早期错误侦测。这种TMP 用途的一个有趣情况是,就连因次为分数的指 数(企actional dimensional exponents) 也可支持,但分数必须先在编译期被约简, EJ.作ctive C++ 中文版,第三版7 条款 48: 认识 template 元编程 237 例如 timeω 的单位和 time 4/8 的单位相同。 ·优化矩阵运算。条款 21 曾经提过某些函数包括 operator* 必须返回新对象,而 条款 44 又导入了一个 SquareMa trix class 。考虑以下代码 3 typedef Square~但tr工x BigMatrix; BigMatrix ml , rn2, m3 ,时,而 ; II 创建矩阵并 II 赋予它们数值。 BigMatrix result = ml * m2 * m3 * m4 * mS; II 计算它们的乘积。 以"正常的"函数调用动作来计算 result ,会创建 4 个暂时性矩阵,每一 个用来存储对 operator* 的调用结果。犹有进者,各自独立的乘法产生了 4 个 作用于矩阵元素身上的循环。如果使用高级、与 TMP 相关的 template 技术,即 所谓臼pression templates , 就有可能消除那些临时对象并合并循环,这一切都无 需改变客户端的用法(像上面那样)。于是 TMP 软件使用较少的内存,执行速 度又有戏剧性的提升。 ·可以生成客户定制之设计模式 (custom design pattern) 实现品。设计模式如 Strategy (见条款 35) ,Observer, Visitor 等等都可以多种方式实现出来。运用 所谓 policy-bωed design 之 TMP-based 技术,有可能产生一些 templates 用来表 述独立的设计选项(所谓 "policies") ,然后可以任意结合它们,导致模式实现 品带着客户定制的行为。这项技术已被用来让若干 templates 实现出智能指针的 行为政策 (behavioral policies) ,用以在编译期间生成数以百计不同的智能指针 类型。这项技术己经超越编程工艺领城如设计模式和智能指针,更广义地成为 generative programming (殖生式编程)的一个基础。 不是每个人都喜欢 TMP 。其语法不直观,其支持工具目前还不充分 (template metaprograms 的调试器?哈,还早咧! )由于 TMP 是一个在相对短时间之前才意 外发现的语言,其编程方式还多少需要倚赖经验。尽管如此,将工作从运行期移往 编译期所带来的效率改善还是令人印象深刻,而 TMP 对"难以或甚至不可能于运 行期实现出来的行为"的表现能力也很吸引人。 TMP 仿佛旭日东升。有可能下一版 C++ 会对它提供明确的支持,甚至 TRl 已 经这样做了(见条款 54) 0 TMP 书籍己逐渐出现,网络上的 TMP 信息愈来愈丰富。 Effective C++ 中文版,第三版238 7 模板与泛型编程 TMP 或许永远不会成为主流,但对某些程序员一书别是程序库开发人员一一 几乎确定会成为他们的主要粮食。 请记住 • Template metaprogramming (TMP ,模板元编程)可将工作由运行期移往编译期, 因而得以实现早期错误侦测和更高的执行效率。 .TMP 可被用来生成"基于政策选择组合" (based on combinations of policy choices) 的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。 £1知ctive C++ 中文版,第三版8 条款 48: 认识 template 元编程 239 8 定制 ne-w末日 delete Customizing new and delete 当计算环境(例如 Java 和 .NET) 夸耀自己内置"垃圾回收能力"的当今,c++ 对内存管理的纯手工法也许看起来有点老气。但是许多苛刻的系统程序开发人员之 所以选择 C忡,就是因为它允许他们手工管理内存。这样的开发人员研究并学习他 们的软件使用内存的行为特征,然后修改分配和归还工作,以求获得其所建置的系 统的最佳效率(包括时间和空间)。 这样做的前提是,了解 c++ 内存管理例程的行为。这正是本章焦点。这场游 戏的两个主角是分配例程和归还例程 (allocation and deallocation routines ,也就是 operator new 和 operator delete) ,配角是 new-handler,这是当 operator new 无法满足客户的内存需求时所调用的函数。 多线程环境下的内存管理,遭受单线程系统不曾有过的挑战。由于 heap 是一 个可被改动的全局性资源,因此多线程系统充斥着发狂访问这一类资源的 race conditions (竟速状态)出现机会。本章多个条款提及使用可改动之static 数据,这 总是会令线程感知(thread-aware) 程序员高度警戒如坐针毡。如果没有适当的同步 控制( synchronization) ,一旦使用无锁(lock-free) 算法或精心防止并发访问 (concurrent access) 时,调用内存例程可能很容易导致管理heap 的数据结构内容 败坏。我不想一再提醒你这些危险,我只打算在这里提-下,然后假设你会牢记在 心。 另外要记住的是, operator new 和 operator delete 只适合用来分配单一对 象。 Arrays 所用的内存由 operator new[] 分配出来,并由 operator delete[]归 还(注意两个函数名称中的[])。除非特别表示,我所写的每一件关于operatornew 和 operator delete 的事也都适用于operator new []和 operator delete[] 。 El知ctive C++ 中文版,第三版240 8 定制 new 和 delete 最后请注意, STL 容器所使用的 heap 内存是由容器所拥有的分配器对象 (allocator objects) 管理,不是被 new 和 delete 直接管理。本章并不讨论STL 分 配器。 条款的:了解new-handler的行为 Understand the behavior ofthe new-handler. 当 operator new 无法满足某一内存分配需求时,它会抛出异常。以前它会返 回一个 null 指针,某些旧式编译器目前也还那么做。你还是可以取得旧行为(有那 么几分像啦) ,但本条款最后才会进行这项讨论。 当 operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用 一个客户指定的错误处理函数,一个所谓的new-hand/ero ( 这其实并非全部事实。 operator new 真正做的事情稍微更复杂些。详见条款510 )为了指定这个"用以 处理内存不足"的函数,客户必须调用set new handler ,那是声明于 的 个标准程序库函数: namespace std { typedef void (*new_handler) ( ); new handler set new handler(new handler p) throw(); 如你所见, new handler 是个 typedef,定义出一个指针指向函数,该函数没{f 参数也不返回任何东西。 set 口ew handler 则是"获得一个 new handler 并返回 4 个且ew handler" 的函数。 set new handler 声明式尾端的 "throw() "是一份异常 明细,表示该函数不抛出任何异常一一虽然事实更有趣些,详见条款29 。 set new handler 的参数是个指针,指向 operatornew 无法分配足够内存时该 被调用的函数。其返回值也是个指针,指向 set new handler 被调用前正在执行(但 马上就要被替换)的那个 new-handler 函数。 你可以这样使用 set new handler: II 以下是当 operator new 无法分配足够内存时,该被调用的函数 void outOfMern () std::cerr « "Unable to satisfy request for mernory\n"; std: :abort ( ); E1作clive C++中文版,第三版8 条款 49: 了解 new-handler 的行为 int main () std::set new handler(outOfMem); int* pBigDataArray = new int[100000000L]; 241 就本例而言,如果 operator new 无法为 IOO , OOO , OOO 个整数分配足够空间, outO fMem 会被调用,于是程序在发出一个信息之后夭折 (abor t) 0 (顺带一提, 如果在写出错误信息至 eerr 过程期间必须动态分配内存,考虑会发生什么事……) 当 operator new 无法满足内存申请时,它会不断调用 new-handler 函数,直到 找到足够内存。引起反复调用的代码显示于条款 5 1,这里的高级描述已足够获得→ 个结论,那就是一个设计良好的 new-handler 函数必须做以下事情 z ·让更多内存可被使用 o 这便造成 operator new 内的下一次内存分配动作可能成 功。实现此策略的二个做法是,程序-开始执行就分配一大块内存,而后当 new-handler 第→次被调用,将它们释还给程序使用。 ·安装另一个 new-handler 。如果目前这个 new-handler 无法取得更多可用内存, 或许它知道另外哪个 new-handler 有此能力。果真如此,日前这个 new-handler 就可以安装另外那个 new-handler 以替换自己(只要调用 set new handler) 。 下次当 operator new 调用 new-handler , . i周用的将是最新安装的那个。(这个 旋律的变奏之一是让 new-handler 修改自己的行为,于是当它下次被调用,就会 做某些不同的事。为达此目的,做法之一是令 new-handler 修改"会影响 new-handler 行为"的 static 数据、 namespace 数据或 global 数据。) ·卸除 new-handler ,也就是将 null 指针传给 set new handlero 一旦没有安装任 何 new-handler , operator new 会在内存分配不成功时抛出异常。 ·抛出 bad ali∞(或派生自 bad all∞)的异常。这样的异常不会被operator new 捕捉,因此会被传播到内存索求处。 ·不返回,通常调用abort 或 exit。 这些选择让你在实现new-handler函数时拥有很大弹性。 E1作ctive C++中文版,第三版242 8 定制 new 和 delete 有时候你或许希望以不同的方式处理内存分配失败情况,你希望视被分配物属 于哪个 class 而定: class X( public: static void outOfMemory(); ) ; class Y( public: static void outOfMemory(); X* pI = new X; II 如果分配不成功, II 调用 X: :outO fM四 lory Y* p2 = new Y; II 如果分配不成功, II 调用 Y: :outOfMemory C++ 并不支持 class 专属之 new-handlers. 但其实也不需要。你可以自己实现 出这种行为。只需令每一个 class 提供自己的 set new handler 和 operator new 即可。其中 set new handler 使客户得以指定 class 专属的 new-handler (就像标准 的 set new handler 允许客户指定 global new-handler) ,至于 operator new 则确 保在分配 class 对象内存的过程中以 class 专属之 new-handler 替换 global new-handler。 现在,假设你打算处理Widget class 的内存分配失败情况。首先你必须登录"当 operator new 无法为一个Widget对象分配足够内存时"调用的函数,所以你需要 声明一个类型为new handler 的 static 成员,用以指向 classWidget 的 new-handler。 看起来像这样: class Widget ( public: static std::new handler set new handler(std::new handler p) throw(); static void* operator new(std: :size_t size) throw(std: :bad二alloc) ; private: static std::new handler currentHandler; Static 成员必须在 class 定义式之外被定义(除非它们是const 而且是整数型,见条 款 2) ,所以需要这么写: std::new handler Widget::currentHandler = 0; II在 class 实现文件内初始化为null Effective C++ 中文版 F 第三版8 条款 49: 了解 new-handler 的行为 243 Widget 内的 set new handler 函数会将它获得的指针存储起来,然后返回先前 (在此调用之前)存储的指针,这也正是标准版 set new handler 的作为 2 std::new handler Widget::set_new_handler(std::口ew_handler p) throw() std::new handler oldHandler = currentHandler; currentHandler = p; return oldHandler; 最后, Widget 的 operator new 做以下事情: 1. 调用标准 set new handler ,告知 Widget 的错误处理函数。这会将 Widget 的 new-handler 安装为 global new-handler 0 2. 调用 global operator new ,执行实际之内存分配。如果分配失败,global operator new 会调用 Widge 的 new-handler,因为那个函数才刚被安装为global new-handler 0 如果 global operator new 最终无法分配足够内存,会抛出→个 bad alIoc 异常。在此情况下Widget 的 operator new 必须恢复原本的 global new-handler,然后再传播该异常。为确保原本的new-handler总是能够被重新安 装回去, Widget将 global new-handler视为资源并遵守条款13 的忠告,运用资 源管理对象 (resource-managingobjects) 防止资源泄漏。 3. 如果 global operator new 能够分配足够一个Widget对象所用的内存, Widget 的 operator new 会返回一个指针,指向分配所得。Widget 析构函数会管理 global new-handler,它会自动将Widget's operator new 被调用前的那个global new-handler恢复回来。 下面以 e++ 代码再阐述一次。我将从资源处理类(resource-handlingclass) 开 始,那里面只杳基础性RAIl操作,在构造过程中获得一笔资源,并在析构过程中 释还(见条款 13) : class NewHandlerHolder { public: explicit NewHandlerHolder(std::new_handler nh) handler (nh) {} -NewHandlerHolder() { std::set new handler(handler); } private: std::new handler handler; NewHandlerHolder(const NewHandlerHolder&); NewHandlerHolder& operator=(const NewHandlerHolder&); II取得目前的 Ilnew-handl缸。 II释放它 II记录下来. II脏1:: copying II(见条款 14) Effective C++ 中文版,第三版244 这就使得 Widget's operator new 的实现相当简单: 8 定制 new 和 delete void* Widget::operator new(std::size t size) throw(std::bad alloc) { NewHandlerHolder h(std::set new handler(currentHandler»; return ::operator new(size); II安装 Widget 的 Ilnew-handl町. II分配内存或抛出异常 II恢复 global new-handler. Widget 的客户应该类似这样使用其 new-handling: void outOfMem(); II 函数声明。此函数在 IIW工dget 对象分配失败时被调用, Widget: :set_new_handler (outOfMem) ; I I 设定 outOfM em 为 Widget 的 II new-handling 函数. Widget* pwl = new Widget; II如果内存分配失败, II调用 outOfMem std::string* ps = new std::stri 呵 ; II 如果内存分配失败, II 调用 global new-handling 函数 II( 如果有的话) . Widget::set_new_handler(O); II设定 Widget专属的 Ilnew-handling函数为 null. Widget* pw2 = new Widget; II如果内存分配失败, II立刻抛出异常. II(class Widget 并没有专属的 Ilnew-handling 函数) . 实现这一方案的代码并不因 class 的不同而不同,因此在它处加以复用是个合 理的构想。一个简单的做法是建立起→个 "mixin" 风格的 base class ,这种 base class 用来允许 derived classes 继承单一特定能力一一在本例中是"设定 class 专属之 new-handler" 的能力。然后将这个 base class 转换为 template ,如此一来每个 derived class 将获得实体互异的 class data 复件。 这个设计的 base class 部分让 derived classes 继承它们所需的 set 口ew handler 和 operatornew,而 template 部分则确保每一个derived class 获得一个实体互异的 currentHandler 成员变量。昕起来似乎有点复杂,但代码非常近似前个版本。实 际上,唯一真正意义上的不同是,它现在可被任何有所需要的class 使用 z Effective C++ 中文版,第三版8 条款 49: 了解 new-handler 的行为 245 template II"mixin" 风格的 base class ,用以支持 class NewHandlerSupport { Ilclass 专属的 set new handler public: static std: :new_handler set_new_handler(std: :new_handler p) throw(); static void* operator new (std: : size t size) throw (std: :bad alloc); II其他的叩eratornew版本一一见条款52 pr~vate: static std::口ew handler currentHandler; teπ~late std::new handler NewHandlerSupport::set new handler(std::new handler p) throw() std::new handler oldHandler = currentHandler; currentHandler = p; return oldHandler; template void* NewHandlerSupport::operator new(std::size_t size) throw(std::bad alloc) NewHandlerHolder h(std::set new handler(currentHandler»; return ::operator new(size); II 以下将每-个currentHandler初始化为 null template std::new handler NewHandlerSupport::currentHandler = 0; 有了这个 class template,为 Widget添加 set new handler 支持能力就轻而易 举了 t 只要令 Widget 继承自 NewHandlerSupport就好,像下面这样。看 起来似乎很奇妙,稍后我将更详细解释它的精确意义。 class Widget: public NewHandlerSupport { II 和先前一样,但不必声明 );II set new handler 或 operator new 这就是 Widget 为了提供 "class 专属之 set new handler" 所需做的全部动作。 但或许你还是对 Widget 继承 NewHandlerSupport感到心慌意乱。果 真如此,你的焦虑还可能因为注意到NewHandlerSupporttemplate 从未使用其类型 参数 T 而更放大数倍。实际上 T 的确不需被使用。我们只是希望,继承自 NewHandlerSupport 的每一个 class ,拥有实体互异的 NewHandlerSupport 复件(更 明确地说是其 static 成员变量 currentHaridler) 。类型参数 T 只是用来区分不同的 Effective C++ 中文版y 第三版246 8 定制 new 和 delete derived classo Template 机制会自动为每一个 T (NewHandlerSupport赖以具现化的 根据)生成一份 currentHandler。 至于说到 Widget 继承自一个模板化的 (templatized) base class,而后者又以 Widget作为类型参数,如果你对此头昏眼花,不要觉得惭魄。每个人一开始都有那 种反应。由于它被证明是一个有用的技术,因此甚至拥有自己的名称:"怪异的循 环模板模式" (curiously recurring template pattern; CRTP) 。有些人认为这个名称 给人的第一眼印象很不自然。嗯,确实如此。 我曾发表过一篇文章,建议给它一个比较好的名称,像是 Do It For Me,因为 当 Widget 继承 NewHandlerSupport 时它其实并不是说"我是Widget, 我要针对 Widget class 继承 NewHandlerSupport" 。但是,哎,没人采用我建议的 名称(甚至我自己也不) ,但如果你看到CRTP 会联想到它说的是 "do it for me" , 或许可以帮助你了解这一模板化继承 (templatized inheritance) 到底用意为何。 像 NewHandlerSupport 这样的 templates ,使得"为任何 class 添加一个它们专 属的 new-handler" 成为易事。然而 "mixin" 风格的继承肯定导致多重继承的争议, 而在开始那条路之前,你需要先阅读条款 40 。 直至 1993 年, C++ 都还要求 operator new 必须在无法分配足够内存时返回 null 。新一代的 operator new 则应该抛出 bad alloc 异常,但很多 C++ 程序是在 编译器开始支持新修规范前写出来的。 C++ 标准委员会不想抛弃那些"侦测 null" 的族群,于是提供另一形式的 operator new ,负责供应传统的"分配失败便返回 null" 行为。这个形式被称为 "no也row" 形式一一某种程度上是因为他们在 new 的 使用场合用了 nothrow 对象(定义于头文件 (pMem» = signature; *(reinterpret_cast(static_cast(pMem) +realSize-sizeof(int») = signature; II返回指针,指向恰位于第一个si伊ature 之后的内存位置 return static east(pMem) + sizeof(int); 这个 operator new 的缺点主要在于它疏忽了身为这个特殊函数所应该具备的 "坚持 C++ 规矩"的态度。举个例子,条款51 说所有 operatorne晒都应该内含 一个循环,反复调用某个new-handling 函数,这里却没有。由于条款51 就是专门 为此协议而写,所以这儿我暂且忽略之。我现在只想专注于一个比较微妙的主题2 齐位(alignment) 。 许多计算机体系结构(computeraτ'chitectures) 要求特定的类型必须放在特定的 内存地址上。例如它可能会要求指针的地址必须是4 倍数 (four-byte aligned) 或 doubles 的地址必须是8 倍数 (eight-byte ali伊e的。如果没有奉行这个约束条件, 可能导致运行期硬件异常。有些体系结构比较慈悲,没有那么霹雳,而是宣称如果 齐位条件获得满足,便提供较佳效率。例如Intel x86 体系结构上的 doubles 可被 对齐于任何 byte 边界,但如果它是8-byte 齐位,其访问速度会快许多。 在我们目前这个主题中,齐位(alignment) 意义重大,因为 C++ 要求所有 operator news 返回的指针都有适当的对齐(取决于数据类型)0 malloe 就是在这 样的要求下工作,所以令 operatornew 返回一个得自 malloe 的指针是安全的。然 而上述 operator new 中我并未返回一个得自 malloe 的指针,而是返回一个得自 malloc 且偏移一个 int 大小的指针。没人能够保证它的安全!如果客户端调用 operatornew 企图获取足够给一个 double 所用的内存(或如果我们写个 operator new[] ,元素类型是 doubles),而我们在一部"i口ts 为 4 bytes 且 doubles 必须 8-byte El知ctive C++中文版,第三版250 8 定制 new 和 delete 齐位"的机器上跑,我们可能会获得一个未有适当齐位的指针。那可能会造成程序 崩溃或执行速度变慢。不论哪种情况都非我们所乐见。 像齐位( alignment) 这一类技术细节,正可以在那种"因其他纷扰因素而被程 序员不断抛出异常"的内存管理器中区分出专业质量的管理器。写一个总是能够运 作的内存管理器并不难,难的是它能够优良地运作。一般而言我建议你在必要时才 试着写写看。 很多时候是非必要的!某些编译器已经在它们的内存管理函数中切换至调试状 态 (enable debugging) 和志记状态(l ogging) 。快速浏览一下你的编译器文档,很 可能就此消除自行撰写 new 和 delete 的需要。许多平台上己有商业产品可以替代 编译器自带的内存管理器。如果需要它们来为你的程序提高机能和改善效能,你唯 一需要做的就是重新连接 (relink) 。当然啦,首先你得花点钱买下它们。 另一个选择是开放源码 (open source) 领域中的内存管理器。它们对许多平台 都可用,你可以下载并试试。 Boost 程序库(见条款 55) 的 Pool 就是这样一个分配 器,它对于最常见的"分配大量小型对象"很有帮助。许多 e++ 书籍,包括本书 早期版本,都曾展示高效率的小型对象分配器源码,但它们往往漏掉可移植性和齐 位考虑、线程安全性……等等令人生厌的麻烦细节。真正称得上程序库者,必然稳 健强固。即使你还是执意写一个自己的 news 和 deletes ,看一看开放源码版本也 可能对若干容易被漠视的细节(它们用来区分"几乎行得通"和"真正行得通"的 制品)取得深刻的理解。齐位就是这样一个细节, TRI (见条款 54) 支持各类型特 定的对齐条件,很值得注意。 本条款的主题是,了解何时可在"全局性的"或"class 专属的"基础上合理替 换缺省的 new 和 delete。挖掘更多细节之前,让我先对答案做一些摘要。 ·为了检测运用错误(如前所述)。 ·为了收集动态分配内存之使用统计信息(如前所述)。 Effective e++中文版,第三版8 条款 50: 了解 new 和 delete 的合理替换时机 251 ·为了增加分配和归还的速度。泛用型分配器往往(虽然并不总是)比定制型分 配器慢,特别是当定制型分配器专门针对某特定类型之对象而设计时。 Class 专 属分配器是"区块尺寸固定"之分配器实例,例如 Boost 提供的 Pool 程序库便 是。如果你的程序是个单线程程序,但你的编译器所带的内存管理器具备线程 安全,你或许可以写个不具线程安全的分配器而大幅改善速度。当然,在获得 "operator new 和 operatordelete 有加快程序速度的价值"这个结论之前, 首先请分析你的程序,确认程序瓶颈的确发生在那些内存函数身上。 ·为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往(虽然 并非总是)不只比定制型慢,它们往往还使用更多内存,那是因为它们常常在 每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器(例如 Boost 的 Pool 程序库)本质上消除了这样的额外开销。 ·为了弥补缺省分配器中的非最佳齐位(suboptimalalignment) 。一如先前所说, 在 x86 体系结构上 doubles 的访问最是快速一一如果它们都是8-byte 齐位。但 是编译器自带的 operator news 并不保证对动态分配而得的doubles 采取 8-byte 齐位。这种情况下,将缺省的operator new 替换为一个 8-byte 齐位保 证版,可导致程序效率大幅提升。 ·为了将相关对象成簇集中。如果你知道特定之某个数据结构往往被一起使用, 而你又希望在处理这些数据时将"内存页错误"(page faults) 的频率降至最低, 那么为此数据结构创建另一个heap 就有意义,这么一来它们就可以被成簇集中 在尽可能少的内存页 (pages) 上。 new 和 delete 的 " placement版本" (见条 款 52) 有可能完成这样的集簇行为。 ·为了获得非传统的行为。有时候你会希望operators new 和 delete做编译器附 带版没做的某些事情。例如你可能会希望分配和归还共享内存(shared memory) 内的区块,但唯一能够管理该内存的只有 CAPI 函数,那么写下一个定制版 new 和 delete (很可能是 placement 版本,见条款 52) ,你便得以为 C API 穿上一 件 C++ 外套。你也可以写一个自定的 operator delete ,在其中将所有归还内 存内容覆盖为 0 ,藉此增加应用程序的数据安全性。 Effective C++ 中文版,第三版252 8 定制 new 和 delete 请记住 ·有许多理由需要写个自定的口 ew 和 delete ,包括改善效能、对 heap 运用错误进 行调试、收集 heap 使用信息。 条款 51: 编写 new 和 delete 时需固守常规 Adhere to convention when writing new and delete. 条款 50 己解释什么时候你会想要写个自己的 operator new 和 operator delete. 但并没有解释当你那么做时必须遵守什么规则。这些规则不难奉行,但其 中一些并不直观,所以知道它们究竟是些什么很重要。 让我们从 operator new 开始。实现一致性 operator new 必得返回正确的值, 内存不足时必得调用 new-handling 函数(见条款 49) ,必须有对付零内存需求的准 备,还需避免不慎掩盖正常形式的 new 一一虽然这比较偏近 class 的接口要求而非 实现要求。正常形式的 new 描述于条款 52 。 operator new 的返回值十分单纯。如果它有能力供应客户申请的内存,就返回 一个指针指向那块内存。如果没有那个能力,就遵循条款49 描述的规则,并抛出 一个 bad alloc 异常。 然而其实也不是非常单纯,因为operatornew 实际上不只一次尝试分配内存, 并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某 些动作将某些内存释放出来。只有当指向new-handling函数的指针是null , operator new 才会抛出异常。 奇怪的是 e++ 规定,即使客户要求o bytes, operatqr new 也得返回→个合法 指针。这种看似诡异的行为其实是为了简化语言其他部分。下面是个non-member operator new 伪码 (pseudocode) : void女 operator new(std::size_t size) throw(std::bad_a11oc) II你的 operaω,rnew 可能接受额外参数 using namespace std; if (size == 0) { size = 1; while (true) { 尝试分配 size bytes; Efj告ctive C++中文版F 第三版 II处理 O-byte 申请. II将它视为 l-b严e 申请.8 条款 51: 编写 new 和 delete 时需固守常规 if( 分配成功) return (一个指针,指向分配得来的内存); II 分配失败:找出目前的 new-handling 函数(见下) new handler globalHandler = set new handler(O); set_new_handler(globalHandler); if (globalHandler) (*globalHandler) (); else throw std::bad alloc(); 253 这里的伎俩是把 o bytes 申请量视为 1 byte 申请量。看起来有点薪搭搭地令人 厌恶,但做法简单、合法、可行,而且毕竟客户多久才会发出→个 o bytes 申请呢? 你也可能带着怀疑的眼光斜眼这份伪码( pseudocode) ,因为其中将 new-handling 函数指针设为 null 而后又立刻恢复原样。那是因为我们很不幸地没有 任何办法可以直接取得new-handling 函数指针,所以必须调用 set new handler 找出它来。拙劣,但有效一一-至少对单线程程序而言。若在多线程环境中你或许需 要某种机锁(lock) 以便安全处置new-handling函数背后的 (global)数据结构。 条款 49 谈到 operatornew 内含一个无穷循环,而上述伪码明白表明出这个循 环; "while (true)" 就是那个无穷循环。退出此循环的唯一办法是:内存被成功 分配或 new-handling 函数做了一件描述于条款49 的事情2 让更多内存可用、安装 另→个 new-handler、卸除 new-handler、抛出 bad alloc 异常(或其派生物) ,或 是承认失败而直接四turn。现在,对于new-handler 为什么必须做出其中某些事你应 该很清楚了。如果不那么做, operator new 内的 while 循环永远不会结束。 许多人没有意识到operator new 成员函数会被derived classes 继承。这会导致 某些有趣的复杂度。注意上述operatornew伪码中,函数尝试分配size bytes (除 非 size 是 0) 。那非常合理,因为 size 是函数接受的实参。然而就像条款50 所言, 写出定制型内存管理器的一个最常见理由是为针对某特定class 的对象分配行为提 供最优化,却不是为了该class 的任何 derived classes 0 也就是说,针对 class X 而设 计的 operator new,其行为很典型地只为大小刚好为 sizeof(X) 的对象而设计。 然而一旦被继承下去,有可能base class 的 operator new 被调用用以分配 derived class 对象: Effective C++ 中文版,第三版254 8 定制 new 和 delete class Base { public: static void" operator new(std: :size_t size) throw(std: :bad_alloc); class Derived: publ工 c Base II假设 Derived未声明 operator new { ... }; Derived* p = new Derived; II这里调用的是Base::operator new 如果 Base class 专属的 operator new 并非被设计用来对付上述情况(实际上 往往如此) ,处理此情势的最佳做法是将"内存申请量错误"的调用行为改来标准 operator new,像这样: void* Base::operator new(std::size_t size) throw(std::bad_alloc) if (size != sizeof(Base)) II如果大小错误, return : :operator new (size); I I 令标准的 operaω,rnew 起而处理。 II 否则在这里处理。 "等一下! "我听到你大叫, "你忘了检验 size 等于 O 这种病态但是可能出 现的情况! "。是的,我没检验,但请你收回你的但是。测试依然存在,只不过它 和上述的" size 与 sizeof(Base) 的检测"融合一起了。是的, C++ 在某种秘境中 运行,而其中一个秘境就是它裁定所有非附属(独立式)对象必须有非零大小(见 条款 39) 。因此 sizeof(Base) 无论如何不能为零,所以如果 size 是 0 ,这份申请 会被转变到: :operator new 手上,后者有责任以某种合理方式对待这份申请。 译注:这里所谓非附属/独立式(freestanding)对象,指的是不以"某对象之base class 成分"存在的对象。此处所言的这个规定,可参考«Inside the C++ 0句ect» by Stanly Lippman, Addison Wesley, 1996 。 如果你打算控制 class 专属之"aπays 内存分配行为飞那么你需要实现 operator new 的 array 兄弟版: operator new []。这个函数通常被称为 "aπaynew" ,因为很 难想出如何发音 "operator new [] "。如果你决定写个operator new [],记住,唯 一需要做的一件事就是分配一块未加工内存 (raw memory) ,因为你无法对 aπay 之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个array 将含 多少个元素对象。首先你不知道每个对象多大,毕竟base class 的 operator new [] 有可能经由继承被调用,将内存分配给"元素为derived class 对象"的 array 使用, 而你当然知道, derived class 对象通常比其base class 对象大。 Effective C++ 中文版,第三版8 条款 51 :编写 new 和 delete 时需固守常规 255 因此,你不能在 Base: :operator new []内假设 array 的每个元素对象的大小是 sizeof (Base) ,这也就意味你不能假设 array 的元素对象个数是 (bytes 申请 数)/sizeof(Base) 。此外,传递给 operator new []的 size t 参数,其值有可能比 "将被填以对象"的内存数量更多,因为条款 16 说过,动态分配的 arrays 可能包 含额外空间用来存放元素个数。 这就是撰写 operator new 时你需要奉行的规矩。 operator delete 情况更简 单,你需要记住的唯一事情就是 C++ 保证"删除 null 指针永远安全",所以你必 须兑现这项保证。下面是 non-member operator delete 的伪码(pseudocode) : void operator delete(void * rawMemory) throw() if (rawMemory == 0) return; II如果将被删除的是个null 指针, II那就什么都不做。 现在,归还rawMemory所指的内存; 这个函数的 member 版本也很简单,只需要多加一个动作检查删除数量。万一 你的 class 专属的 operator new 将大小有误的分配行为转变: :operator new 执行, 你也必须将大小有误的删除行为转变::operator delete执行: class Base { II 一如以往,但此刻重点在 operaωrdele能 public: static void女叩erator new(std::size_t size) 甘lrow (std: :bad_alloc) ; static void operator delete(void* rawM凹Dry , std::size t size) throw(); ) ; void Base: :operator delete(void* rawMemory, std: :size_t size) throw() { if (rawMemory == 0) return; if (s 工 ze != s 工 zeof (Base)) { ::operator delete(ra协1emory) ; return; 现在,归还rawMemory所指的内存; return; II检查 null 指针。 II如果大小错误,令标准版 Iloperaωr delete 处理此一申请。 有趣的是,如果即将被删除的对象派生自某个base class 而后者欠缺virtual 析 构函数,那么 C++ 传给 operator delete 的 size t 数值可能不正确。这是"让你 的 base classes 拥有 virtual 析构函数"的一个够好的理由:条款7 还提过一个更好 El知ctive C++中文版,第三版256 8 定制 new 和 delete 的理由。我就不岔开话题了,此刻只要你提高警觉,如果你的 base classes 遗漏 virtual 析构函数, operator delete 可能无法正确运作。 请记住 • operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满 足内存需求,就该调用new-handler。它也应该有能力处理o bytes 申请。 Class 专属版本则还应该处理"比正确大小更大的(错误)申请"。 • operator delete 应该在收到 null 指针时不做任何事。 Class 专属版本则还应该 处理"比正确大小更大的(错误)申请"。 条款 52: 写了 placementnew也要写 placementdelete Write placement delete ifyou write placement new. placement new 和 placement delete 并非 C++ 兽栏中最常见的动物,如果你不 熟悉它们,不要感到挫折或忧虑。回忆条款 16 和 17 ,当你写一个 new 表达式像这 样: Widget* pw = new Widget; 共有两个函数被调用:一个是用以分配内存的operator new,一个是 Widget 的 default构造函数。 假设其中第一个函数调用成功,第二个函数却抛出异常。既然那样,步骤一的 内存分配所得必须取消并恢复旧观,否则会造成内存泄漏(memory leak) 。在这个 时候,客户没有能力归还内存,因为如果Widget 构造函数抛出异常, pw 尚未被赋 值,客户手上也就没有指针指向该被归还的内存。取消步骤一并恢复旧观的责任因 此落到 C++ 运行期系统身上。 运行期系统会高高兴兴地调用步骤一所调用的operator new 的相应 operator delete版本,前提当然是它必须知道哪一个(因为可能有许多个)operatordelete 该被调用。如果目前面对的是拥有正常签名式(signature) 的 new 和 delete,这并 不是问题,因为正常的operator new: void* operator new(std::size t) throw(std::bad alloc); 对应于正常的 operator delete: R萨ctive C++中文版,第三版8 条款 52: 写了 placement new 也要写 placement delete 257 飞loid operator delete(void* rawMemory) throw(); Ilglobal 作用域中的正常签名式. void operator delete(void* rawMemory, std::size_t size) throw(); Ilclass 作用域中典型的签名式 因此,当你只使用正常形式的new 和 delete,运行期系统毫无问题可以找出 那个"知道如何取消 new 所作所为并恢复旧观"的delete。然而当你开始声明非 正常形式的 operator new,也就是带有附加参数的operator new, "究竟哪一个 delete 伴随这个 new" 的问题便浮现了。 举个例子,假设你写了一个 class 专属的 operator new ,要求接受一个 ostream,用来志记 (logged) 相关分配信息,同时又写了一个正常形式的 class 专 属 operator delete: class Widget { public: staticvoid* operator new (std: :size_t size, std: :ostream& logStream) throw(std::bad alloc); II非正常形式的new static void operator delete(void* pMemory std::size_t size) throw(); II正常的 class 专属 delete 这个设计有问题,但在探讨原因之前,我们需要先绕道,扼要讨论若干术语。 如果 operatornew接受的参数除了干定会有的那个size t 之外还有其他,这 便是个所谓的 placement new。因此,上述的operator new 是个 placement版本。 众多 placement new 版本中特别有用的一个是"接受一个指针指向对象该被构造之 处",那样的 operator new 长相如下: void* operator new(std::size t , void* pMemory) throw(); IIplacementnew 这个版本的 new 己被纳入 C++ 标准程序库,你只要 #include 就可以 取用它。这个 new 的用途之一是负责在 vector 的未使用空间上创建对象。它同时 也是最早的 placement new 版本。实际上它正是这个函数的命名根据 g 一个特定位 置上的 new 。以上说明意味术语 placement new 有多重定义。当人们谈到 placement new ,大多数时候他们谈的是此二特定版本,也就是"唯一额外实参是个 void 户, 少数时候才是指接受任意额外实参之 operato r: newo 上下文语境往往也能够使意 义不明确的含糊话语清晰起来,但了解这一点相当重要 z 一般性术语 "placement Effective C++ 中文版,第三版258 8 定制 new 和 delete new" 意味带任意额外参数的 new ,因为另一个术语 "placement delete" 直接派生 自它。稍后我们即将遭遇后者。 现在让我们回到 Widget class 的声明式,也就是先前我说设计有问题的那个。 这里的技术困难在于,那个 class 将引起微妙的内存泄漏。考虑以下客户代码,它 在动态创建一个 Widget 时将相关的分配信息志记Cl ogs) 于 cerr: Widget* pw = new (std::cerr) Widget; II调用叩mωrnew并传递ωrr 为其 110掬回m 实参:这个动作会在Widget II构造函数抛出异常时泄漏内存 再说一次,如果内存分配成功,而Widget 构造函数抛出异常,运行期系统有 责任取消 operator new 的分配并恢复旧观。然而运行期系统无法知道真正被调用 的那个 operator new 如何运作,因此它无法取消分配并恢复旧观,所以上述做法 行不通。取而代之的是,运行期系统寻找"参数个数和类型都与operator new 相 同"的某个 operatordelete。如果找到,那就是它的调用对象。既然这里的operator new 接受类型为 ostream& 的额外实参,所以对应的operator delete就应该是g void operator delete(void*, std::ostream&) throw(); 类似于 new 的 placement版本, operator delete 如果接受额外参数,便称为 placement deletes 。现在,既然 Widget 没有声明 placement 版本的 operator delete ,所以运行期系统不知道如何取消并恢复原先对 placement new 的调用。于 是什么也不做。本例之中如果 Widget 构造函数抛出异常,不会有任何 operator delete 被调用(那当然不妙)。 规则很简单:如果一个带额外参数的 operator new 没有"带相同额外参数" 的对应版 operator delete ,那么当 new 的内存分配动作需要取消并恢复旧观时就 没有任何 operator delete 会被调用。因此,为了消再稍早代码中的内存泄漏, Widget 有必要声明一个 pIa臼ment delete ,对应于那个有志记功能Cl ogging) 的 placement new: class Widget { public: staticvoid* operator new (std: : size t size, std: :ostream& logStream) throw(std::bad alloc); El知ctive C++中文版,第三版8 条款 52: 写了 placement new 也要写 placement delete 259 static void operator delete(void* pMemory) throw(); static void operator delete (void女 pMemory, std: :ostream& logStream) throw (); 这样改变之后,如果以下语句引发Widget构造函数抛出异常: Widget* pw = new (std::eerr) Widget; II一如以往,但这次不再泄漏 对应的 placementdelete会被自动调用,让Widget 有机会确保不泄漏任何内存。 然而如果没有抛出异常(通常如此).而客户代码中有个对应的delete. 会 发生什么事: delete pw; II调用正常的 operator delete 就如上一行注释所言,调用的是正常形式的operatordelete,而非其placement 版本。 placementdelete 只有在"伴随placementnew 调用而触发的构造函数"出现 异常时才会被调用。对着一个指针(例如上述的pw) 施行 delete绝不会导致调用 placement delete 。不,绝对不会。 这意味如果要对所有与 placement new 相关的内存泄漏宣战,我们必须同时提 供一个正常的 opera tor delete(用于构造期间无任何异常被抛出〉和一个placement 版本(用于构造期间有异常被抛出)。后者的额外参数必须和。perator new 一样。 只要这样做,你就再也不会因为难以察觉的内存泄漏而失眠。晤,至少不是本条款 所说的这些难以察觉的内存泄漏。 附带一提,由于成员函数的名称会掩盖其外围作用域中的相同名称(见条款 33) ,你必须小心避免让class 专属的 news 掩盖客户期望的其他ne晒(包括正常版 本)。假设你有一个base class,其中声明唯一-个placementoperator new,客户 端会发现他们无法使用正常形式的new: class Base { public: static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad alloe); II这个 new 会遮掩正常的global 形式 E1知ctive C++中文版,第三版260 8 定制 new 和 delete Base* pb = new Base; I I错误!因为正常形式的 operator new 被掩盖 Base* pb = new (std::cerr) Base; II正确,调用Base 的 placementnew. 同样道理, derived classes 中的 operator news 会掩盖 global 版本和继承而得 的 operator new 版本: class Derived: public Base ( II 继承白先前的 Base public: static void* operator new(std::size t size) II重新声明正常形式的 throw(std::bad alloc); II形式的 new Derived* pd = new (std::clog) Derived; Derived* pd = new Derived; II错误!因为 Base 的 II placementnew 被掩盖了。 II 没问题,调用Derived 的 II operator new. 条款 33 更详细地讨论了这种名称遮掩问题。对于撰写内存分配函数,你需要 记住的是,缺省情况下C++ 在 global 作用域内提供以下形式的operator new: void* operator new(std::size t) throw(std::bad alloc); Ilnormalnew. void* operator new(std::size t , void叫 throw () ; IIplacement new. void* operator new(std: :size t , Ilnothrownew, const std::nothrow t&) throw(); II见条款 49. 如果你在 class 内声明任何 operator news,它会遮掩上述这些标准形式。除 非你的意思就是要阻止class 的客户使用这些形式,否则请确保它们在你所生成的 任何定制型 operator new 之外还可用。对于每一个可用的operator new 也请确定 提供对应的 operator delete。如果你希望这些函数有着平常的行为,只要令你的 class 专属版本调用 global 版本即可。 完成以上所言的一个简单做法是,建立一个base class,内含所有正常形式的 new 和 delete: class StandardNewDeleteForms ( public: II normal new/delete static void* operator new(std::size t size) throw(std::bad alloc) ( return ::operator new(size); } static void operator delete(void* pMemory) throw() ( ::operator delete(pMemory); } Effective C++ 中文版,第三版8 条款 52: 写了 placement new 也要写 placement delete 261 II placement new/delete static void* operator new(std::size t size, void* ptr) throw() { return ::operator new(size, ptr); ) static void operator delete(void* pMemory, void* ptr) throw() { return ::operator delete(pMemory, ptr); } II nothrow new/delete static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); } static void operator delete(void *pMemory, const std::nothrow t&) throw() { ::operator delete(pMemory}; } 凡是想以自定形式扩充标准形式的客户,可利用继承机制及 using 声明式(见 条款 33) 取得标准形式: class Widget: public StandardNewDeleteForrns { II 继承标准形式 public: using StandardNewDeleteForrns::operator new; II让这些形式可见 using StandardNewDeleteForrns::operator delete; static void* operator new(std::size_t size, II添加一个自定的 std: :ostrearn& logStrearn) Ilplacementnew throw(std::bad alloc); static void operator delete(void* pMemory, II 添加一个对应的 std::ostrearn& logStrearn) Ilpl部ement delete throw() ; 请记住 ·当你写一个 placement operator new ,请确定也写出了对应的 placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内 存泄漏。 ·当你声明 placementnew 和 placementdelete,请确定不要无意识(非故意)地 遮掩了它们的正常版本。 El作ctive C++中文版,第三版262 9 杂项讨论 9 杂顶讨论 Miscellany 欢迎来到大杂熔的一章。本章只有3 个条款,但千万别被低微的数字或不迷人 的布景愚弄了,它们都很重要! 第一个条款强调不可以轻忽编译器警告信息。至少,如果你希望你的软件有适 当行为的话,别太轻忽它们。第二个条款带你综览 C++ 标准程序库,其中覆盖由 TRl 引进的重大新机能。最后一个条款带你综览Boost ,那是我认为最重要的一个 C++泛用型网站。如果你尝试写出高效 C++ 软件,却没有参考这些条款所提供的 信息,那么充其量也只是一场事倍功半的恶战。 条款 53: 不要轻忽编译器的警告 Pay attention to compiler warnings. 许多程序员习惯性地忽略编译器警告。他们认为,毕竟,如果问题很严重,编 译器应该给一个错误信息而非警告信息,不是吗?这种想法对其他语言或许相对无 害,但在 C++,我敢打赌编译器作者对于将会发生的事情比你有更好的领悟。举个 例子,下面是多多少少都会发生在每个人身上的一个错误: class B( public: virtual void f( ) const; class D: public B( public: virtual void f( ); Effective C++ 中文版 F 第三版9 条款 54: 让自己熟悉包括 TR1 在内的标准程序库 263 这里希望以 0: :f 重新定义 virtual 函数民汀,但其中有个错误: B 中的 f 是个 canst 成员函数,而在 D 中它未被声明为 canst 。我手上的一个编译器于是这样说 话了 z warning: D::f() hides virtual B::f() 太多经验不足的程序员对这个信息的反应是: "噢当然, D::f 遮掩了 B: 汀, 那正是想象中该有的事I "错,这个编译器试图告诉你声明于B 中的 f 并未在 D 中 被重新声明,而是被整个遮掩了(条款33 描述为什么会这样)。如果忽略这个编 译器警告,几乎肯定导致错误的程序行为,然后是许多调试行为,只为了找出编译 器其实早就侦测出来并告诉你的事情。 一旦从某个特定编译器的警告信息中获得经验,你将学会了解,不同的信息意 味什么一一那往往和它们"看起来"的意义十分不同!尽管一般认为,写出一个在 最高警告级别下也无任何警告信息的程序最是理想,然而一旦有了上述的经验和对 警告信息的深刻理解,你倒是可以选择忽略某些警告信息。不管怎样说,在你打发 某个警告信息之前,请确定你了解它意图说出的精确意义。这很重要。 记住,警告信息夭生和编译器相依,不同的编译器有不同的警告标准。所以, 草率编程然后倚赖编译器为你指出错误,并不可取。例如上述发生"函数遮掩"的 代码就可能通过另一个编译器,连半句抱怨和抗议也没有。 请记住 ·严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级 别下争取"无任何警告"的荣誉。 ·不要过度倚赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。 一旦移植到另一个编译器上,你原本倚赖的警告信息有可能消失。 条款 54: 让自己熟悉包括TRl 在内的标准程廖库 Familiarize yourselfwith the standard library, including TR1. C++ Standard 一一定义 C++ 语言及其标准程序库的规范一一-早在 1998 年就被 标准委员会核准了。标准委员会又于 2003 年发布一个不很重要的"错误修正版", 并预计于 2008 年左右发布。 + Standard 2.0 。日期的不确定性使得人们总是称呼下 一版 C++ 为 "C++Ox" ,意指 200x 版的。+。 El知ctive C++ 中文版,第三版264 9 杂项讨论 C忡。x 或许会覆盖某些有趣的语言新特性,但大部分新机能将以标准程序库的 扩充形式体现。如今我们已经能够知道某些新的程序库机能,因为它被详细叙述于 一份称为 TRI 的文档内。 TRI 代表 "Technical Report I" ,那是 C++ 程序库工作小 组对该份文档的称呼。标准委员会保留了TRI 被正式铭记于 C++Ox 之前的修改权, 不过目前己不可能再接受任何重大改变了。就所有意图和目标而言,TRI 宣示了一 个新版 C++ 的来临,我们可能称之为 Standard C++ 1. 1 。不熟悉 TRI 机能而却奢望 成为一位高效的 C++ 程序员是不可能的,因为 TRI 提供的机能几乎对每一种程序 库和每一种应用程序都带来利益。 在概括论述 TRI 有些什么之前,让我们先回顾一下 C++98 列入的 C++ 标准程 序库有哪些主要成分: • STL (Standard Template Library ,标准模板库) ,覆盖容器 ( containers 如 vector, string,旧p) 、迭代器 ( iterators )、算法 ( algorithms 如 fin d, sort, transform) 、 函数对象(向nction 0句;ects 如 less , greater) 、各种容器适配器 ( container adapters 如 stack , priority queue) 和函数对象适配器 (function object adatpers 如 mem fun , notl) 。 • los仕earns ,覆盖用户自定缓冲功能、国际化 ν0 ,以及预先定义好的对象 ci口, cout, cerr 和 clog 。 ·国际化支持,包括多区域 (multiple active locales) 能力。像 wchar t (通常是 16 bits/char) 和 wstring( 由 wchar ts 组成的 strings) 等类型都对促进 Unicode 有所帮助。 ·数值处理,包括复数模板 (complex) 和纯数值数组 (valarray) 。 ·异常阶层体系 (exception hierarc~y) ,包括 base class exception 及其 derived classes logic error和 runtime error,以及更深继承的各个classes。 • C89 标准程序库。 1989 C 标准程序库内的每个东西也都被覆盖于 C++ 内。 如果你对上述任何→项不很熟悉,我建议你好好排出一些时间,带着你最喜爱 的 C++ 书籍,把情势扭转过来。 TRl 详细叙述了 14 个新组件( components ,也就是程序库机能单位) ,统统 都放在 std 命名空间内,更正确地说是在其嵌套命名空间 trl 内。因此, TRl 组件 sharedytr 的全名是 std::trl::shared_ptro 本书通常在讨论标准程序库组件时 略而不写 std: :,但我总是会在 TRl 组件之前加上 trl: :。 Et知ctive C++ 中文版,第三版9 条款 54: 让自己熟悉包括 TRl 在内的标准程序库 本书展示以下 TRl 组件实例: 265 ·智能指针 (smart pointers) trl::shared ptr 和 trl::weak ptr。前者的作用 有如内置指针,但会记录有多少个trl::shared_ptrs共同指向同→个对象。这 便是所谓的 r价renee counting ( 引用计数)。一旦最后一个这样的指针被销毁, 也就是一旦某对象的引用次数变成 0 ,这个对象会被自动删除。这在非环形 (acyclic) 数据结构中防止资源泄漏很有帮助,但如果两个或多个对象内含 trl: : shared_ptrs 并形成环状 (cycle) ,这个环形会造成每个对象的引用次数 都超过命一一即使指向这个环形的所有指针都已被销毁(也就是这一群对象整体 看来己无法触及)。这就是为什么又有个 trl: :weak ptrs 的原因。 trl: :weak_ptrs 的设计使其表现像是"非环形trl: : sharedytr-based 数据结 构"中的环形感生指针 (cycle-inducing pointers) 0 trl::weak_ptrs 并不参与 引用计数的计算:当最后一个指向某对象的trl: : shared ptr 被销毁,纵使还 有个 trl ::weak_ptrs 继续指向同一对象,该对象仍旧会被删除。这种情况下的 trl ::weakytrs 会被自动标示无效。 trl: : shared ptr 或许是拥有最广泛用途的TRl 组件。本书多次使用它,条款 13 解释它为什么如此重要。本书并未示范使用trl: :weakytr,抱歉。 • trl:: function,此物得以表示任何callable entity ( 可调用物,也就是任何函数 或函数对象) ,只要其签名符合目标。假设我们想注册一个 callback 函数,该 函数接受一个 int 并返回一个 stri呵,我们可以这么写z void registerCallback(std::string func(工nt)) ; II参数类型是函数,该函数接受一个 int 并返回一个 string 其中参数名称 func 可有可无,所以上述的 registerCallback 也可以这样声明: void registerCallback(std::string (int)); II与上同;参数名称略而未写 注意这里的 "std: :string (int)" 是个函数签名。 El知ctive C++中文版,第三版266 9 杂项讨论 trl::function 使上述的 RegisterCallback 有可能更富弹性地接受任何可调用 物 (callable entity) ,只要这个可调用物接受一个int 或任何可被转换为int 的 东西,并返回一个 string 或任何可被转换为 string 的东西。 trl::function 是个 template,以其目标函数的签名(target function signature) 为参数: void registerCallback (std: : trl: : function func) ; II参数"也町'接受任何可调用物 (ωllable entity) II 只要该"可调用物"的签名与 "std::string (int)" →致 这种弹性真令人惊讶,我尽最大的努力在条款 35 示范了它的用法。 • trl::b缸吼,它能够做 STL 绑定器 (binders) bindlst 和 bind2 nd 所做的每一件 事,而又更多。和前任绑定器不同的是, trl: :bind可以和 canst 及 non-canst 成员函数协同运作,可以和by-reference参数协同运作。而且它不需特殊协助就 可以处理函数指针,所以我们调用trl: :bind 之前不必再被什么 ptr fun, mem fun 或 mem fun ref 搞得一团混乱了。简单地说,trl: :bind 是第二代绑定 工具 (binding facility) ,比其前一代好很多。我在条款35 示范过它的用法。 我把其他 TRI 组件划分为两组。第一组提供彼此互不相干的独立机能z • Hash tables,用来实现 sets, multisets, maps 和 multi-maps 。每个新容器的接口都 以其前任 (TRl 之前的)对应容器塑模而成。最令人惊讶的是它们的名称: trl: : unordered二set , trl::unordered_multiset , trl::unordered_map 以及 trl: :u口ordered multimap。这些名称强调它们和 set, multiset, map 或 multimap 不同 z 以 hash 为基础的这些 TRl 容器内的元素并无任何可预期的次 序。 ·正则表达式( Regular expressions) ,包括以正则表达式为基础的字符串查找和 替换,或是从某个匹配字符串到另一个匹配字符串的逐一选代(iteration) 等等。 • Tuples (变量组) ,这是标准程序库中的pair template 的新一代制品。 pair 只 能持有两个对象, trl:: tuple 可持有任意个数的对象。漫游于P州Ion 和 Eiffel 的程序员,额手称庆吧!你们前一个家园的某些好东西现在已经纳入C++ 。 Effective C++ 中文版 F 第三版9 条款 54: 让自己熟悉包括 TRI 在内的标准程序库 267 • t恒rl: :缸'ra:句:y, 本质上是个"咆STL 化"数组,即→个支持成员函数如 b民egi扫n 和 e凹r口l 的数组。不过 trl: :array 的大小固定,并不使用动态内存。 • trl:: 翩翩L缸,这是个语句构造上与成员函数指针 (member function pointers) 一致的东西。就像 trl: :bind 纳入并扩充 C++饨的 bindlst 和 bind2nd 的能 力一样, trl: :mem fn 纳入并扩充了 C++饨的 mem fun 和 mem fun ref 的能力。 • trl: :ref,缸剧。气晴鸣:per,→个"让 references 的行为更像对象"的设施。它 可以造成容器"犹如持有references" 。而你知道,容器实际上只能持有对象或 指针。 ·随机数 (random number) 生成工具,它大大超越了rand,那是 C++ 继承自 C 标准程序库的一个函数。 ·数学特殊函数,包括Laguerre多项式、Bessel 函数、完全椭圆积分(complete elliptic integrals) ,以及更多数学函数。 • C99 兼容扩充。这是一大堆函数和模板( templates) ,用来将许多新的 C99 程 序库特性带进 C++ 。 第二组 TRI 组件由更精巧的 template 编程技术(包括 template metaprogramming,也就是模板元编程,见条款48) 构成 z • Type traits,一组 traits classes (见条款 47) ,用以提供类型 (types) 的编译期信 息。给予一个类型 T , TRI 的 type traits 可以指出 T 是否是个内置类型,是否提 供 virtual 析构函数,是否是个 empty class (见条款 39) ,可隐式转换为其他类 型 U 吗……等等。 TRI 的 type traits 也可以显现该给定类型之适当齐位 (proper alignment) ,这对定制型内存分配器(见条款50) 的编写人员是十分关键的信 息。 • trl: :result_of,这是个 template,用来推导函数调用的返回类型。当我们编 写 templates 时,能够"指涉 (r,价rω) 函数(或函数模板)调用动作所返回的 对象的类型"往往很重要,但是该类型有可能以复杂的方式取决于函数的参数 类型。 trl::result of 使得"指涉函数返回类型"变得十分容易。它也被TRl 自身的若干组件采用。 虽然若干 TRI 成分(特别是 trl: :bind和 trl: :mem fn) 纳入了某些"前 TRl" 组件能力,但其实 TRl 是对标准程序库的纯粹添加,没有任何 TRI 组件用来替换 既有组件,所以早期(写于 TRl 之前的)代码仍然有效。 El知ctive C++ 中文版,第三版268 9 杂项讨论 TRl 自身只是一份文档唱。为了取得它所规范的那些机能,你还需要取得实现 代码。这些代码最终会随编译器出货。在我下笔的 2005 年此刻,如果你在你于上 的标准程序库实现版本内寻找 TRl 组件,极可能有某些遗漏。幸运的是你可以补齐 它们: TRl 的 14 个组件中的 10 个奠基于免费的 Boost 程序库(见条款 55) .所以 对 TR1-like 机能而言. Boost 是个绝佳资源。我说 "TR1-like" 是因为虽然许多 TRl 机能奠基于 Boost 程序库,但毕竟有些 Boost 机能并不完全吻合 TRl 规范。当你阅 读这一段文字,说不定 Boost 已经不只提供与 TRl 一致的实现(对于那些奠基于 Boost 程序库的 10 个 TRl 组件) .还供应 4 个不以 Boost 为基础的 TRl 组件实现。 在编译器附带 TRl 实现品的那一刻到来之前,如果你喜欢以 Boost 的 TR1-like 程序库作为一时权宣,或许你会愿意以一个命名空间上的小伎俩让自己将来好过 些。所有 Boost 组件都位于命名空间 boost 内,但 TRl 组件都置于 std::trl 内。 你可以这样告诉你的编译器,令它对待 r价 renc臼 to std: :trl 就像对待r价rences ω boost 一样: n缸nespace std { namespace trl ::boost; //namespace std::trl 是 //namespace boost 的一个别名 纯就技术而言,这简直是把你流放到"未定义行为"的国土去了,因为就如条 款 25 所言,任何人不得加任何东西到std 命名空间去。然而实际上你很可能不会 有任何麻烦。一旦将来你的编译器提供它们自己的TRl 实现品,你需要做的唯一事 情就是消除上述的namespace 别名,而后指涉std::trl 的代码继续生效,好极了。 非以 Boost 程序库为基础的那些TRl 组件之中,最重要的或许是hash tables。 其实 hash tables 早已行之有年,分别以名称hash_set, hash_ffiultiset, hash_map 和 hash_ffiultimap为人熟知。也许你的编译器已经附带那些templates 实现码。如 果没有,请启动你最喜欢的查找引擎,查找那些名称(及其TRl 称号) .你一定可 以找到若干来源,包括商业产品和免费产品。 唱在我下笔此刻的 2005 年初,这份文件尚未定稿,其URL 常有变化。我建议你咨询 Effective C++ TRl 信息网页. htφ://aristeia.com 尼C3E /TRUnfo.html 。这个 URL 很稳定。 E庐 ctive C++ 中文版,第三版9 条款 55: 让自己熟悉 Boost 请记住 269 • C++ 标准程序库的主要机能由 STL 、 iostreams 、 locales 组成。并包含 C99 标准 程序库。 • TRl 添加了智能指针(例如 trl::shared ptr) 、一般化函数指针 (trl: : function) 、 hash-based 容器、正则表达式 (regular expressions) 以及另外 10 个组件的支持。 • TRl 自身只是一份规范。为获得 TRl 提供的好处,你需要一份实物。一个好的 实物来源是 Boost 。 条款 55: 让自己熟悉 Boost Familiarize yourselfwith Boost. 你正在寻找一个高质量、源码开放、平台独立、编译器独立的程序库吗?看看 Boost 吧。有兴趣加入一个由雄心勃勃充满才干的 C++ 开发人员组成的社群,致力 发展(设计和实现)当前最高技术水平之程序库吗?看看 Boost 吧!想要一瞥未来 的 C++可能长相吗?看看 Boost 吧! Boost 是一个 C++ 开发者集结的社群,也是一个可自由下载的 C++ 程序库群。 它的网址是 http://boost.o 呵。现在你应该把它设为你的桌面书签之一。 当然,世上多得是 C++ 组织和网站,但 Boost 有两件事是其他任何组织无可 匹敌的。第一,它和 C++ 标准委员会之间有着独一无二的密切关系,并且对委员 会深具影响力。 Boost 由委员会成员创设,因此 Boost 成员和委员会成员有很大的 重叠。 Boost 有个目标 2 作为一个"可被加入标准 C++ 之各种功能"的测试场。这 层关系造就的结果是,以 TRl (见条款 54) 提案进入标准 C++ 的 14 个新程序库 中,超过三分之二奠基于 Boost 的工作成果。 Boost 的第二个特点是 z 它接纳程序库的过程。它以公开进行的同僚复审 (public peer review) 为基础。如果你打算贡献一个程序库给Boost,首先要对 Boost 开发者 电邮名单 (mailing list)投递作品,让他们评估这个程序库的重要性,并启动初步 审查程序。然后开始这个网站所谓的"讨论、琢磨、再次提交"循环周期,直到一 切都获得满足为止。 最后,你准备好你的程序库,要正式提交了。会有一位复审管理员出面确认你 的程序库符合 Boost 最低要求。例如它必须通过至少两个编译器(以展现至此仍还 微不足道的可移植性) ,你必须证明你的程序库在一个可接受的授权许可下是可用 Effective C++ 中文版,第三版270 9 杂项讨论 的(例如这个程序库必须允许免费的商业化和非商业化用途)。然后你的提交正式 进入 Boost 社群,等待官方复审。复审期间会有志愿者察看你的程序库各种素材(例 如源码、设计文档、使用说明等等) ,并考虑诸如此类的问题: ·这一份设计和实现有多好? ·这些代码可跨编译器和操作系统吗? ·这个程序库有可能被它所设定的目标用户一一也就是在这个程序库企图解决问 题的领域中工作的人们一一使用吗? ·文档是否清楚、齐备,而且精确? 所有批注都会被投寄至一份 Boost 邮件列表,所以复审者和其他人可以看到并 响应其他人的评论。复审最后周期结束之后,复审管理员便表决你的程序库被接受、 被有条件接受,或被拒绝。 同僚复审对于阻挡低劣的程序库很有贡献,同时也教育程序库作者认真考虑­ 个工业强度、跨平台的程序库的设计、实现和文档工程。许多程序库在被 Boost 接 受之前,往往经历了一次以上的官方复审。 Boost 内含数十个程序库,而且还不断有更多添加进来。偶尔也会有程序库被 从中移除,通常那是因为它们的机能己被新程序库取代,而新程序库提供了更多、 更好的机能,或更好的设计(例如更弹性或更有效率)。 Boost 各程序库之间的大小和作用范围有很大变化。举-个极端例子,某些程 序库概念上只需数行代码(但在加入错误处理和可移植性后往往变长很多〉。例如 Conversion 程序库,提供较安全或较方便的转型操作符,其口 urr配 ic cast 函数在 将数值从某类型转换为另一类型而导致溢出 (overflow) 或下溢( underflow) 或类 似问题时会抛出异常。 lexical cast 则使我们得以将任何类型(只要支持 operator«) 转换为字符串,对程序的诊断和运转志记 v; II让 boost: :1田1bda 的机能曝光 std::for_each(v.begin() , v.end() , II针对 v 内的每一个元素 X , std::cout« 1 * 2 + 10 « "\n"); I I 印出 x * 2+10; II其中" l It是Lambda 程序库 II针对当前元素的一个 II 占位符号 (placeholder) ·泛型编程(Generic programming) ,覆盖一大组 traits classes。关于 traits 请见条 款 47 。 ·模板元编程 (Template metaprogramming, TMP ,见条款 48) ,覆盖一个针对编 译期 assertions 而写的程序库,以及 Boost MPL 程序库。 MPL 提供了极好的东 西,其中支持编译期实物 (compile-time entities) 诸如 types 的 STL-like 数据结 构,等等。 II创建一个 list幡like 编译期容器,其中收纳三个类型: II(floa飞 double , long double) ,并将此容器命名为 "floats" typedef boost::rnpl::list floats; II再创建一个编译期间用以收纳类型的list ,以 "floa饵"内的类型为基础, II最前面再加上 "int"。新容器取名为可pes" 。 typedef boost::盯pl::push_front::type types; 这样的"类型容器"(常被称为typelists一一虽然它们也可以以一个rnpl: :vector 或 rnpl:: list 为基础)开启了一扇大门,通往大范围、火力强大且重要的TMP 应用程序。 ·数学和数值 (Math and numerics) ,包括有理数、八元数和四元数(octonions and quaternions) 、常见的公约数 (divisor) 和少见的多重运算、随机数(又一个影 El知ctive C++中文版,第三版272 9 杂项讨论 响 TRl 内部相关机能的程序库)。 ·正确性与测试 (Correctness and testing) ,覆盖用来将隐式模板接口 (implicit template interfaces ,见条款 4 1)形式化的程序库,以及针对"测试优先"编程 形态而设计的措施。 ·数据结构,覆盖类型安全(type-safe) 的 unions (存储各具差异之"任何"类型) , 以及 tuple 程序库(它是 TRl 同类机能的基础〉。 ·语言间的支持 Onter-language support) ,包括允许 C++ 和 Python 之间的无缝 互操作性 (seamless interoperability )。 ·内存,覆盖 Pool 程序库,用来做出高效率而区块大小固定的分配器(见条款 50) , 以及多变化的智能指针(smart pointers ,见条款 13) ,包括(但不仅仅是) TRl 智能指针。另有一个non-TRl 智能指针是 scoped_a口呵,那是个 auto_ptr-like 智能指针,用来动态分配数组:条款44 曾经示范其用法。 ·杂项,包括CRC 检验、日期和时间的处理、在文件系统上来回移动等等。 请记住,这只是可在Boost 中找到的程序库抽样,不是一份详尽清单。 Boost 提供的程序库可以做很多恨多事,但它并未覆盖整套编程风光。例如其 中就没有针对GUI 开发而设计的程序库,也没有用以连通数据库的程序库一一至少 在我下笔此刻没有。然而当你阅读本书时就有了也说不定。到底有没有,唯一可以 确定的办法是常常上网检核。我建议你现在就去访问:http://boost. 0呵。纵使你 没能找到刚好符合需求的作品,也→定会在其中发现一些有趣的东西。 请记住 • Boost 是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的 C+十程 序库开发。 Boost 在 C++ 标准化过程中扮演深具影响力的角色。 • Boost 提供许多 TRl 组件实现品,以及其他许多程序库。 E1知ctive C++ 中文版,第三版A 本书之外 273 A 本书之外 Beyond Effective C++ «Effective C++» 一书覆盖我认为对于以编程为业的 C++ 程序员最重要的→般 性准则。如果你有兴趣更强化各种高效做法,我鼓励你试试我的另外两本书 : «More E1作 ctive C++» 和 «E1知ctive STL» 。 «More Effective C++» 覆盖了另一些编程准则,以及对于效能和异常的广泛论 述。它也描述重要的 C++ 编程技术如智能指针( smart pointers) 、引用计数 (reference counting) 和代理对象 (proxy objects) 等等。 «Effective STL» 是一本和 «Effective C++» →样的准则导向( guideline-oriented) 书籍,专注于对 STL (Standard Template Library ,标准模板库)的高效运用。 两本书的目录摘要于下。 «More E加clive C++» 目录 Basics Item 01: Distinguish between pointers and references Item 02: Prefer C++-style casts Item 03: Never treat aηays polymorphically Item 04: Avoid gratuitous default constructors Operators Item 05: Be wary ofuser-defined conversion functions Item 06: Distinguish between prefix and postfix forms ofincrement and decrement operators Item 07: Never overload &&, II, or , Item 08: Understand the different meanings ofnew and delete Effective C++ 中文版,第二版274 A 本书之外 Exc叩,tions Item 09: Use destructors to prevent r巳吕ource leaks Item 10: Prevent resource leaks in constructors Item 11: Prevent exceptions from leaving destructors Item 12: Understand how throwing an exception differs 企om passing a parameter or calling a virtual function Item 13: Catch exceptions by reference Item 14: Use exception specifications judiciously Item 15: Understand the costs ofexception handling E缸。iency Item 16: Remember the 80-20 rule Item 17: Consider using lazy evaluation Item 18: Amortize the cost ofexpected computations Item 19: Understand the origin oftemporary objects Item 20: Facilitate the return value optimization Item 21: Overload to avoid implicit type conversions Item 22: Consider using op= instead ofstand-alone op Item 23: Consider alternative libraries Item 24: Understand the costs of virtual functions, multiple inheritance, virtual base classes, and RTTI Techniques Item 25: Virtualizing constructors and non-member functions Item 26: Limiting the number ofobjects ofa class Item 27: Requiring or prohibiting heap-based objects Item 28: Smart pointers Item 29: Reference counting Item 30: Proxy classes Item 31: Making functions virtual with respect to more than one object Miscellany Item 32: Program in the future tense Item 33: Make non-leafclasses abstract Item 34: Understand how to combine C++ and C in the same program Item 35: Familiarize yourselfwith the language standard Effective C++ 中文版,第三版A 本书之外 275 «Effective STL» 目录 Chap阳 1: Containers Item 0I: Choose your containers with care. Item 02: Beware the illusion ofcontainer-independent code. Item 03: Make copying cheap and correct for objects in containers. Item 04: Call empty instead ofchecking sizeO against zero. Item 05: Prefer range member functions to their single-element counterparts. Item 06: Be alert for C++'s most vexing parse. Item 07: When using containers ofnewed pointers, remember to delete the pointers before the container is destroyed. Item 08: Never create containers ofauto---.ptrs. Item 09: Choose carefully among erasing options. Item 10: Be aware ofallocator conventions and restrictions. Item II: Understand the legitimate uses ofcustom allocators. Item 12: Have realistic expectations about the thread safety ofSTL containers. Chapter 2: vee阳'rands位ittg Item 13: Prefer vector and string to dynamically allocated arrays. Item 14: Use reserve to avoid unnecessary reallocations. Item 15: Be aware ofvariations in string implementations. Item 16: Know how to pass vector and string data to legacy APls. Item 17: Use "the swap 位ick" to trim excess capacity. Item 18: Avoid using vector. ch叩阳 3: Associative Containers Item 19: Understand the difference between equality and equivalence. Item 20: Speci 命 comparison types for associative containers ofpointers. Item 21: Always have comp缸ison functions return false for equal values. Item 22: Avoid in-place key modification in set and multiset. Item 23: Consider replacing associative containers with sorted vectors. Item 24: Choose carefully between map::operator[] and map::inse此 when efficiency is important. Item 25: Familiarize yourselfwith the nonstandard hashed containers. £1如ctive C++ 中文版,第二版276 A 本书之外 Chapter 4: Iterators Item 26: Prefer iterator to const_iterator, reverse_iterator, and constJeverse一iterator. Item 27: Use distance and advance to convert a container's const iterators to lterators. Item 28: Understand how to use a reverse iterator's base iterator. Item 29: Consider istreambuCiterators for character-by-character input. Chapter 5: Algori也IDS Item 30: Make sure destination ranges are big enough. Item 31: Know your sorting options. Item 32: Follow remove-like algorithms by erase ifyou really want to remove something. Item 33: Be wary ofremove-like algorithms on containers ofpointers. Item 34: Note which algorithms expect sorted ranges. Item 35: Implement simple case-insensitive string comparisons via mismatch or lexicographical_compare. Item 36: Understand the proper implementation ofcopy_if. Item 37: Use accumulate or for_each to summarize ranges. Chapter 6: Functors, Func伽 Classes , Functions, etc. Item 38: Design functor classes for pass-by-value. Item 39: Make predicates pure functions. Item 40: Make functor classes adaptable. Item 41: Understand the reasons for ptr_fun, mem_fun, and mem_funJef. Item 42: Make sure less means operator<. Chapt町 7: Programming with the STL Item 43: Prefer algorithm calls to hand-written loops. Item 44: Prefer member functions to algorithms with the same names. Item 45: Distinguish among count, fi时, binary_search, lower_bound, upper_bound, and equalJange. Item 46: Consider function objects instead offunctions as algorithm parameters. Item 47: Avoid producing write-only code. Item 48: Always #include the proper headers. Item 49: Learn to decipher STL-related compiler diagnostics. Item 50: Familiarize yourselfwith STL-related web sites. E1.作ctive C++ 中文版,第三版B 新旧版条款对照 277 B 新旧版条款对照 Item Mappings Between Second and Third Editions «El作ctive C++» 第三版和先前的第二版之间有许多不同,最重要的是它覆盖 了许多新信息。第二版内容大多继续存在于第三版中,不过往往以修改过的形式和 位置出现。下页表格列出第二版条款内的信息可在第三版哪里找到,下下页表格则 是相反方向的对应。 以下表格所列的是信息的对应,不是文字的对应。例如第二版条款39 "避免在 继承体系中做向下转型动作"的观念被移到第三版的条款27. 并赋予崭新的文字和 示例。更极端的例子是第二版条款 18 "努力让 class 接口完满且最小化"。这个条 款的主要结论是,如果函数不需特别访问class 的 non-public 成分,它通常应该被设 计为一个 non-members。第三版中相同的结论却是藉由不同(更强烈)的理由触发, 因此第二版的条款 18 对应至第三版的条款 23. 尽管这两个条款之间的唯一共同点 只是它们的结论。 Effective C++ 中文版,第三版278 第二版映射至第三版 B 新旧版条款对照 第二版 第三版 第二版 第三版 第二版 第三版 2 18 23 35 32 2 19 24 36 34 3 20 22 37 36 4 21 3 38 37 5 16 22 20 39 27 6 13 23 21 40 38 7 49 24 41 41 8 51 25 42 39,44 9 52 26 43 40 10 50 27 6 44 1 14 28 45 5 12 4 29 28 46 18 4 I 如:叩 28 47 4 记 14 7 i 21 53 15 10 3~ 26 49 54 16 12 33 30 50 11 11 34 31 Effective C++ 中文版,第三版B 新|日版条款对照 第三版映射至第二版 279 第三版 第二版 第三版 第二版 第三版 第二版 20 22 39 42 2 21 23,31 40 43 3 21 22 20 41 41 4 12,13,47 23 18 42 5 45 24 19 43 6 27 25 44 42 7 14 26 32 45 8 27 39 46 9 28 29,30 47 10 15 29 48 11 17 30 33 49 7 12 16 31 34 50 10 13 6 32 35 51 8 14 11 33 9 52 9 15 34 36 53 48 16 5 35 54 49 17 36 37 55 18 46 37 38 19 pp.77-79 38 40 Effective C++ 中文版,第二版280 索 51 Index 所有操作符 (operator) 都列于词条 operator 之下,亦即 operator« 列于词条 operator之下而非《之下。依此类推。范例所用之classes、 structs、 class templates、 structtemplates名称列于词条example classes / templates 之下,范例所用之函数名称 列于词条 example户nctions / templates 之下。 译注:由于中译本和英文版页页对译,因此保留英文版完整索引不做任何改动。中 英术语之对照请见 ix 页。 BeforeA .NET 7.81. 135. 145. 194 see also C# =. in initialization vs. assignment 6 1066 150 2nd edition ofthis book compared to 3rd edition xv-xvi. 277-279 see also inside back cover 3rd edition of this book compared to 2nd edition xv-xvi. 277-279 see also inside back cover 80-20 rule 139. 168 A Abrahams. David x叫l.x叫ii. 到x abstract classes 43 accessib也ty control over data members' 95 name, multiple inheritance and 193 accessing names, in templatized bases 207-212 addresses inline functions 136 objects 118 aggregation. see composition Alexandrescu, Andrei 刻X aliasing 54 alignment 249-250 allocators,位1 the SτL 240 E.fj告ctive C++ 中文版,第三版 alte口latives to virtual functions 169-177 ambiguity multiple inheritance and 192 nested dependent names 缸ld types 205 Arbiter, Petronius vu a缸rg伊u山m且lment吮t←t-d由epend由ent芷tI o∞o优kup 110 arithmetic, mixed-mode 103, 222-226 array layout, vs. object layout 73 array new 254-255 array, invalid index and 7 ASPECT RATIO 13 assignment see also operator= chaining assignments 52 copy-and-swap and 56 generalized 220 to self, operator= and 53-57 vs. initialization 6. 27-29, 114 assignment operator, copy 5 auto_ptr, see std::auto_ptr automatically generated functions 34-37 copy constructor and copy assignment operator 221 disallowing 37-39 avoiding code duplication 50. 60 B Bal, Yun xix Barry. Dave. allusion to 229 Barlolucci, Guido 到X索引 base classes copyfng 59 duplication of data in 193 lookup 恼. this-> and 210. 214 names hidden in derived classes 263 polymorphic 44 pol严norphlc. destn.tctors and 4Q-44 templatized 207-212 virtual 193 basic guarantee. the 128 Battle of Hastings 150 Berek. B叫amin 血 bldlrectionallterators 227 bidireetionaUteratoctag 228 binary upgradeabillty.垃111nlng and 138 binding dynamic. see dynamic binding static. see static binding birds 缸ld pengt血IS 151-153 bitwise const member functions 21-22 books C++P,.,咽ramming Language. ηIe xvii C++ Templates xviII Des甸nPa优:erns xvii 町ecti回回1. 273. 275-276 Exceptional C++ xvii Exceptional C++ Style x咐. xviII More 乓[fective C++ 273. 273-274 More exceptional C++ xvii Sα tyricon vii SomeMust Wiαtch while Some Must Sleep 150 Boost 10. 269-272 containers 271 Conversion library 270 correctness and testing support 272 data stn.tctures 272 function 0均ects and higher-order pro- gramming utilities 271 functionality not provided 272 generic programming support 271 Graph library 270 Inter-language support 272 Lambda library 271 math and numerics utilities 271 memory management utilities 272 MPL library 270.271 noncopyable base class 39 Poollibr但y 250.251 scoped_array 65.216.272 shared_array 65 shared_ptr Implementation. costs 83 smart pointers 65. 272 web page xvii string and text utilities 271 template metaprogramming support 271 281 TR1 and 9-10. 268. 269 typelist support 271 web site 10. 269. 272 boost. as synonym for std::trl 268 Bosch. Derek x时II breakpoints. and inlinlng 139 B吨ffi.J the viαmpire Slayer xx bugs. reporting xvi built-in types 26-27 efficiency and passing 89 Incompatlbllltles WI世180 c C standard library and C++ standard library 264 C# 43. 76. 97. 100. 116. 118. 190 see also .NET C++ Programming Language. 霄Ie xvii C++ standard library 263-269 and 144 array replacements and 75 C standard library and 264 C89 standard library and 264 header organization of 101 list template 186 logic_error and 113 set template 185 vector template 75 C++ Ten可olates xviii C++. as language federation 11-13 C++Ox 264 C++-style casts 117 C. as sublanguage of C++ 12 C99 standard library. TRI and 267 caching const and 22 mutable and 22 Cal. Steve 刻X calling swap 110 calls to base classes. casting and 119 Cargill. Tom xviII Carrara. Enrico 刻X Carroll. Glenn xviII casting 116-123 see also const_cast. static_cast. dynamic_cast. and reinterpreccast base class calls and 119 constness away 24-25 encapsulation and 123 gr叩缸ld 117 syntactic fOnDS 116-117 type systems and 116 undefined behavior and 119 chaining assignments 52 El知ctive C++ 中文版,第三版282 Chang. Brandon ~ Clamage. Steve xviII class definitions artificial client dependencies. eliminating 143 class declarations VS. 143 0均 eet sizes and 141 class design. see type design class names. explicitly specifying 162 class. vs. typename 203 classes see also class definitions. Interfaces abstract 43. 162 base see also base classes duplication of data In 193 polymorphiC 44 templatlzed 207-212 virtual 193 defining 4 dertved see also Inheritance virtual base Initialization of 194 Handle 144-145 Interface 145-147 meaning of no virtual 仇mctlons 41 RAIl, see RAIl specification. see Interfaces traits 226-232 client 7 clustertng objects 251 code bloat 24. 135. 230 avoiding. In templates 212-217 copy assignment operator 60 duplication. see duplication exception-safe 127-134 factortng out oftemplates 212-217 Incorrect. efficiency and 90 reuse 195 shartng. see duplication. avoiding Cohen. Jake ~ Comeau. Greg xviII URL for his C/C++ FAQ xviII common features and Inherttance 164 commonality and vartabllity皿alysls 212 compatlblli钞. vptrs and 42 compatible types. accepting 218-222 compilation dependencies 14{}-148 minimizing 14{}-148. 190 pointers. references. and objects and 143 compiler warnings 262-263 calls to vlrtuals and 50 Inlinlng and 136 partial copies and 58 El知ctive C++中文版,第三版 索引 compiler-generated functions 34-37 disallowing 37-39 functions compilers may generate 221 compilers parsing nested dependent names 204 programs executing within. see tem- plate metaprogrammlng register usage and 89 reordertng operations 76 typename and 207 when errors are diagnosed 212 compile静time polymorphism 20I composition 184-186 meanings of 184 replacing prtvate Inherttance with 189 synonyms for 184 vs. prtvate Inherttance 188 conceptual constness. see const. logical consistency with the built-In types 19. 86 const 13. 17-26 bitwise 21-22 caching and 22 casting away 24-25 function declarations and 18 logical 22-23 member functions 19-25 duplication and 23-25 members. initialization of 29 overloading on 19-20 pass by reference and 86-90 passing std::auto_ptr and 220 pointers 17 陀tum value 18 uses 17 VS. #define 13-14 consccast 25. 117 see also casting consCiterator. vs. iterators 18 constants. see const constraints on Interfaces. 仕om k让lerttance 85 constructors 84 copy 5 default 4 empty. l1Iuslon of 137 explicit 5.85. 104 Implicitly generated 34 Inlinlng and 137-138 operator new and 137 possible Implementation In dertved classes 138 relationship to new 73 static fi.且nctlons and 52 virtual 146. 147 virtual functions and 48-52 WI仕1 vs. without arguments 114 containers. In Boost 271索引 containment. see composition continue. delete and 62 control over data members' accessibUity 95 convenience functions 100 Conversion library. in Boost 270 conversions. type. see type conversions copies. pa此tal58 copy assignment operator 5 code in copy constructor and 60 derived classes and 60 copy constructors default definition 35 derived classes and 60 generalized 219 howused 5 implicitly generated 34 pass-by-value and 6 copy-and-swap 131 assignment and 56 exception-safe code and 132 copying base class parts 59 behavior. resource management and 66-69 functions. the 57 0均 ects 57毛O correctness designing interfaces for 7ι83 testing and. Boost support 272 coπesponding forms of new and delete 73--75 corrupt data structures, exception-safe code and 127 cows, coming home 139 crimes against English 39. 204 cross-DLL problem 82 CRTP 246 C-style casts 116 ctor 8 curiously recurring template pattern 246 -0 dangling handles 126 Dashtinezhad. Sasan 对x data members adding, copying functions and 58 control over accessibUity 95 protected 97 static. initialization of 242 why private 9• ·98 data structures exception-safe code and 127 in Boost 272 Davis, Tony xvlll 283 deadly MI diamond 193 debuggers #define and 13 inlme functions and 139 declarations 3 inline functions 135 replacing definitions 143 static const integral members 14 default constructors 4 construction with arguments vs. 114 implicitly generated 34 default implementations for virtual functions. danger of 163--167 of copy constructor 35 of operator= 35 default initialization. unintended 59 default parameters 180-- 183 impact if changed 183 static binding of 182 #define debuggers and 13 disadvantages of 13. 16 vs_ const 13-14 vs_ inline functions 16-17 definitions 4 classes 4 deliberate omission of 38 functions 4 implicitly generated functions 35 objects 4 pure virtual functions 162. 16&-167 replacing with declarations 143 static class members 242 static const integral members 14 templates 4 variable. postponing 11 3--116 delete see also operator delete forms of 73--75 operator delete and 73 relationship to destructors 73 usage problem scenarios 62 delete n, std::auto_ptr and trl::shared_ptr and 65 deleters std::auto_ptr and 68 trl::shared_ptr and 68.81-83 Delphi 97 Dement, William 150 dependencies. compilation 140-148 dependent names 204 dereferencing a null pointer. undefined behavior of 6 derived classes copy assigqment operators and 60 copy constructors and 60 hiding names in base classes 263 Effective C++ 中文版,第三版284 Implementing constructors In 138 virtual base Initialization and 194 design contradiction In 179 oflnterfaces 78-83 of types 78-86 Design P,αttems xvii design patterns curtously recurring template ICRrP) 246 encapsulation and 173 generating from templates 237 Singleton 31 Strategy 171-177 Template Method 170 T币liP and 237 destructors 84 exceptions and 44-48 inllning and 137-138 pure 叫rtual43 relationship to delete 73 resource managing 0均ects and 63 static functions and 52 virtual operator delete and 255 polymorphic base classes and 40-44 virtual functions and 48-52 Dewhurst, Steve xvii dimensional unit correctness.τMP and 236 DLLs. delete and 82 dtor 8 Dulimov. Peter xix duplication avoiding 23--25.29.50.60. 164. 183.212­ 217 base class data and 193 init function and 60 dynamic binding definition of 181 。f virtual functions 179 d严lamlc type. definition of 181 dynamic_cast 50. 117. 120-123 see also casting efficlencyof 120 E early binding 180 easy to use correctly and hard to use Incorrectly 78-83 EBO. see empty base optimization 乓[fective C++. comp町ed to More 乓[fecttve C++ 缸ld 乓g泣cttve STL 273 Effective STL 273. 275-276 compared to 乓[fectt 回归+ 273 Effective C++中文版,第三版 索引 contents of 275-276 efficiency assignment vs. construction and destruction 94 default parameter binding 182 dynamiccast 120 Handle classes 147 Incorrect code and 90. 94 Inlt. with vs. without args 114 Interface classes 147 macros vs. InIine functions 16 member Inlt. vs. assignment 28 minimizing compilation dependencies 147 operator new/operator delete and 248 pass-by-reference and 87 pass-by-value and 86-87 passing built-In types 臼ld 89 runtime vs. compile-time tests 230 template metaprogrammlng and 233 template vs. function parameters 216 unused 0问jects 113 virtual functions 168 EiITel 100 embedding. see composition empty base optimization lEBO) 190-191 encapsulation 95. 99 casts and 123 design patterns and 173 handles and 124 measuring 99 protected members and 97 RAIl classes and 72 enum hack 15-16.236 errata list. for this book xvi errors detected during linking 39. 44 runtime 152 evaluation order. ofparameters 76 example classes/templates A4 ABEntry 27 AccessLevels 95 Address 184 Airplane 164. 165. 166 Airport 164 AtomicClock 40 AWOV 43 B 4. 178.262 Base 54.118.137.157.158.159.160.254. 255.259 BelowBottom 219 bidirectionaUterator_tag 228 Bird 15 1, 152. 153 Bitmap 54 Borrowableltem 192 Bottom 218 BuyTransaction 49.51索引 C 5 Circle 181 CompanyA 208 CompanyB 208 CompanyZ 209 CostEstimate 15 CPerson 198 CTextBlock 21.22.23 Customer 57, 58 o 178, 262 DatabaselD 197 Date 58, 79 Day 79 DBConn 45, 47 DBConnection 45 deque 229 deque::iterator 229 Derived 54, 118, 137, 157, 158, 159, 160, 206, 254, 260 Directory 31 ElectronicGadget 192 Ellipse 161 Empty 34, 190 EvilBadGuy 172, 174 EyeCandyCharacter 175 Factorial 235 FactoriakO> 235 File 193, 194 FileSystem 30 FlyingBird 152 Font 71 forward_iterator_tag 228 Gan、 eCharacter 169, 170, 172, 173, 176 GameLevel 174 GamePlayer 14, 15 GraphNode 4 GUIObject 126 HealthCalcFunc 176 HealthCalculator 174 HoldsAnlnt 190, 191 HomeForSale 37, 38, 39 inpuUteratoctag 228 inpuciterator_tag 230 InputFile 193, 194 Invest 町、 ent 61 , 70 IOFile 193, 194 IPerson 195, 197 iterator_traits 229 see also std::iterator traits list 229 Iist::iterator 229 Lock 66.67.68 LoggingMsgSender 208.210.211 Middle 218 ModelA 164. 165, 167 ModelB 164, 165. 167 ModelC 164, 166, 167 Month 79, 80 MP3Player 192 285 Msglnfo 208 MsgSender 208 MsgSender 209 NamedObject 35, 36 NewHandlerHolder 243 NewHandlerSupport 245 outpuUteratoctag 228 OutputFile 193. 194 Penguin 151. 152. 153 Person 86, 135, 140, 141 , 142, 145, 146, 150, 184. 187 Personlnfo 195. 197 PhoneNumber 27 , 184 PMlmpl 131 Point 26.41, 123 PrettyMenu 127.130. 131 PriorityCustomer 58 random_access_iterator_tag 228 Rational 90. 102, 103. 105, 222, 223, 224, 225, 226 RealPerson 147 Rectangle 124. 125, 154. 161, 181. 183 RectData 124 SellTransaction 49 Set 185 Shape 161, 162. 163, 167, 180, 182, 183 5口、 art Pt r 218, 219.220 SpecialString 42 SpecialWindow 119, 120. 12 1, 122 SpeedDataCollection 96 Square 154 SquareMatrix 213 电 214 ‘ 215 , 216 SquareMatrixBase 214, 215 StandardNewDeleteForms 260 Student 86, 150, 187 TextBlock 20, 23, 24 TimeKeeper 40.41 Timer 188 Top 218 Transaction 48.50, 51 Uncopyable 39 WaterClock 40 WebBrowser 98, 1 ∞, 101 Widget 4, 5, 44, 52, 53, 54, 56, 107, 108, 109, 118, 189, 199, 201 , 242, 245, 246, 257, 258, 261 Widget::WidgetTimer 189 Widgetlmpl 106, 108 Window 88. 119, 12 1, 122 WindowWithScrollBars 88 WristWatch 40 X 242 Y 242 Year 79 example functions/templates ABEntry::ABEntry 27.28 AccessLevels::getReadOnly 95 AccessLevels::getReadWrite 95 AccessLevels::setReadOnly 95 Ej如ctive C++中文版,第三版286 Accesslevels::setWriteOnly 95 advance 228.230, 232, 233, 234 Airplane::defaultFly 165 Airplane::fIy 164, 165, 166. 167 askUserForDatabaselD 195 AWOV::AWOV 43 B::mf 178 Base::operator delete 255 Base::operator new 254 Bird::fIy 151 Borrowableltem::checkOut 192 boundingBox 126 BuyTransaction::BuyTransaction 51 BuyTransaction::createlogString 51 calcHealth 174 callWithMax 16 changeFontSize 71 Circle::draw 181 c1earAppointments 143. 144 c1earBrowser 98 CPerson::birthDate 198 CPerson::CPerson 198 CPerson::name 198 CPerson::valueDelimClose 198 CPerson::valueDelimOpen 198 createlnvestment 62.70.81.82.83 CTextBlock::length 22. 23 CTextBlock::operator[] 21 Customer::Customer 58 Customer::operator= 58 D::mf 178 Date::Date 79 Day::Day 79 daysHeld 69 DBConn::-DBζonn 45.46.47 DBConn::c1 ose 47 defaultHealthCalc 172. 173 Derived::Derived 138. 206 Derived::mfl 160 Derived::mf4 157 Direetory::Directory 3 1, 32 doAdvance 231 doMultiply 226 doProcessing 200. 202 doSomething 5. 44. 54. 110 doSomeWork 118 eat 151, 187 ElectronicGadget::checkOut 192 Empty::-Empty 34 Empty::Empty 34 Empty::operator= 34 encryptPassword 114. 115 error 152 EvIlBadGuy::EvIlBadGuy 172 f 62. 63. 64 FlyingBird: 刑Y 152 Font::-Font 71 Font::Font 71 Font::get 71 £1知 clive C++ 中文版,第二版 索引 Font::operator FontHandle 71 GameCharacter::doHealthValue 170 GameCharacter::GameCharacter 172. 174, 176 GameCharacter::healthValue 169. 170, 172.174, 176 Gamelevel::health 174 getFont 70 hasAcceptableQuality 6 HealthCaIcFunc::calc 176 HealthCalculator::operator() 174 lock 66 lock::-lock 66 lock::lock 66. 68 logCall 57 loggingMsgSender::sendClear 208, 210 logginMsgSender::sendClear 210.211 loseHealthQuickly 172 loseHealthSlowly 172 main 14 1, 142.236.241 makeBigger 154 makePerson 195 max 135 ModelA::fIy 165, 167 ModeIB:: 何 Y 165. 167 ModelC::fIy 166. 167 Month::Dec 80 Month::Feb 80 Month::Jan 80 Month::Month 79, 80 MsgSender::sendClear 208 MsgSender::sendSecret 208 MsgSender::sendSecret 209 NewHandlerHolder::-NewHandlerHolder 243 NewHandlerHolder::NewHandlerHolder 243 NewHandlerSupport::operator new 245 NewHandlerSupport::secnew_handler 245 numDigits 4 operator delete 255 operator new 249, 252 operato俨 91.92.94.105.222.224.225 , 226 operator== 93 outOfMem 240 Penguin::何Y 152 Person::age 135 Person::create 146, 147 Person::name 145 Person::Person 145 Personlnfo: :t heNa 町、e 196 Personlnfo::valueDelimClose 196 Personlnfo::valueDelimOpen 196 PrettyMenu::changeBackground 127. 128, 130. 131 print 20 print2nd 204, 205 printNameAndDisplay 88. 89 priority 75 PriorityCustomer::operator= 59索引 Priority(ustomer::PriorityCustomer 59 processWidget 75 ReaIPerson::-ReaIPerson 147 ReaIPerson::ReaIPerson 147 Rectangle::doDraw 183 Rectangle::draw 181. 183 Rectangle::lowerRight 124. 125 Rectangle::upperLeft 124. 125 releaseFont 70 Set::insert 186 Set::member 186 Set::remove 186 Set::size 186 Shape::doDraw 183 Shape::draw 161. 162. 180. 182. 183 Shape::error 161.163 Shape::objectID 161. 167 SmartPtr::get 220 SmartPtr::SmartPtr 220 SO"、 eFunc 132. 156 SpeciaIWindow::blink 122 SpeciaIWindow::onResize 119. 120 SquareMatrix::invert 214 SquareMatrix::setDataPtr 215 SquareMatrix::SquareMatrix 215.216 StandardNewDeleteForms::operator delete 260.261 StandardNewDeleteForms::operator new 260.261 std::swap 109 std::swap 107. 108 study 15 1, 187 swap 106.1ω tempDir 32 TextBlock::operator[) 20. 23. 24 tfs 32 Timer::onTick 188 Transaction::init 50 Transaction::Transaetion 49.50.51 Uncopyable::operator= 39 Uncopyable::Uncopyable 39 unlock 66 validateStudent 87 Widget::onTick 189 Widget::operator new 244 Widget::operator+= 53 Widget::operator= 53. 54. 55. 团 .107 Widget::set_new_handler 243 Widget::swap 108 Window::blink 122 Window::onResize 119 workWithlterator 206. 207 Year::Year 79 exception specifications 85 ExceptωnalC++ x咄 Exceptional C++ S阳Ie xv臼 .x时H exceptions 113 delete and 62 287 destructors and 4今-48 member swap and 112 standard hierarchy for 264 swallowing 46 unused 0句ects and 114 exception-safe code 127-134 copy-and-swap and 132 lega叩 code and 133 pimpl idiom and 131 side effects and 132 exception-safety gt且ar缸~tees 128-129 explicit calls to base class functions 211 explicit constructors 5. 85. 104 generaliZed copy construction and 219 explicit tnline request 135 explic 挝 specification. ofclass names 162 explicit type conversions vs. implicit 70- 72 皿pression templates 237 expressions. implicit interfaces and 201 F factoring code. out of templates 21也217 factory function 40.62.69.81. 146. 195 Fallenstedt. Martin 刻x federation. oflanguages. C++ as 11-13· Feher. Attl1a F. 刘x final classes. tn Java 43 final methods. tn Java 190 fixed-siZe static buffers. problems of 196 forms of new and delete 73-75 FORfRAN 42 forward iterators 227 forward_iterator_tag 228 forw盯ding functions 144. 160 French. Donald 皿 台1end functions 38. 85. 105. 135. 173. 223­ 225 vs. member functions 98-102 台1endship tnr四llife 105 without needing special access rights 225 Fruchterman.τllomas 刻X FUDGE FACTOR 15 Fuller. John 皿 function declarations. const in 18 function objects deflnttion of 6 higher-order programming utilities and. tn Boost 271 functions convenience 100 copying 57 Ej作ctive C++ 中文版,第三版288 de伽山19 4 deliberately not defining 38 factory. see factory function forw町ding 144. 160 implicitly generated 3• 37.221 disallowing 37-39 inline. declaring 135 member templatized 21 8-222 vs. non-member 104-105 non-member templates and 222-226 type conversions and 102-105. 222­ 226 non-member non-缸end. vs member 98-102 non-叽此ual. meaning 168 return values. modi刷ng 21 signatures. explicit interfaces and 201 static ctors and dtors and 52 virtual. see virtual functions function-style casts 116 G Gamma. Erich xvii Geller. Alan 皿x generalized assignment 220 generalized copy constructors 219 generative programming 237 generic programming support. in Boost 271 get. smart pointers and 70 goddess. see Urbano. Nancy L. goto. delete and 62 Graph library. 位1 Boost 270 grep. casts and 117 guar缸ltees. exception safety 128-129 Gutnik. Gene 刻X H Handle classes 144-145 handles 125 dangling 126 encapsulation and 124 operator[] 缸ld 126 returr由19 123-126 has-a relationship 184 hash tables. inτRl266 Hastings. Battle of 150 Haugland. Solveig 皿 head scratching. avoiding 95 header files. see headers Ej知ctive C++中文版,第三版 索引 headers for declarations vs. for definitions 1钊 inline fi且nctions and 135 n缸nespaces and 100 of C++ standard library 101 templates and 136 usage. in this book 3 hello world. template metaprogramming and 235 Helm. Richard xvii Henney. Kevlin 到X Hicks. Cory 刻x hiding names. see name hiding higher-order programming and function 01功ect utilities. in Boost 271 highligh 恤恕. in this book 5 E identity test 55 if.. .else for types 230 #ifdef 17 #ifndef 17 implementation-dependent behavior. warnings anq 263 implementations decoupling仕om interfaces 165 default. danger of 163-167 inherttance of 161-169 ofderived class constructors and destructors 137 ofInterface classes 147 references 89 std::何,ax 135 std::swap 106 implicit inline request 135 implicit interfaces 199-203 implic让 type conversions vs. explicit 70­ 72 implicitly generated functions 34-37.221 disallowing 37-39 #include directives 17 compilation dependencies and 140 incompatib 出"白. with built-in types 80 incorrect code and efficiency 90 infinite loop. 缸1 operator new 253 缸由erttance accidental 165-166 combining with templates 243-245 common features and 164 intuition and 151-155 mathematics and 155 mtxin-style 244 name hiding and 156-161 of implementation 161-169 of interface 161-169索引 。 f Interface vs. implementation 161-169 operator new and 253-254 penguins and birds and 151-153 private 187-192 protected 151 public 150-155 rectangles and squares and 153-155 redefining non-virtual functions and 178-180 scopes and 156 sharing features and 164 inheritance, multiple 192-198 ambiguity 组ld 192 combining public and private 197 deadly diamond 193 inheritance, private 214 combining with public 197 eliminating 189 for redefining virtual functions 197 meaning 187 vs. composition 188 Inheritance.public combining with private 197 is-a relationship and 150-155 meaning of 150 name hiding and 159 virtuallnheritance and 194 inheritance, virtual194 init function 60 initialization 4. 26-27 assignment vs. 6 built-In types 26-27 const members 29 const static members 14 defauli, unintended 59 In-class, ofstatic const integral members 14 local static objects 31 non-local static 0均 ects 30 objects 26-33 reference members 29 static members 242 virtual base classes and 194 vs. assignment 27-29. 114 W时1 vs. without arguments 114 Initialization order class members 29 Importance of 31 non-local statics 29-33 inline functions see also In1InIng address of 136 as request to compiler 135 debuggers and 139 declaring 135 headers and 135 optimizing compilers and 134 recursion and 136 vs. #define 16-17 289 vs. macros, efficiency 缸ld 16 in!汩 ing 134---139 constructors/destructors and 137-138 dynamic Ii!虫ing and 139 Handle classes and 148 inheritance and 137-138 Interface classes and 148 library design and 138 recompiling and 139 relinking and 139 suggested strategy for 139 templates and 136 time of 135 virtual 缸nctions and 136 input iterators 227 inpuUterator_tag 228 input一iteratoctag 230 insomnia 150 instructions, reordering by compilers 76 Integral 可pes 14 Interface classes 145-- 147 interfaces decoupling仕om Implementations 165 definition of 7 design considerations 78-86 explicit, signatures and 201 implic抗 199-203 expressions and 201 inheritance of 161-169 newtypes 缸ld 79-80 separating 仕om implementations 140 template parameters and 199-203 undeclared 85 Inter-language support, in Boost 272 internationalization, library support for 264 invalid array Index, undefined behavior and7 Invariants NVI and 171 over specialization 168 144 is-a relationship 150-155 is-lmplemented- In-te口ns-of 184-186. 187 istrean 、 iterators 227 Iterator categories 227-228 iterator_category 229 iterators as handles 125 iterators, vs. consciterators 18 J Jagdhar, Emily xi芷 Janert, Philipp 对X Java 7.43.76.81. 100. 116. 118, 142, 145, 190, 194 Effective C++ 中文版,第三版290 Johnson. Ralph xvii Johnson. Tim xviII ,足X Josuttls. Nicolai M, xviII K Kaelbllng. Mike xviII Kakulapatl. Gunavardhan 血 Kalenkovlch, Eugene 对X Kennedy. Glenn xix Kernighan. Brian xviII,刻X Kimura. Junichi xviII Klrman. Jak xviII KI口nse , Andrew XIX Knox, TImo由ly xviII ,对X Koenig lookup 110 Kourounls, Drosos 足X Kreuzer. Gerhard 对X L Laeuchll, Jesse 刻X Lambda library, in Boost 271 Langer, Angellka 刻X languages, other. compatibility WI世1 42 Lanzet钮. Michael XIX late binding 180 layering, see composition layouts, o战jects vs. arrays 73 Lea. Doug xviII leaks. exception-safe code and 127 Leary-Coutu, Chanda xx Lee. Sam XIX legacy code. exception-safety缸ld 133 Lejter, Moises xviII, xx lemur, ring-tailed 196 Lewandowski. Scott xviII Ihs, as parameter name 8 Lj, Greg 刻X link-time errors 39. 44 link-time inllning 135 list 186 local static objects definition of 30 Initialization of 31 locales 264 locks, RAIl and 66-68 logic_error class 113 logically const member functions 22-23 M mailing list for Scott Meyers XVI Effective C++ 中文版,第二版 索引 maintenance common base classes and 164 delete and 62 managing resources. see resource man- agement Manis. Vtncent 刻X Marin, Alex 刻x math and numerics utilities. 让1 Boost 271 mathematical functions. In TRI 267 mathematics, Inheritance and 155 matrix operations. optimizing 237 Matthews, Leon XIX max. std. Implementation of 135 Meadowbrooke. Chrysta XIX meantng ofclasses Without virtual functions 41 of composition 184 of non-virtual functions 168 of pass-by-value 6 。 f private Inheritance 187 ofpubllclnherltance 150 。f pure virtual functions 162 of references 91 of simple virtual functions 163 measuring encapsulation 99 Meehan. Jim XIX member data, see data members member function templates 218--222 member functions bitwise const 21-22 common design errors 168-- 169 const 19-25 duplication and 23-25 encapsulation and 99 implicitly generated 3牛37 , 221 disalloWing 37-39 logically const 22-23 private 38 protected 166 vs, non-member functions 10• 105 vs, non-member non-friends 98-- 102 member tnitlallzatlon for const static tntegral members 14 lists 28--29 vs, assignment 28--29 order 29 memory allocation arrays and 254--255 error handling for 240-246 memory leal岱. new expressions and 256 memory management functions. replacing 247-252 multithreading and 239, 253 utilities, in Boost 272 metaprogrammlng. see template metapro­ grammtng索引 Meyers. Scott mailing list for XVI web site for XVI mf. as Identifier 9 Michaels. Laura XVIII Mickelsen. Denise xx 刚刚刚 zing compilation dependencies 140-148. 190 Mlttal. Nlshant 到X mixed-mode arithmetic 103. 104. 222-226 m1x1n-style 缸由erttance 244 modeling Is-Implemented-In-terms- of 184-186 mod均1ng function return values 21 Monty 町由on. allusion to 91 M∞reo V,缸lessa xx More Effective C++ 273.273-274 compared to 尾{fee蚀Je C++ 273 contents of 273-274 More EXceptional C++ x时 I Mora窟.Hal 刻X MPL library. In Boost 270.271 multiparadigm programming language. C++ as 11 multiplelnherttance.seelnherttance multithreading memory management routines and 239.253 non-canst static objects and 32 treatment In由is book 9 mutable 22-23 mutexes. RAIl and 66-毛8 N Nagler. Ertc 血 NahU. Julie xx namehiding Inherttance and 156-161 operators new/delete and 259--261 using declarations and 159 namelookup this-> and 210.214 using declarations and 211 n缸ne shadowing. see name hiding names accessing In templatlzed bases 207-212 available In both C and C++ 3 dependent 204 hidden by dertved classes 263 nested. dependent 204 non-dependent 204 n缸nespaces 110 headers and 100 namespace pollution In a class 166 Nancy. see Urbano. Nancy L. 291 Nauroth. Chrts 刻X nested dependent names 204 nested dependent守pe names. typename and 205 new see also operator new expressions. memory leaks and 256 fa口ns of 73-75 operator new and 73 relationship to cons甘uctors 73 smart pointers and 7 5-77 new types. Interface design and 79-80 new-handler 240-247 definition of 240 deinstalling 241 Identi命姐g 253 new-handling functions. behavior of 241 new-style casts 117 noncopyable base class. In Boost 39 non-dependent names 204 non-local static 0均ects. initialization 。f 30 non-member functions member functions vs. 104-105 templates and 222-226 守pe conversions and 102-105. 222-226 non-member non-归end functions 96-102 non-type p缸'ameters 213 non-virtual functions 176-180 static binding of 178 Interface idiom. see NVI nothrow guarantee. 由e 129 no吐lrow new 246 null po缸lter deleting 255 dereferenclng 6 set new handler and 241 NVI 170-171. 183 o object-ortented C++. as sublanguage of C++ 12 object-ortented p血ciples. encapsulation and 99 objects alignment of 249--250 clustering 251 compilation dependencies and 143 copying all parts 57--60 defining 4 definitions. postponing 113-116 handles to internals of 123-126 initlalization. with vs. without 缸'guments 114 layout vs. array layout 73 Effective C++ 中文版 F 第三版292 multiple addresses for 118 partial copies of 58 placing In shared memory 251 resource management and 61-66 returning. vs. references 90-94 size. pass-by-value and 89 sizes. determining 141 vs. variables 3 Oldham. Jeffrey D. 对x old-style casts 117 operations. reordering by compilers 76 operator delete 84 see also delete behavior of 255 efficiency of 248 name hiding and 259-261 non-member. pseudocode for 255 placement 256-261 replacing 247-252 standard forms of 260 vi此ual destructors and 255 operator delete[] 84. 255 operator new 84 see also new arrays and 254-255 bad_alloc and 246. 252 behavior of 252-255 efficiency of 248 Infinite loop wi吐lin 253 Inheritance and 253-254 member. and "wrongly sized" requests 254 name hiding and 259-261 new-handling functions and 241 non-member. pseudocode for 252 out-oιmemoryconditionsand 240-241, 252-253 placement 256-261 replacing 247-252 returning 0 and 246 standard forms of 260 std::bad_alloc and 246. 252 operator new[] 84. 254-255 operatorO (function call operator) 6 operator= const members and 36-37 default Implementation 35 Implicit generation 34 reference members and 36-37 return value of 52-53 self-assignment and 53-57 when not Implicitly generated 36-37 operator[] 126 overloading on const 19-20 return type of 21 optimization by compilers 94 Effective C++ 中文版,第二版 索引 during compilation 134 Inline functions and 134 order Initialization of non-local statics 29-33 member Initialization 29 ostream_iterators 227 other languages. compatibility wi世142 output Iterators 227 outpuCiterator_tag 228 overloading as if...else for types 230 on const 19-20 std::swap 109 overrides ofvirtuals. preventing 189 ownership transfer 68 P Pal, Balog XIX parameters see also pass-by-value, pass-by-refer ence, passing small objects default 180-183 evaluation order 76 non-type. for templates 213 type conver百Ions and, see type conver­ slons Pareto PrInciple, see 80-20 rule parsing problems. nested dependent names and 204 partial copies 58 pa此lal specialization function templates 109 std::swap 108 parts.ofo 问jects. cop泸ng all 57毛O pass-by-reference. efficiency and 87 pass-by-reference-to-const. vs pass-by- value 86-90 pass-by-value copy constructor and 6 efficiency of 8ι87 meanlngof 6 。均 ect size and 89 vs. pass-by-reference-to-const 86-90 patterns see design patte口IS Pedersen, Roger E 皿x penguins and birds 151-153 performance. see efficiency Persephone lx, xx, 36 pesslmlzatlon 93 physical constness. see const. bitwise plmplidiom definition of 106 exception-safe code and 131索引 placement delete. see operator delete placement new. see operator new Plato 87 pointer arithmetic and undefined behavior 119 pointers see also smart pointers as handles 125 bitwise const member functions and 21 compilation dependencies and 143 const 17 in headers 14 null. dereferencing 6 template par缸neters and 217 to single vs. multiple objects. and delete 73 polymorphic base classes. destructors and4ι44 polymorphism 199-201 compile-time 201 runtime 200 PoollibraIγ. in Boost 250. 251 postponing variable definlUons 113-116 ,Pra sert刨出. Chuti 皿 preconditions. NVI and 171 pregnancy. exception-safe code and 133 private data members. why 94-98 private inheritance. see inheritance private member functions 38 private virtual functions 171 properties 97 protected data members 97 inheritance. see inheritance member functions 166 members. encapsulation of 97 public inheritance. see inheritance pun、 really bad 152 pure virtual destructors defining 43 implementing 43 pure virtual functions 43 de缸咀ng 162. 166-167 meaning 162 R Rabbani. Danny 对芷 Rabinowitz. Marty xx RAIl 66. 70. 243 classes 72 copying behavior 缸ld 6ι69 encapsulation and 72 mutexes and 66-68 random access iterators 227 293 random number generation. in TRI 267 random_access_iterator_tag 228 RCSP. see smart pointers reading uninitialized values 26 rectangles and squares 153-155 recursive functions. inUning and 136 redefining inherited non-virtual functions 178卢 180 Reed. Kathy 皿 Reeves. Jack XIX references as handles 125 compilation dependencies and 143 functions retu口ling 31 implementation 89 meaIling 91 members 、 initialization of 29 returning 90-94 to static object‘ as function return value 92-94 register usage, objects and 89 regular expressions, in TRl 266 reinterpreCcast 117.249 see also casting relationships has-a 184 is-a 150-155 is-implemented-in-terms-of 184-186. 187 reordering operations. by compilers 76 replacing definitions with declarations 143 replacing new/delete 247-252 replication. see duplication reporting, bugs in this book xvi Resource Acquisition Is Initialization, see RAIl resource leaks. exception-safe code and 127 resource management see also RAIl copying behavior and 66-69 objects and 61 -&5 raw resource access and 69-73 resources, managing 0均ects and 69-73 return by reference 90-94 return types const 18 objects vs. references 90-94 of operator[] 21 return value of operator= 52-53 returning handles 123-126 reuse. see code reuse revenge. compilers taking 58 rhs. 且s parameter name 8 El知ctive C++ 中文版,第三版294 Roze. Mike 刻X rule of80-20 139. 168 runtime errors 152 inl1ning 135 polymorphism 2∞ s Saks. Dan xviII Santos. Eugene. Jr. xviII Satch 36 Satν巾口n vii Scherpelz. Jeff 刻X Schlrrlpa. Steve 到X Schober. Hendrtk xviII. 刻X Schroeder. Sandra xx scoped_array 65, 216, 272 scopes. inheritance and 156 sealed classes. in C# 43 sealed methods. In C# 190 second edition. see 2nd edition self-assignment. operator= and 53--57 set 185 se飞new_handler class-specific. Implementing 243--245 using 24(}-246 seCunexpected function 129 shadowing. names. see name shadowing Shakespeare. William 156 shared memory. placing objects In 251 shared_array 65 shared_ptr Implementation in Boost. costs 83 sh部也19 code. see duplication. avoiding sharing common features 164 Shewchuk. John XVIII side effects. exception safety and 132 signatures definition of 3 explicit interfaces and 201 simple virtual functions. meaning of 163 Singh. Slddh缸甘la 血 Singleton pattern 31 size_t 3 sizeof 253. 254 empty classes 缸ld 190 仕'eestanding classes and 254 sizes of 仕'Cestandingclasses 254 of objects 141 sleeping pills 150 slist 227 Smallberg. David xviII. 到X EJ.拷ctive C++中文版,第三版 索引 Smalltalk 142 smart pointers 63.64.70, 81 , 121. 146.237 see also std::auto_ptr and trl::shared_ptr get and 70 in Boost 65, 272 web page for xv姐 姐 τ'R l 265 newed objects and 75--77 type conversions and 218町220 Socrates 87 Some Must Watch While Some Must Sleep 150 Somers. Jeff 刻x specialization invariants over 168 partial. of std::swap 108 total. of std::swap 107 , 108 specification. see interfaces squ缸'es and rectangles 153-- 155 standard exception hierarchy 264 standard fOffi1S ofoperator new/ delete 260 standard library. see C++ standard library. C standard library standard template library. see STI.. Stasko. John xviII statements using new. smart pointers and 75--77 static binding of default parameters 182 ofnon-virtual functions 178 objects. returning references to 92-94 type. definition of 180 static functions. ctors and dtors and 52 static members const member functions and 21 definition 242 initialization 242 static 0均ects definition of 30 multithreading and 32 static_cast 25.82.117, 119, 249 see also casting std n缸nespace. specializing templates in 107 std::auto_ptr 63--65, 70 conversion to trl::shared_ptr and 220 delete []缸ld 65 pass by const and 220 std::auto_ptr. deleter support and 68 std::char traits 232 std::iterator_traits. pointers and 230 std::list 186 std::max. Implementation of 135 std::numeric limits 232索引 std::set 185 std::size t 3 std::swap see also swap implementation of 106 overloading 1ω part坦1 specialization of 108 total specialization of 107. 108 std::trl. see TRI stepping through functions. inlining and 139 STL allocators 240 as sublanguage of C++ 12 containers. swap and 108 definition of 6 iterator categories in 227-228 Strategy pattern 171-177 string and text utilities. in Boost 271 strong guarantee. the 128 StroustTup. BJarne xviI, xviII StroustTup. Nicholas XIX Sutter. Herb xvii. xviII. 刻x swallowing exceptions 46 swap 106-- 112 see also std::swap calling 110 exceptions and 112 sτL containers and 108 when to write 111 symbols. available in both C and C++ 3 T template C++. as sublanguage of C++ 12 template metaprogramming 233-238 efficiency and 233 hello world in 235 patte口1 implementations and 237 support in Boost 271 support in TRI 267 Template Method pattern 170 templates code bloat. avoiding in 212-217 combining with inheritance 243-245 defining 4 errors. whendetected 212 expression 237 headers and 136 in std. specializing 107 inlining and 136 instantiation of 222 member functions 218-222 names in base classes and 207-212 non-type parameters 213 parameters. omitting 224 295 pointer type parameters and 217 shorthand for 224 specializations 229. 235 partial 109. 230 total 107. 209 type conversions and 222-226 type deduction for 223 temporary objects. eliminated by compilers 94 terminology. used in this book 3-8 testing and correctness. Boost support for 272 text and string utilities. in Boost 271 third edition. see 3rd edition this->. to force base class lookup 210.214 threading. see multithreading Tilly. Barbara xviii TMP. see template metaprogramming Tondo. Clovis xviII Topic. Michael 对X total class template specialization 209 total specialization of std::swap 107. 108 total template specializations 107 TRl 9. 264-267 array component 267 bind component 266 Boost and 9-10.268.269 boost as synonym for std::trl 268 Cgg compatibility component 267 function component 265 hash tables component 266 math functions component 267 mem3n component 267 random numbers component 267 reference_wrapper component 267 regular expression component 266 resulCof component 267 smart pointers component 265 support for τMP 267 tuples component 266 type traits component 267 URL for information on 268 trl::array 267 trl::bind 175. 266 trl ::function 17J.:. 175. 265 trl ::mem fn 267 trl::reference_wrapper 267 trl::result of 267 trl ::shared_ptr 53. 64-65. 70. 71>-77 construction from other smart pointers and 220 cross-DLL problem and 82 delete [] and 65 deleter support in 68.81-83 member template ctors in 220-221 trl ::tuple 266 Ej知ctive C++ 中文版,第三版296 trl::unordered_map 43. 266 trl::unordered_multimap 266 trl::unordered multiset 266 trl::unordered set 266 trl::weak_ptr 265 tra1ts classes 226-232 transfer. ownership 68 translation unit. definition of 30 nux. Antoine xviII Tsao. Mike 刻X tuples. in TRl 266 type conversions 85. 104 explicit ctors and 5 implicit 104 implicit vs. expllctt 7D-72 non-member functions and 102一 105. 222-226 private inheritance and 187 smart pointers and 2 H弘220 templates and 222-226 type deduction. for templates 223 可pe design 78-86 type tra1ts. in 11王 1 267 typedef. typename and 206-207 typedefs. new/ delete 缸ld 75 typeid 50. 230. 234. 235 守pelists 271 typename 203-207 compiler variations and 207 typedef and 206-207 vs. class 203 types built-in. initialization 26-27 compatible. accepting all 218-222 if...else for 230 integral. definition of 14 tr创ts classes and 226-232 U undeclared interface 85 undefined behavior advance and 231 array deletion and 73 casting + po缸lter arithmetic and 119 definition of 6 destroyed 0均ects and 91 exceptions and 45 initialization order and 30 invalid array index and 7 multiple deletes and 63.247 null pointers and 6 。均ect deletion and 4 1, 43. 74 uninitialized values and 26 undefined values of members before con­ struction and after destruction 50 £1知ctive C++中文版,第三版 索引 unexpected function 129 uninitialized data members. virtual functions and 49 values. reading 26 unnecessary ot鸟Jects. avoiding 115 unused 0均ects cost of 113 exceptions and 114 Urbano. Nancy L. vII. x时lI .xx see also goddess URLs Boost 10. 269. 272 Boost smart pointers x吐i 乓[fective C++ errata list XVI Effective C++ 四 1 Info. Page 268 Greg Comeau's C/C++ FAQ xviII Scott Meyers' ma1ling list XVI Scott Meyer毡'web site x叫 this book's errata list XVI usage statistics. memory management and 248 using declarations name hiding and 159 name lookup and 211 v valarray 264 value. pass by. see pass-by-value V缸1 Wyk. Chris x叫II. xix Vandevoorde. David xviII variable. vs. object 3 variables definitions. postponing 11 3-116 vector template 75 Victana. Paco 刻x virtual base classes 193 virtual constructors 146. 147 virtual destructors operator delete and 255 polymorphic base classes and 4D-44 virtual functions alternatives to 169-177 ctors/dtors and 48-52 default implementations and 163-167 default parameters and 18D-183 dynamic binding of 179 efficiency and 168 expl1ct base class qua1ification and 211 implementation 42 inlining and 136 language interoperab1Uty and 42 日leaning ofnone in class 41 preventing overrides 189 private 171 pure. see pure virtual functions simple. meaning of 163索引 uninitlalized data members and 49 叫rtual 位让lerttance. see 1J由erttance 观rtual table 42 virtual table po1Jlter 42 Vlissides. John XVIi vptr 42 vtbl42 w Walt. John xx warn1Jlgs. 仕om compiler 262-263 calls to virtuals and 50 1nl1n1ng and 136 pa此ial copies and 58 web sites. see URLs Widget class. as used 1Jl this book 8 Wiegers. Karl 血 Wilson. Matthew XIX Wizard ofOz. allusion to 1 日 x XP. allusion to 225 XYZ Airlines 163 z zabluda. Oleg XVIii Zolman, Leor x吼ii. XIX 297 Effective C++ 中文版 F 第三版

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档

相关文档