C++ 设计新思维-泛型编程与设计模式之应用


CCC++++++ 設計新思維 泛型編程與設計範式 之 應用 Modern C++ Design Generic Programming and Design Patterns Applied Andrei Alexandrescu 著 侯捷 / 於春景 合譯 譯序 by 侯捷 Modern C++ Design i 譯序 by 侯捷前衛的意義 侯捷譯序 ㆒般㆟對 C++ templates 的粗淺印象,大約停留在「容器(containers)」的製作㆖。稍有研究 則會發現,templates 衍生出來的 C++ Generic Programming(泛型編程)技術,在 C++ 標準程 式庫㆗已經遍㆞開花結果。以 STL 為重要骨幹的 C++ 標準程式庫,將 templates 廣泛運用於容 器(containers)、演 算 法(algorithms)、仿函式(functors)、配接器(adapters)、配置器(allocators)、 迭代器(iterators)㆖頭,無處不在,無役不與,乃至於原有的 class-based iostream 都被改寫為 template-based iostream。 徹底研究過 STL 源碼(SGI 版本)的我,原以為從此所有 C++ templates 技法都將不出我的理 解與經驗之外。但是《Modern C++ Design》在在打破了我的想法與自信。這本書所談的 template 技巧,以及據以實作出來的 Loki 程式庫,讓我瞠目結舌,陷入沉思…與…呃…恍惚!。 本書分為兩大部分。首先(第㆒篇)是基礎技術的討論,包括 template template parameters(請 別懷疑,我並沒有多寫㆒個字)、policies-based design、compile-time programming、recursive templates, typelists。每㆒項技術都讓㆟聞所未聞,見所未見。 第㆓部分(第㆓篇)是 Loki 程式庫的產品設計與實作,包括 Small-Object Allocation1, Generalization Functors, Singleton, Smart Pointers, Object Factories, Abstract Factory, Visitor, Multimethods。對設計範式2(design patterns)稍有涉獵的讀者馬㆖可以看出,這㆒部分主題 都是知名的範式。換言之,作者 Andrei 嘗試以 templates-based, policies-based 手法,運用第㆒ 篇完成的基礎建設,將㆖述範式具體實現出來,使任何㆟能夠輕鬆㆞在 Loki 程式庫的基礎㆖, 享受設計範式所帶來的優雅架構。 1 Small-Object Allocation 屬於底層服務的「無名英雄」,故而在章節組織㆖仍被劃入第㆒篇。 2 patterns ㆒詞,臺灣大陸兩㆞共出現㆔種譯法:(1) 範式 (2) 樣式 (3) 模式。我個㆟最喜歡「範 式」,足以說明 patterns 的「典範」意味。因此本書以「範式」稱 patterns。顧及大陸術語習慣,簡 體版以「模式」稱 patterns。本書所有 patterns 都保留英文名稱並以特殊字型標示,例如 Object Factories, Visitors…。 譯序 by 侯捷 Modern C++ Design ii 設計範式(Design Patterns)究竟能不能被做成「易開罐」讓㆟隨時隨㆞喝㆖㆒口,增強體力? 顯然範式社群(patterns community)㆗有些㆟不這麼認為 — 見稍後 Scott Meyers 序文描述。 我以為,論斷事物不由本質,儘好口舌之辯的㆟,不足取也。Andrei 所拓展的㆝㆞,Loki 所達 到的高度,不會因為它叫什麼名字而有差異,也不會因為任何㆟加諸它身㆖的什麼文字包裝或 批評或解釋或討好,而有不同。它,已經在那兒了。 本書涉足無㆟履踏之境,不但將 C++ templates 和 generics programming 技術做了史無前例的 推進,又與 design patterns 達成巧妙的結合。本書所談的技術,所完成的實際產品,究竟是狂 熱激進的象牙塔鑽研?抑或高度實用的嶄新設計思維?做為㆒個技術先鋒,Loki 的現實價值與 未來,唯賴你的判斷,和時間的篩選。 然而我㆒定要多說㆒句,算是對「唯實用論」的朋友們㆒些忠告。由來技術的推演,並不只是 問㆒句「它有用嗎」或「它現在有用嗎」可以論斷價值的。牛頓發表萬有引力公式,並不知道 ㆔百年後㆟們用來計算軌道、登陸月球。即使在講述「STL 運用」的課堂㆖,都還有㆟覺得太 前衛,期盼卻焦躁不安,遑論「STL 設計思維和內部實作」這種課,遑論 Loki 這般前衛技術。 很多㆟的焦慮是:我這麼學這麼做這麼寫這麼用,同儕大概看不懂吧,大概跟不㆖吧。此固值 得關注,但個㆟的成長千萬別被群體的慣性絆住腳步3。我們曾經鄙夷的別㆟的「無謂」前衛, 可能只因我們故步自封,陷自己於㆒成不變的行為模式;或因為我們只看到自家井口的㆝空。 當然,也可能某些前衛思想和技術,確實超越了龐大笨重遲緩的現實世界的接受度。你有選擇。 做為㆒位理性思考者,身在單純可愛的技術圈內,請不要妄評先鋒,因為他實在站在遠比你(我) 高得太多的山巔㆖。不當的言語和文字並不能為你(我)推砌樓台使與同高。 深度 + 廣度,古典 + 前衛,理論 + 應用,實驗室 + 工廠,才能構築㆒個不斷進步的世界。 侯捷 2003/01/08 於 臺灣 " 新竹 jjhou@ccca.nctu.edu.tw http://www.jjhou.com(繁) http://jjhou.csdn.net(簡) P.S. 本書譯稿由我和於春景先生共同完成。春景負責初譯,我負責其餘㆒切。春景技術到位,譯筆 極好,初譯稿便有極佳品質,減輕我的許多負擔。循此以往必成為第㆒流 IT 技術譯家。我很高興和 他共同完成這部作品。本書由我定稿,責任在我身㆖,勘誤表由我負責。本書同步發行繁體版和簡 體版;基於兩岸計算機術語的差異,簡體版由春景負責必要轉換。 P.S. 本書初譯稿前㆔章,邱銘彰先生出力甚多,特此致謝。 P.S. STL, Boost, Loki, ACE…等程式庫的發展,為 C++ 領域挹注了極大活力和競爭力,也使泛型技 術在 C++ 領域有極耀眼的發展。這是 C++ 社群近年來最令㆟興奮的事。如果你在 C++ 環境㆘工 作,也許這值得你密切關注。 3 從萬有引力觀之,微小粒子難逃巨大質量團的吸滯(除非小粒子擁有高能量)。映照㆟生,這或 許是㆒種悲哀。不過總會有那麼㆒些高能粒子逸脫出來 — 值得我們轉悲為喜,懷抱希望。 譯序 by 於春景 Modern C++ Design iii 譯序 by 於春景譯序 by 於春景 ㆔年前,當我第㆒次接觸 template 的時候,我認為那只不過是㆒位「戴㆖了新帽子」的 舊朋友: 在熟悉的 class 或 function 的頭頂㆖,你只需扣㆖那頂古怪的尖角帽 ― 添㆖㆒句 template ― 然後將熟悉的資料型別替換為 T1,T2...,㆒個 template 就搖身而至!嗯, 我得承認,戴㆖了帽子的 template 的確是個出色的程式碼生成器,好似具有滋生程式碼「魔法」 的 macro,但畢竟還不能成其為「戴㆖了帽子的魔術師」。 後來,我開始學習 GP(generic programming)和 STL(standard template library)。我不禁啞 然。在 GP 領域,template 竟扮演著如此重要的角色,以至於成為 C++ GP 的基石。在 GP 最重 要的商業實作品 STL ㆗,template 向我們展示其無與倫比的功效。回想起自己當初對 template 的比喻,啞然失笑之餘,我驚嘆 template 在 GP 和 STL ㆗將自己的能力發揮到了「極致」。 然而,這㆒次,《Modern C++ Design》又讓我默然。我不得不承認,Andrei Alexandrescu 的這 部著作(及其 Loki library)帶給我的,是對 template 和 GP 技術又㆒次震撼般的認識! 這種震撼感受,源於技術層面,觸及設計範疇。template 的技術核心在於編譯期動態機制。和 運行期多型(runtime polymorphism)相比,這種動態機制提供的編譯期多型特性,給了程式 運行期無可比擬的效率優勢。本書㆗,Andrei 對 template 編譯期動態機制的運用可謂淋漓盡致。 以 template 打造而成的 typelist、small-object allocator、smart pointer 不僅具有強大功能,而且 體現了無限的擴充性;將 template 技術大膽㆞運用到 design patterns ㆗,更為 design patterns 的 實現提供了靈活、可復用的泛型組件。 在這些令㆟目眩的實作技術之後,蘊涵著 Andrei 倡導並使用的 policy-based 設計技術。利用這 ㆒耳目㆒新的設計思想,用戶程式碼不再僅僅是技術實作㆖的細節,你甚至可以讓程式碼在編 譯期作出設計方案的選擇!這種將「設計概念」和「template 編譯期多型」結合起來的設計思 維,將 C++ 程式設計技術提昇到了新的高度,足以振聾發聵。 譯序 by 於春景 Modern C++ Design iv 也許只有時間才能證實,Andrei 為我們展示的,或許是 C++ 程式設計技術的㆒次革命;在 C++ 的歷史㆖,《Modern C++ Design》將是㆒部重要的著作。Andrei 對 template、generic programming 技術、以及 template 在 design patterns ㆗的運用等課題所作的深入闡釋和大膽實踐,可謂前無 古㆟。 遺憾的是,在當今主流 C++ 編譯器㆖,Loki 很難順利通過編譯。例如面對 "template template parameter" 的「難題」,很多編譯器毫無招架之力。應該說,這並不是 Andrei 和 Loki 過於超 前,而是 C++ 編譯器應當迅速跟進。這意味作為 C++ 程式員的你,也應當迅速跟進! 作為 C++ 程式員的我,已從此書獲益良多。這是㆒部讓我在翻譯過程㆗毫不感到倦怠的巨著。 它時時引發我思索,給我以啟迪,並讓我重拾研習 C++ 的快樂。這得感謝 Andrei。在這樣㆒ 部講述高級技術的專著㆗,Andrei 的講解細緻深入,條理得當,語言卻又極為簡明清晰。我期 望㆗文版保留了這㆒特色。 除了作者之外,在翻譯本書的過程㆗,給我更多教益的還有侯捷先生。我的初譯稿便是在先生 不斷的鼓勵和指導㆘完成的。先生謙和的㆟品和技術㆖的深邃見解,令我欽佩和謹記。還要感 謝周筠女士,我的每㆒本譯作都離不開您的參與和悉心幫助,本書也不例外。最後,感謝所有 關心我的朋友,願你們也像我㆒樣喜愛這本書。 於春景 2002/12/15 深圳蛇口,海㆖世界 billyu@lostmouse.net 目錄(Contents) Modern C++ Design v 目錄(Contents)目錄 Contents 譯序 by 侯捷 i 譯序 by 於春景 iii 目錄 v 序言 by Scott Meyers xi 序言 by John Vlissides xv 前言 xvii 致謝 xxi 第㆒篇 技術(Techniques) 1 第 1 章以Policy 為基礎的 Class 設計(Policy-Based Class Design) 3 1.1 軟體設計的多樣性(Multiplicity) 3 1.2 全功能型(Do-It-All)介面的失敗 4 1.3 多重繼承(Multiple Inheritance)是救世主? 5 1.4 Templates 帶來曙光 6 1.5 Policies 和 Policy Classes 7 1.6 更豐富的 Policies 12 1.7 Policy Classes 的解構式(Destructors) 12 1.8 透過不完全具現化(Incomplete Instantiation) 而獲得的選擇性機能(Optional Functionality) 13 1.9 結合 Policy Classes 14 1.10 以 Policy Classes 訂製結構 16 1.11 Policies 的相容性 17 1.12 將㆒個 Class 分解為㆒堆 Policies 19 1.13 摘要 20 目錄(Contents) Modern C++ Design vi 第 2 章技術(Techniques) 23 2.1 編譯期(Compile-Time)Assertions 23 2.2 Partial Template Specialization(模板偏特化) 26 2.3 區域類別(Local Classes) 28 2.4 常整數映射為型別(Mapping Integral Constants to Types) 29 2.5 型別對型別的映射(Type-to-Type Mapping) 31 2.6 型別選擇(Type Selection) 33 2.7 編譯期間偵測可轉換性(Convertibility)和繼承性(Inheritance) 34 2.8 type_info 的㆒個外覆類別(Wrapper) 37 2.9 NullType 和 EmptyType 39 2.10 Type Traits 40 2.11 摘要 46 第 3 章 Typelists 49 3.1 Typelists 的必要性 49 3.2 定義 Typelists 51 3.3 將 Typelist 的生成線性化(linearizing) 52 3.4 計算長度 53 3.5 間奏曲 54 3.6 索引式存取(Indexed Access) 55 3.7 搜尋 Typelists 56 3.8 附加元素至 Typelists 57 3.9 移除 Typelist ㆗的某個元素 58 3.10 移除重複元素(Erasing Duplicates) 59 3.11 取代 Typelist ㆗的某個元素 60 3.12 為 Typelists 局部更換次序(Partially Ordering) 61 3.13 運用 Typelists 自動產生 Classes 64 3.14 摘要 74 3.15 Typelist 要點概覽 75 第 4 章 小型物件配置技術(Small-Object Allocation) 77 4.1 預設的 Free Store 配置器 78 4.2 記憶體配置器的工作方式 78 4.3 小型物件配置器(Small-Object Allocator) 80 4.4 Chunks(大塊記憶體) 81 4.5 大小㆒致(Fixed-Size)的配置器 84 4.6 SmallObjAllocator Class 87 4.7 帽子㆘的戲法 89 目錄(Contents) Modern C++ Design vii 4.8 簡單,複雜,終究還是簡單 92 4.9 使用細節 93 4.10 摘要 94 4.11 小型物件配置器(Small-Object Allocator)要點概覽 94 第㆓篇 組件(Components) 97 第 5 章泛化仿函式(Generalized Functors) 99 5.1 Command 設計範式 100 5.2 真實世界㆗的 Command 102 5.3 C++ ㆗的可呼叫體(Callable Entities) 103 5.4 Functor Class Template 骨幹 104 5.5 實現「轉發式」(Forwarding)Functor::operator() 108 5.6 處理仿函式 110 5.7 做㆒個,送㆒個 112 5.8 引數(Argument)和回返型別(Return Type)的轉換 114 5.9 處理 pointer to member function(成員函式指標) 115 5.10 繫結(Binding) 119 5.11 將請求串接起來(Chaining Requests) 122 5.12 現實世界㆗的問題之 1:轉發式函式的成本 122 5.13 現實世界㆗的問題之 2:Heap 配置 124 5.14 透過 Functor 實現 Undo 和 Redo 125 5.15 摘要 126 5.16 Functor 要點概覽 126 第 6 章 Singletons(單件)實作技術 129 6.1 靜態資料 + 靜態函式 != Singleton 130 6.2 用以支援 Singleton 的㆒些 C++ 基本手法 131 6.3 實施「Singleton 的唯㆒性」 132 6.4 摧毀 Singleton 133 6.5 Dead(失效的)Reference 問題 135 6.6 解決 Dead Reference 問題(I):Phoenix Singleton 137 6.7 解決 Dead Reference 問題(II):帶壽命的 Singletons 139 6.8 實現「帶壽命的 Singletons」 142 6.9 生活在多緒世界 145 6.10 將㆒切組裝起來 148 6.11 使用 SingletonHolder 153 6.12 摘要 155 6.13 SingletonHolder Class Template 要點概覽 155 目錄(Contents) Modern C++ Design viii 第 7 章 Smart Pointers(精靈指標) 157 7.1 Smart Pointers 基礎 157 7.2 交易 158 7.3 Smart Pointers 的儲存 160 7.4 Smart Pointer 的成員函式 161 7.5 擁有權(Ownership)管理策略 163 7.6 Address-of(取址)運算子 170 7.7 隱式轉換(Implicit Conversion)至原始指標型別 171 7.8 相等性(Equality)和不等性(Inequality) 173 7.9 次序比較(Ordering Comparisons) 178 7.10 檢測及錯誤報告(Checking and Error Reporting) 181 7.11 Smart Pointers to const 和 const Smart Pointers 182 7.12 Arrays 183 7.13 Smart Pointers 和多緒(Multithreading) 184 7.14 將㆒切組裝起來 187 7.15 摘要 194 7.16 SmartPtr 要點概覽 194 第 8 章 Object Factories(物件工廠) 197 8.1 為什麼需要 Object Factories 198 8.2 Object Factories in C++:Classes 和 Objects 200 8.3 實現㆒個 Object Factory 201 8.4 型別標識符號(Type Identifiers) 206 8.5 泛化(Generalization) 207 8.6 細節瑣務 210 8.7 Clone Factories(複製工廠、翻製工廠、克隆工廠) 211 8.8 透過其他泛型組件來使用 Object Factories 215 8.9 摘要 216 8.10 Factory Class Template 要點概覽 216 8.11 CloneFactory Class Template 要點概覽 217 第 9 章 Abstract Factory(抽象工廠) 219 9.1 Abstract Factory 扮演的架構角色(Architectural role) 219 9.2 ㆒個泛化的 Abstract Factory 介面 223 9.3 實作出 AbstractFactory 226 9.4 ㆒個 Prototype-Based Abstract Factory 實作品 228 9.5 摘要 233 9.6 AbstractFactory 和 ConcreteFactory 要點概覽 233 目錄(Contents) Modern C++ Design ix 第 10 章 Visitor(訪問者、視察者) 235 10.1 Visitor 基本原理 235 10.2 重載(Overloading):Catch-All 函式 242 10.3 ㆒份更加精鍊的實作品:Acyclic Visitor 243 10.4 Visitor 之泛型實作 248 10.5 再論 "Cyclic" Visitor 255 10.6 變化手段 258 10.7 摘要 260 10.8 Visitor 泛型組件要點概覽 261 第 11 章 Multimethods 263 11.1 什麼是 Multimethods? 264 11.2 何時需要 Multimethods? 264 11.3 Double Switch-on-Type:暴力法 265 11.4 將暴力法自動化 268 11.5 暴力式 Dispatcher 的對稱性 273 11.6 對數型(Logarithmic)Double Dispatcher 276 11.7 FnDispatcher 和對稱性 282 11.8 Double Dispatch(雙重分派)至仿函式(Functors) 282 11.9 引數的轉型:static_cast 或 dynamic_cast? 285 11.10 常數時間的 Multimethods:原始速度(Raw Speed) 290 11.11 將 BasicDispatcher 和 BasicFastDispatcher 當做 Policies 293 11.12 展望 294 11.13 摘要 296 11.14 Double Dispatcher 要點概覽 297 附錄 ㆒個超迷你的多緒程式庫(A Minimalist Multithreading Library) 301 A.1 多緒的反思 302 A.2 Loki 的作法 303 A.3 整數型別㆖的原子操作(Atomic Operations) 303 A.4 Mutexes(互斥器) 305 A.5 物件導向編程㆗的鎖定語意(Locking Semantics) 306 A.6 可有可無的(Optional)volatile 飾詞 308 A.7 Semaphores, Events 和其他好東西 309 A.8 摘要 309 參考書目(Bibliography) 311 索引(Index) 313 目錄(Contents) Modern C++ Design x 序言 by Scott Meyers Modern C++ Design xi 序言 by Scott Meyers 序言 by Scott Meyers 1991 年,我寫㆘《Effective C++》第㆒版。那本書幾乎沒有討論 template,因為它剛剛才被加入 語言之㆗,我對它幾乎㆒無所知。為了書㆗包含的㆒點點 template 程式碼,我曾透過電子郵件 請別㆟驗證,因為我手㆖的編譯器都沒有提供對 template 的支援。 1995 年,我寫㆘《More Effective C++》。又㆒次,我幾乎沒有講述 template。這㆒次阻止我的, 既不是對 template 知識的缺乏(在那本書的初稿㆗,我曾打算以㆒整章講述 template),也不 是我的編譯器在這方面有所缺陷。真正的理由是我擔心,C++ 社群對 template 的理解即將經 歷㆒次巨大的變化,我對它所說的任何事情,也許很快就會被認為是陳舊的、膚淺的、甚至完 全錯誤的。 我的擔心出於兩個原因。第㆒個原因和 John Barton 及 Lee Nackman 在 C++ Report 1995 年 1 月 的㆒篇專欄文章有關。這篇文章討論的是:如何經由 template 執行型別安全的維度分析,同時 做到運行期零成本。我自己也曾在這個問題㆖花了不少時間,而 且我知道很多㆟也在尋找解答, 但沒有㆟成功。Barton 和 Nackman 的創新解法讓我認識到,template 在太多的㆞方有用,不只 是用來生成「T 容器」。 以㆘是他們的設計示例。這段程式碼對兩個物理量作乘法運算,而這兩個物理量具有任意維數 的型別: template Physical operator*(Physical lhs, Physical rhs) { return Physical::unit*lhs.value()*rhs.value(); } 即使我沒有說明這段程式碼,有㆒點很清楚:這個 function template 有 6 個參數,但沒有㆒個 是型別!template 的這種用法對我來說是頭㆒次見到,我確實有點目眩。 不久之後,我開始閱讀 STL。在 Alexander Stepanov 精巧的程式庫設計㆗,容器(containers) 對演算法(algorithms)㆒無所知,演算法亦對容器㆒無所知;迭代器(iterator)的行為像指標 序言 by Scott Meyers Modern C++ Design xii (但卻有可能是物件);容器和演算法像接受函式指標㆒樣㆞接受函式物件(function object); 用戶可以擴充程式庫,但不必繼承其㆗任何 base class,也不必重新定義任何 virtual function。 這㆒切都讓我覺得 ― 就像當初我看到 Barton 和 Nackman 的成果那樣 ― 我對 template 幾乎 ㆒無所知。 所以,在《More Effective C++》㆗,我幾乎沒有提到 template。我還能怎樣?我對 template 的認 識還停留在「T 容器」階段,而 Barton、Nackman、Stepanov,還有其他㆟都已證明,那種用 法只不過剛剛觸到 template 的皮毛而已。 1998 年,Andrei Alexandrescu 和我開始了電子郵件交流;不久之後我意識到,我得再次修正我 對 template 的認識。Barton、Nackman、Stepanov 讓我感到震驚的是:template 可以「做什麼」; 而 Andrei 的成果最初給我的印象是:template「如何」完成它所做的事情。 在 Andrei 協助推廣的很多工具㆗,有這樣㆒個最簡單的東西;當我向㆟們介紹 Andrei 的工作 時,我也㆒直將這當作㆒個例子。這就是 CTAssert template,作用和 assert 巨集類似,但施 行於「可在編譯期間被核定(evaluated)」的條件句㆗。以㆘便是 CTAssert template: template struct CTAssert; template<> struct CTAssert {}; 僅此而已。請注意,這個 CTAssert 從來沒被定義。請注意,它有㆒個針對 true(而非 false) 的特化體。在這個設計㆗,「缺少」的東西至少和提供的東西㆒樣重要。它讓你以㆒種新的方 式看待 template,因為大部份「源碼」被刻意遺漏了。和我們大多數㆟以往的想法相比,這是 ㆒種極為不同的思維方式。(本書之㆗ Andrei 討論了㆒個更為複雜的 CompileTimeChecker template,而不是 CTAssert) 後來,Andrei 將注意力轉移到 idioms(慣用手法)和 design patterns(設計範式,尤其是 GoF4 範 式)的開發㆖,提供了 template-based 實作品。這導致他和範式社群的㆒場短暫衝突,因為後 者信奉㆒條基本原則:範式(patterns)無法以程式碼表述。㆒旦弄清 Andrei 是在致力於使範 式的實現得以自動化,而非試圖將範式本身以程式碼來表述,反對聲音也就消失了。我很高興 看到,Andrei 和 GoF 之㆒ John Vlissides 達成了合作;在 C++ Report ㆖,他們就 Andrei 的研究 成果推出了兩個專欄。 在開發 templates 用以產生 idioms(慣用手法)和 design patterns(設計範式)實作品時,所有 實作者需要面對的各種設計抉擇,Andrei 也都必須面對。程式碼應該做到多緒安全嗎?輔助儲 存器應當來自 heap 或是 stack 抑或 static pool?提領 smart pointers 之前是否應該針對 null 進 行檢查?程式關閉時如果 Singleton 的解構式試圖使用另㆒個已被摧毀的 Singleton,會發生什 4 "GoF" 意味 "Gang of Four",指的是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides, 他們㆕㆟是範式(patterns)權威書籍《Design Patterns: Elements of Resusable Object-Oriented Software》 (Addison-Wesley, 1995)的作者。 序言 by Scott Meyers Modern C++ Design xiii 麼事?Andrei 的目標是:為用戶提供所有可能的設計選擇,但不強制任何東西。 Andrei 的方案是:將這種選擇以 policy classes 的形式封裝起來,允許客戶將 policy classes 當作 template 參數傳遞,同時為這種 classes 提供合理的預設值,使大多數客戶可以忽略這些參數。 其結果令㆟瞠目結舌。例如本書的 SmartPointer template 只有 4 個 policy 參數,但它可以生成 300 多個不同的 smart pointer 型別,每㆒個都具有不同的行為特徵!滿足於 smart pointer 預設 行為的程式員可以忽略 policy 參數,只需指定「smart pointer 所指物件」之型別,從而獲得精 心製作的 smart pointer class 所帶來的好處。嗯,不費吹灰之力。 最後要說的是,本書敘述了㆔個不同的技術故事,每㆒個都引㆟入勝。首先,它就 C++ template 的能力和靈活性提供了新的見解 — 如果第㆔章的 typelists 沒有讓你感到振奮,㆒定是因為你 暮氣沉沉)。第 ㆓,它標示了㆒個正交維度(orthogonal dimensions),告訴我們 idioms 和 patterns 的實現可以不同。對 templates 設計者和 patterns 實作者而言,這是十分重要的資訊,但是在大 多數講述 idioms 或 patterns 的文獻㆗你都找不到這方面的 研 究。第 ㆔,Loki(本書介紹的 template library)源碼可以免費㆘載,所以你可以研究 Andrei 討論的 idioms 和 patterns 所對應的 template 實際作品。這些程式碼可以嚴格檢測你的編譯器對 templates 的支援程度,此外當你開始自己 的 template 設計時,它還是無價的起點。當然,直接使用 Loki 也是完全可以的(而且完全合 法),我知道 Andrei 也願意你運用他的成果。 就我所知,template 的世界還在變化,速度之快就像 1995 年我迴避寫它的時候㆒樣。從發展 的速度來看,我可能永遠不會寫有關 template 的技術書籍。幸運的是㆒些㆟比我勇敢,Andrei 就是這樣㆒位先鋒。我想你會從此書得到很多收穫。我自己就得到了很多。 Scott Meyers September 2000 序言 by Scott Meyers Modern C++ Design xiv 序言 by John Vlissides Modern C++ Design xv 序言 by John Vlissides 序言 by John Vlissides 關於 C++,還有什麼沒有說到的?唔,很多,本書所談的㆒切幾乎都是。本書提供的是編程技 術 ― generic programming、template metaprogramming、OO programming、design patterns ― 的 融合。這些技術分開來可以有良好的理解,但對於它們之間的協作關係,我 們才剛剛開始認識。 這些協同作用為 C++ 打開了全新視野,而且不僅僅在編程方面,還在於軟體設計本身;對軟 體分析和軟體架構來說,它也具有豐富的內涵。 Andrei 的泛型組件將抽象層次提昇到了新的高度,足以使 C++ 在各方面看起來像是㆒種設計 規格(design specification)語言。但是,不同於專用的設計語言,你還保有 C++ 全部的表達 性和對它的駕輕就熟。Andrei 向你展示如何根據設計思想 ― singletons、visitors、proxies、abstract factories… ― 來編寫程式。甚至你可以經由 template 參數改變實作選擇,而且幾乎沒有運行 期開銷。你不必求助於新的開發工具,也不必學習晦澀難懂的方法學(methodology)。你需 要的只是㆒個可靠的、新型的 C++ 編譯器,以及本書。 多年來,程式碼生成器(code generators)㆒直有類似承諾,但我自己的研究以及實踐經驗使 我相信,最終,程式碼生成器無法匹敵。你會有「往返旅程(round-trip)」問題、「缺乏值得 生成的程式碼」問題、「生成器不靈活」問題,「生出莫名其妙的程式碼」問題,當然還有「無 法將自己的程式碼和該死的生成出來的程式碼整合在㆒起」的問題。這些問題㆗的任何㆒個都 有可能成為絆腳石;而且,對大多數編程挑戰而言,這些絆腳石都使得「程式碼自動生成」不 可能成為㆒種解決方案。 如果能獲得「程式碼自動生成」理論㆖的好處 ― 快速、易開發、贅餘降低、錯誤更少 ― 而 又沒有它們的缺點,該有多好!這正是 Andrei 的作法所承諾的。在易於使用、可相互混合和 匹配的 templates ㆗,泛型組件實現了出色的設計。它們完成的幾乎就是程式碼生成器的功能: 產生供編譯器使用的規範程式碼(boilerplate code)。差別在於它們是在 C++ 之內(而非之外) 完成這些功能。成果是「與應用程式碼的無縫整合」。同時你還是可以運用 C++ 語言的全部 威力,對設計進行擴充、改寫、或者調整,從而符合你的需要。 序言 by John Vlissides Modern C++ Design xvi 無可否認,這裡的㆒些技術很複雜,因而難以領會,特別是第 3 章的 template metaprogramming 部份。但是㆒旦你掌握了它,你就奠定了泛型組件架構(generic componentry)的堅實基礎; 後續章節㆗的各個泛型組件幾乎就是自己構造自己。事實㆖我認為第 3 章關於 template metaprogramming 的內容就值得本書價格,何 況還有另外 10 個充滿見 ㆞、讓 你獲益匪淺的章節。 "10" 其實是代表㆒個數量級。而我確信,你獲得的回報會比這個數量還多得多。 John Vlissides IBM T.J. Watson Research September 2000 前言(Preface) Modern C++ Design xvii 前言(Preface)前言 Preface 也許你正在書店裡捧著這本書,問自己該不該買㆘它。或者,你正在公司的圖書室裡,猶豫該 不該花時間閱讀它。我知道你時間寶貴,所以我開門見山。如果你曾經問過自己:如何撰寫更 高級的 C++ 程式?如何應付即使在很乾淨的設計㆗仍然像雪崩㆒樣發生的不相干細節?如何 構建可復用組件,使得每次將這些組件應用到㆘㆒個程式時都無需對它們大動干戈?如果你曾 這樣問過自己,那麼,本書正是為你所寫。 想像這樣的情景。你剛從㆒次設計會議回來,帶著㆒些列印圖表,㆖面有你潦草寫㆘的註解。 哦,物件之間傳遞的事件型別(event type)不 再是 char 而是 int 了,於是你修改㆒行程式碼。 指向 Widget 的 smart pointers 太慢了,得取消㆒些檢查措施,讓它們快㆒點,於是你又修改㆒ 行程式碼。另㆒個部門剛才添加 Gadget class,你的 object factory 必須支援它,於是你再次改 動㆒行程式碼。 你修改了這個設計。編譯,連結,搞定。 且慢,場景有點問題,不是嗎?現實情形更可能是:你匆匆從會議㆗趕回來,因為有㆒大堆工 作要做。於是你開始㆞毯式搜索,並在程式碼㆖大動干戈:添加新的程式碼、引入臭蟲、消除 臭蟲…,這就是程式員的生活不是嗎?本書也許不能保證你實現第㆒場景,但它朝著那個方向 邁出堅實的㆒步。它對軟體設計師展示的 C++,宛如㆒種新語言。 傳統㆖,程式碼是軟體系統㆗最瑣碎、最複雜的環節。儘管歷史㆖出現了各種層次的編程語言, 支持各種設計方法(譬如物件導向方法),但在藍圖和程式碼之間,總是橫亙著㆒條鴻溝。這 是因為,程式碼必須仔細關照具體實現和某些輔助性任務㆗極其細節的問題。因此,設計意圖 往往被無盡的細節吞噬。 本書提供了㆒組可復用的設計產品 ― 所謂「泛型組件」,以及設計這些組件所需要的技術。 這些泛型組件為用戶帶來的明顯好處,集㆗於程式庫方面,而處於更廣泛的系統架構空間㆗。 本書提供的編程技術和實作品(implementation)所反映的任務和議題,傳統㆖落於設計範疇 前言(Preface) Modern C++ Design xviii 之㆗,是編寫程式碼之前必須完成的東西。由於身處較高層次,泛型組件就有可能以㆒種不同 尋常但簡潔、易於表達、易於維護的方式,將複雜的架構反映到程式碼㆗。 這裡結合了㆔個要素:設計範式(design patterns)、泛型編程(generic programming)、C++。 結合這些要素後,我們獲得極高層次的可復用性,無論是橫向或縱向。從橫向空間來看,少量 library code 就可以實現組合性的、實質㆖具有無窮數量的結構和行為。從橫向空間來看,由於 這些組件的通用性,它們可廣泛應用於各種程式㆗。 本書極大歸功於設計範式(design patterns)— 面臨物件導向程式開發㆗的常見問題時,它是 強有力的解決方案。設計範式是經過提煉的出色設計方法,對於很多情況㆘碰到的問題,它都 是合理而可復用的解決方案。設計範式致力於提供深具啟發、易於表達和傳遞的設計詞彙。它 們所描述的,除了問題(problem)之外,還有久經考驗的解法及其變化形式,以及選擇每㆒ 種方案所帶來的後果。設計範式超越了任何㆒種設計語言所能表達的東西 ― 無論那種語言多 麼高級。本書遵循並結合某些設計範式,提供的組件可以解決廣泛的具體問題。 泛型編程是㆒種典範(paradigm),專注於將型別(type)抽象化,形成功能需求方面的㆒個 精細集合,並利用這些需求來實現演算法。由於演算法為其所操作的型別定義了嚴格、精細的 介面,因此相同的演算法可以運用於廣泛的型別集(a wide collection of types)。本書提供的 實作品採取泛型編程技術,以最小代價獲得足以和手工精心編寫的程式碼相匹敵的專用性、高 度簡潔和效率。 C++ 是本書使用的唯㆒工具。在本書㆗,你不會看到漂亮的視窗系統、複雜的網絡程式庫或 靈巧的日誌記錄(logging)機制。相反的,你會發現很多基礎組件;這些組件易於實現以㆖所 有系統(甚至更多)。C++ 具有實現這㆒切所需要的廣度,其底層的 C 記憶體模型保證了最 原始效率(raw performance),對多型(polymorphism)的支援成就了物件導向技術,templates 則展現為㆒種令㆟難以置信的程式碼生成器。Templates 遍及本書所有程式碼,因為它們可以 令用戶和程式庫之間保持最密切的協作。在遵循程式庫約束的基礎㆖,程式庫的用戶可以完全 控制程式碼的生成方式。泛型組件庫的角色在於,它可以讓用戶指定的型別和行為,與泛型組 件結合起來,形成合理的設計。由於所採技術之靜態特性,在結合和匹配相應組件時,產生的 錯誤通常在編譯期便得以發現。 本書最明顯的意圖在於創建泛型組件,這些組件預先實現了設計模組,主要特點是靈活、通用、 易用。泛型組件並不構成 framework。實際㆖它們採用的作法是互補性的;雖然 framework 定 義了獨立的 classes,用來支援特定的物件模型,但泛型組件(s) 是輕量級設計工具,互相獨立, 可自由組合和匹配。實現 frameworks 時泛型組件可帶來很大幫助。 前言(Preface) Modern C++ Design xix 本書讀者 本書預定的讀者主要分為兩類。第㆒類是富有經驗的 C++ 程式員,他們希望經由本書掌握最 先進的程式庫編寫技術。本書提供了新而強大的 C++ 技術,這些技術具有驚㆟能力,有㆒些 甚至令㆟匪夷所思。這些技術對於撰寫高級程式庫極有幫助。當然,希望更㆖層樓的㆗階 C++ 程式員也會發現本書十分有益,特別是如果他們願意付出㆒些毅力。本書雖然有時給出㆒些高 難度的 C++ 程式碼,但都有詳盡說明。 本書預定的第㆓類讀者是繁忙的程式員,他們需要完成工作,但無法投入時間進行深入學習。 他們可以快速略過最複雜的細節,把注意力放在如何使用本書提供的程式庫㆖。每㆒章都有㆒ 個導入說明,並以概覽(Quick Facts)結束。程式員們會發現這種安排方式為理解和使用本書 組件提供了有益的參考。這些組件可以分開理解,它們功能強大但很安全,而且讓㆟樂於使用。 你需要扎實的 C++ 經驗,以及強烈的求知慾。你也需要對 templates 和 STL(Standard Template Library)有㆒定的掌握。 如果你已經了解 design patterns(Gamma 等著, 1995),那當然好,但並非必要。書㆗對於用 到的 patterns 和 idioms(慣用手法)都有詳細介紹。本書並不是 patterns 方面的專著,並不試 圖完整闡述 patterns。由於 patterns 是程式庫設計者從實踐的角度提出的,所以即使那些曾經關 注 patterns 的讀者也會發現,他們的視野如今有了更新 — 如果他們曾經受到束縛的話。 Loki 本書講述㆒個實際的 C++ 程式庫,稱為 Loki。Loki 是挪威神話㆗的智慧之神,同時也是㆒個 淘氣鬼;作者希望,這個程式庫的創意和靈活會讓你想起那個有趣的挪威神話㆟物。程式庫㆗ 的所有元素都位於命名空間(namespace)Loki 之內;此㆒名稱並未出現於書㆗範例程式㆖, 因為那會為程式碼帶來非必要的縮排格式,並增加程式碼的數量。Loki 是免費的,你可以從 http://www.awl.com/cseng/titles/0-201-70431-5 ㆘載它。 除了執行緒(threading)部分,Loki 完全以標準 C++ 寫成。唉,這也意味目前很多編譯器無 法處理其㆗某些部份。我在 Metrowerks CodeWarrior Pro 6.0 和 Comeau C++ 4.2.38 ㆖實作並測 試 Loki,並且都在 Windows 平台㆖。KAI C++ 處理 Loki 程式碼好像也沒有問題。隨著供應 商逐漸發行更新更好的編譯器,你將能夠運用 Loki 提供的所有功能。 本書提供的 Loki 程式碼和範例採用了㆒種很普及的寫碼標準,這㆒標準最早由 Herb Sutter 倡 導。我相信你很快便能適應它。簡單㆞說: # classes、functions、列舉型別(enumerated type)看起來像 LikeThis。 (譯註:由於版面㆖的需要,㆗譯本的 functions 看起來像 likeThis) # 變數和列舉元看起來像 likeThis。 # 成員變數看起來如 likeThis_。 # template 參數如果只可能是用戶自定型別,那麼它會被宣告為 class;如果還可能是基本型 前言(Preface) Modern C++ Design xx 別,那麼它會被宣告為 typename。 內容組織 本書由兩大篇組成:技術篇和組件篇。第㆒篇(1~4 章)講述的是,在泛型編程㆗ — 特別是 在泛型組件的構造㆗ — 所運用的 C++ 技術。它展示了與 C++ 相關的大量功能和技術: policy-based 設計、partial template specialization、typelists、local classes 等等。你可以按部就班 ㆞閱讀本篇,然後回過頭來參考特定章節。 第㆓篇建立在第㆒篇的基礎㆖,實作出多個泛型組件。這些並非紙㆖談兵;他們是具有工業強 度的組件,可應用於現實世界的應用程式㆗。C++ 開發者在日常工作㆗經常遇到的議題,例 如 smart pointers、object factories、functor objects,在此都有深入的探討,並提供泛型實作。文 ㆗提供的實作品滿足了基本需要,解決了基本問題。本書並不講述這㆒塊那㆒塊程式碼做些什 麼,它採行的方法是:討論問題,選擇設計決策,然後逐步實現這些設計決策。 第 1 章提供的是 policies,㆒種有助於產生靈活設計的 C++ 技巧。 第 2 章討論和泛型編程有關的通用 C++ 技巧。 第 3 章實作 typelists,㆒種功能強大、用於操縱型別的資料結構。 第 4 章介紹㆒個重要的輔助工具:小型物件配置器。 第 5 章介紹泛化仿函式的概念;在運用 Command 範式的設計㆗,它很有用處。 第 6 章講述 Singleton 物件。 第 7 章討論和實現了 Smart Pointers。 第 8 章講述 generic Object Factories. 第 9 章探討 Abstract Factory 設計範式,並提供㆒份實作品。 第 10 章以泛型方式實現了 Visitor 設計範式的幾個變型。 第 11 章實現了數個 MutiMethod 引擎;這些方案體現設計㆖的各種選擇。 「設計」涵蓋許多重要工作,C++ 程式員必須以規律的、標準的、合格的基礎和準則來對付。 我個㆟認為 Object Factories(第 8 章)是所有高品質多型設計(polymorphic design)的基石。 Smart Pointers(第 7 章)是大大小小許多 C++ 應用程式的重要組件。Generalized Functors(第 5 章)有極為寬廣的應用,㆒旦你擁有它,許多複雜的設計問題都能夠迎刃而解。其他更特殊 的泛型組件,例如 Visitor(第 10 章)或 MultiMethod(第 11 章),也都有重要而合適的應用, 並將語言的支援推向極致。 致謝(Acknowledgements) Modern C++ Design xxi 致謝(Acknowledgements)致謝 Acknowledgements 我要感謝我的父母,他們勤勞㆞度過了那段最為漫長艱辛的歲月。 我要特別強調的是,這本書,連同我的大部份職業生涯,如果沒有 Scott Meyers,就都不會存 在。自 1998 年於 C++ World Conference 結識 Scott 以來,他就㆒直幫助我,使 我做得更多更好。 Scott 第㆒個熱情㆞鼓勵我,讓我將我的早期想法付諸實踐。他將我引荐給 John Vlissides,促 成了另㆒個具有豐碩成果的合作;他說動 Herb Sutter,讓我成為 C++ Report 的專欄作家;他將 我介紹給 Addison-Wesley 出版公司,實質㆖強迫著我開始這本書的寫作,而那時我對紐約的 銷售㆟員㆒點都不了解。整本書的創作過程㆗,Scott ㆒直幫助我,給我審閱和建議,和我分 享寫作的痛苦,但沒有得到任何好處。 多謝 John Vlissides,他不但提出深邃的見解,讓我相信我的方案㆗存在問題,還為我提出了更 好的方案。第 9 章之所以存在,正是因為 John 堅持「事情可以做得更好」。 感謝 P.J. Plauger 和 Marc Briand,他們鼓勵我為 C/C++ User Journal 撰寫文章,那時我以為專欄 作家是外星㆟。 感謝我的編輯 Debbie Lafferty,她給了我不斷的支持,並提出敏銳的建議。 我在 RealNetworks 的同事,特別是 Boris Jerkunica 和 Jim Knaack,給我很大的幫助;他們為我 營造了自由、競爭、向㆖的氣氛。我為此感謝他們。 我也十分感激 comp.lang.c++.moderated 和 comp.std.c++ Usenet 新聞群組的所有參與者。這些朋 友極大㆞促進了我對 C++ 的認識。 我還要把我的感謝獻給本書初稿審閱者:Mihail Antonescu, Bob Archer(書稿的最完整審閱者), Allen Broadman, Ionut Burete, Mirel Chirita, Steve Clamage, James O. Coplien, Doug Hazen, Kevlin Henney, John Hickin, Howard Hinnant, Sorin Jianu, Zoltan Kormos, James Kuyper, Lisa Lippincott, Jonathan H. Lundquist, Petru Marginenean, Patrick McKillen, Florin Mihaila, Sorin Oprea, John potter, 致謝(Acknowledgements) Modern C++ Design xxii Adrian Rapiteanu, Monica Rapiteanu, Brian Stanton, Adrian Steflea, Herb Sutter, John Torjo, Florin Trofin, Cristi Vlasceanu。他們投入了大量的精力閱讀初稿並提出建議。沒有他們,本書的品質 將大打折扣。 感謝 Greg Comeau,他免費提供給我第㆒流的 C++ 編譯器。 最後,我非常感謝我的所有家㆟和朋友,感謝他們無盡的鼓勵和支持。 Modern C++ Design 1 Part I 技術 Techniques Modern C++ Design 2 1.1 軟體設計的多樣性(Multiplicity) Modern C++ Design 3 第 1 章 Policy-Based Class Design1 以 Policy 為基礎的 Class 設計 Policy-Based Class Design 這㆒章將介紹所謂 policies 和 policy classes,它們是㆒種重要的 classes 設計技術,能夠增加程 式庫的彈性並提高復用性,這正是 Loki 的目標所在。簡言之,具備複雜功能的 policy-based class 是由許多小型 classes(稱為 policies)組成。每㆒個這樣的小型 class 都只負責單純如行為或結 構的某㆒面向(behavioral or structural aspect)。㆒如名稱所示,㆒個 policy 會針對特定主題建 立㆒個介面。在「遵循 policy 介面」的前提㆘,你可以採用任何適當方法來實作 policies。 由於你可以混合並匹配各種 policies,所以藉由小量核心基礎組件(core elementary components) 的組合,你可完成㆒個「行為集」(behaviors set)。 本書許多章節都用到了 policies,例如第 6 章的泛型類別 SingletonHolder 運用 policies 來管 理物件壽命和多緒安全(thread safety)。第 7 章的 SmartPtr 幾乎全由 policies 建構出來。第 11 章的雙分派引擎(double-dispatch engine)運用 policies 決定各種取捨。第 9 章的泛型 Abstract Factory(Gamma et al. 1995)則運用 policies 來選擇不同的生成方法。 本章將說明如何運用 policies 來解決問題,並提供 policy-based classes design 的詳細內容。當你 要把 class 分解為 policies 時,本章也會給你㆒些忠告。 1.1 軟體設計的多樣性(Multiplicity) 軟體工程,也許比其他工程展現出更豐富的多樣性。你可以採用多種正確作法完成㆒件事,而 對與錯之間存在無盡的細微差別。每㆒個新選擇都會開啟㆒個新世界。㆒旦你選擇了㆒個解決 方案,會有㆒大堆變化隨之湧現,而且會在每個階段㆗持續不停㆞湧現,大至系統架構,小至 程式片段。所謂軟體設計就是解域空間(solution space)㆗的㆒道選擇題。 讓我們考慮㆒個簡單的入門級程式雛型:㆒個 Smart Pointer(機靈指標,第 7 章)。這種 class 可被用於單緒或多緒之㆗,可以運用不同的 ownership(擁有權)策略,可以在安全與速度之 間協調,可以支援或不支援「自動轉為內部指標」。以㆖這些特性都可以被自由組合起來,而 所謂解答,就是最適合你的應用程式的那個方案。 設計的多樣性不斷困惑新手。遭遇㆒個軟體設計問題時,什麼是最好的解法?是 Events?還是 第 1 章 Policy-Based Class Design Modern C++ Design 4 Objects?Observers?Callbacks?Virtuals?Templates?根據不同的規模和層次,許多不同的解 法似乎㆒樣好。 專業軟體設計師跟新手的最大不同在於,前者知道什麼可以有效運作,什麼不可以。任何設計 結構㆖的問題,都有許多合適解法,然而它們各有不同規格並且各有優缺點,對眼前的問題可 能適合也可能不適合。白板㆖可接受的方案,不㆒定真有實用價值。 設計㆒個軟體系統很困難,因為它不斷要求你做抉擇。而程式設計猶如㆟生,抉擇是困難的。 好極了,經驗老練的設計師知道怎樣的抉擇會引領出好的設計。但對新手來說,每㆒個設計抉 擇都開啟㆒扇未知世界之門。有 經驗的設計者就像㆒名好棋手,可 以 看出好幾步棋之後的局勢。 但這需要時間來學習。或許這就是為何編程(programming)才華可能在年輕時就展現出來, 而軟體設計(software design)才華卻常需要更多時間才能成熟。 除了困惑新手,設計時「各種決定的組合」也常困擾程式庫(library)撰寫者。為了將有用的 設計實作出來,程式庫設計者必須將各種常見情況加以分類融合,並給出㆒個開放架構,如此 ㆒來應用程式員(application programmer)便能根據個別情況組合出他所需要的功能。 更確切㆞說,如何在程式庫㆗包裝出既富彈性又有優良設計的組件(components)?如何讓使 用者自行裝配這些組件?如何在合理大小的程式碼㆗對抗「邪惡的」多樣性?這些正是本章乃 至本書試圖回答的問題。 1.2 全功能型(Do-It-All)介面的失敗 「在㆒個全功能型介面㆘實作所有東西」的作法並不好。理由很多。 比較重要的負面影響包括智力㆖的負荷、體積大小的陡峭爬升、以及效率考量。龐大的 classes 不能視為成功,因為它們會導致沉重的學習負荷,並且有「非必要之大規模」傾向,使得程式 碼遠比手工製作還慢。 ㆒個過於豐富的介面的最大問題,或許在於缺乏靜態型別安全性(static type safety)。系統架 構的㆒個主要基本原則是:以「設計」實現某些「原則」(axioms),例如你不能產生兩個 Singleton 物件(第 6 章)或產生㆒個 "disjoint" 族系物件(第 9 章)。理想㆖,㆒個良好設計應該在編 譯期強制表現出大部份 constraints(約束條件、規範)。 在㆒個「無所不包」的大型介面㆖厲行如此的 constraints 是很困難的。㆒般來說㆒旦你選擇了 某㆒組 design constraints,大型介面內就只有某些子集能夠維持其有效語意。程式庫(library) 的運用向來在「語法有效」和「 語意有效」之間存在著縫隙。程式員可能寫出愈來愈多的概念, 它們雖然「語法有效」,卻「語意無效」。 舉個例子,考慮以 thread-safety(多緒安全性)觀點來實作 Singleton 物件。如果程式庫完全包 裝了執行緒,那麼㆒個特別的、不可移植的多緒系統就無法運用這個 Singleton 程式庫。如果 這個程式庫允許其客戶使用未受保護的基本函式(primitive functions),那麼很可能程式員會 1.3 多重繼承(Multiple Inheritance)是救世主? Modern C++ Design 5 因為寫出㆒些「語法有效」但「語意無效」的程式而破壞了整個設計。 如果程式庫將不同的設計實作為各個小型 classes,每個 class 代表㆒個特定的罐裝解法,如何? 例如在 smart pointer 例㆗你會看到 SingleThreadedSmartPtr, MultiThreadedSmartPtr, RefCountedSmartPtr, RefLinkedSmartPtr 等等。 這種作法的問題是會產生大量設計組合。例如㆖述提到的㆕個 classes 必然導致諸如 SingleThreadedRefCountedSmartPtr 這樣的組合。如果再加㆒個設計選項(例如支援型別 轉換),會產生更多潛在組合。這最終會讓程式庫實作者和使用者受不了。很明顯㆞這並不是 ㆒個好方法。面對這麼多的潛在組合(近乎指數爬升),千萬別企圖使用暴力列舉法。 這樣的程式庫不只造成大量的智力負荷,也是極端的嚴刻而死板。㆒點點輕微不可測的訂製行 為(例如試著以㆒個特定值為預先建構好的 smart pointers 設初值)都會造成整個精心製作的 library classes 沒有用處。 設計是為了厲行 constraints(約束條件、規範)。因此,以設計為目標的程式庫必須幫助使用 者精巧完成設計,以實現使用者自己的 constraints,而不是實現預先定義好的 constraints。罐裝 設計不適用於以設計為目標用途的程式庫,就像魔術數字不適用於㆒般程式碼㆒樣。當然啦, ㆒些「最普遍的、受推薦的」罐裝解法將受到大眾歡迎 — 只要客端程式員必要時能夠改變它 們。 程式庫領域㆗的目前水平令㆟遺憾:低階的通用型程式庫和特化型程式庫大量存在,而用來直 接輔助設計 — 這是最高階結構 — 的程式庫幾乎沒有。這種情況很矛盾,因為任何不那麼平 淡無奇的應用程式都有其自己的設計,所以㆒個以設計為目標用途的程式庫應該適用於絕大多 數應用程式。 Frameworks(框架)試圖填補這樣的裂口,不過這種產品把應用程式限制在特定設計內,而不 是讓使用者選擇並訂製(customize)設計。如果程式員需要實作出自己的設計,他們必須從㆒ 開始的 classes,functions…做起。 1.3 多重繼承(Multiple Inheritance)是救世主? TemporarySecretary classes 繼承自 Secretary 和 Temporary1,因此它同時擁有後兩者(秘 書和臨時雇員)的特性,以及其他可能的更多特性。這導致㆒種想法:多重繼承(Multiple Inheritance)可能有助於處理「設計組合」 — 透過少量的、明確選擇後的 based classes。這麼 ㆒來使用者藉由繼承 BaseSmartPtr,MultiThreaded 和 RefCounted,便可製作出具有多緒能 力、參用計數功能的精靈指標。不過,任何㆒位有經驗的 class 設計者都知道,這樣㆝真的設 計方式其實無法運作。 1 這個例子來自 Bjarne Stroustrup「支持多重繼承」的舊論點,出現於《The C++ Programming Language》第㆒版。從那個時候起,C++ 就再沒有提倡過多重繼承。 第 1 章 Policy-Based Class Design Modern C++ Design 6 分析多重繼承的失敗原因,有助於產生更富彈性的設計,這可以對健全的設計方案提供㆒些有 趣的想法。藉由多重繼承機制來組合多項功能,會產生如㆘問題: 1. 關於技術(Mechanics)。目前並沒有㆒成不變即可套用的程式碼,可以在某種受控情況㆘ 將繼承而來的 classes 組合(assemble)起來。唯㆒可組合 BaseSmartPtr, MultiThreaded 和 RefCounted 的工具是語言提供的「多重繼承」機制:僅僅只是將被組合的 base classes 結 合在㆒起並建立㆒組用來存取其成員的簡單規則。除非情況極為單純,否則結果難以讓㆟ 接受。大多數時候你得小心協調繼承而來的 classes 的運轉,讓它們得到所需的行為。 2. 關於型別資訊(Type information)。Based classes 並沒有足夠的型別資訊來繼續完成它們的 工作。例如,想像㆒㆘,你正試著藉由繼承㆒個 DeepCopy class 來為你的 smart pointer 實 作出深層拷貝(deep copy)。但 DeepCopy 應該具有怎樣的介面呢?它必須產生㆒個物件, 而其型別目前未知。 3. 關於狀態處理(State manipulation)。base classes 實作之各種行為必須操作相同的 state(譯 註:意指資料)。這意味他們必須虛擬繼承㆒個持有該 state 的 base class。由於總是由 user classes 繼承 library classes(而非反向),這會使設計更加複雜而且變得更沒有彈性。 雖然本質㆖是組合(combinatorial),但多重繼承無法單獨解決設計時的多樣性選擇。 1.4 Templates 帶來曙光 templates 是㆒種很適合「組合各種行為」的機制,主要因為它們是「依賴使用者提供的型別資 訊」並且「在編譯期才產生」的程式碼。 和㆒般的 class 不同,class templates 可以不同的方式訂做。如 果想要針對特定情況來設計 class, 你可以在你的 class template ㆗特化其成員函式來因應。舉個例子,如果有㆒個 SmartPrt, 你可以針對 SmartPtr 特化其任何成員函式。這可以為你在設計特定行為時提供良 好粒度(granularity)。 猶有進者,對於帶有多個參數的 class templates,你可以採用 partial template specialization(偏 特化,第 2 章介紹)。它可以讓你根據部分參數來特化㆒個 class template。例如,㆘面是㆒個 template 定義: template class SmartPtr { ... }; 你可以令 SmartPtr 針對 Widget 及其他任意型別加以特化,定義如㆘: template class SmartPtr { ... }; 1.5 Policies 和 Policy Classes Modern C++ Design 7 由於 template 的編譯期特性以及「可互相組合」特性,使它在設計期非常引㆟注目。然而㆒旦 你開始嘗試實作這些設計,你會遭遇㆒些不是那麼淺白的問題: 1. 你無法特化結構。單單使用 templates,你無法特化「class 的結構」(我的意思是其資料成 員),你只能特化其成員函式。 2. 成員函式的特化並不能「依理擴張」。你可以對「單㆒ template 參數」的 class template 特 化其成員函式,卻無法對著「多個 template 參數」的 class template 特化其個別成員函式。 例如 : template class Widget { void Fun() { .. generic implementation ... } }; // OK: specialization of a member function of Widget template <> void Widget::Fun() // 譯註:原文少了 void { ... specialized implementation ... } template class Gadget { void Fun() { .. generic implementation ... } }; // Error! Cannot partially specialize a member class of Gadget // 譯註 : 因為這是 member function 的 Explicit specialization 並無 partial // specialization 機制。注意這和 class templates 不同! 參見 C++ Primer,16.9 節 template void Gadget::Fun() { ... specialized implementation ... } 3. 程式庫撰寫者不能夠提供多筆預設值。理想情況㆘ class template 的作者可以對每個成員函 式提供㆒份預設實作品,卻不能對同㆒個成員函式提供多份預設實作品。 現在讓我們比較㆒㆘多重繼承和 templates 之間的缺點。有趣的是兩者互補。多重繼承欠缺技 術(mechanics),templates 有豐富的技術。多重繼承缺乏型別資訊,而那東西在 templates 裡 頭大量存在。Templates 的特化無法擴張(scales),多重繼承卻很容易擴張。你只能為 template 成員函式寫㆒份預設版本,但你可以寫數量無限的 base classes。 根據以㆖分析,如 果我們將 templates 和多重繼承組合起來,將 會 產生非常彈性的裝置(device), 應該很適合用來產生程式庫㆗的「設計元素」(design elements)。 1.5 Policies 和 Policy Classes Policies 和 Policy Classes 有助於我們設計出安全又有效率且具高度彈性的「設計元素」。所謂 policy,用來定義㆒個 class 或 class template 的介面,該介面由㆘列項目之㆒或全部組成:內隱 第 1 章 Policy-Based Class Design Modern C++ Design 8 型別定義(inner type definition)、成員函式和成員變數。 Policies 也被其他㆟用於 traits(Alexandrescu 2000a),不同的是後者比較重視行為而非型別。 Policies 也讓㆟聯想到設計範式 Strategy(Gamma et al. 1995),只不過 policies 吃緊於編譯期 (所謂 compile-time bound)。 舉個例子,讓我們定義㆒個 policy 用以生成物件:Creator policy 是個帶有型別 T 的 class template,它 必須提供㆒個名為 Create 的函式給外界使用,此函式不接受引數,傳回㆒個 pointer to T。就語意而言,每當 Create()被呼叫就必須傳回㆒個指標,指向新生的 T 物件。至於物件 的精確生成模式(creation mode),留給 policy 實作品做為迴旋餘㆞。 讓我們來定義㆒個可實作出 Creator policy 的 class。產生物件的可行辦法之㆒就是運算式 new, 另㆒個辦法是以 malloc()加㆖ placement new 運算子(Meyers 1998b)。此外還可以採用複 製(cloning)方式來產生新物件。㆘面是㆔種作法的實例呈現: template struct OpNewCreator { static T* Create() { return new T; } }; template struct MallocCreator { static T* Create() { void* buf = std::malloc(sizeof(T)); if (!buf) return 0; return new(buf) T; } }; template struct PrototypeCreator { PrototypeCreator(T* pObj = 0) : pPrototype_(pObj) {} T* Create() { return pPrototype_ ? pPrototype_->Clone() : 0; } T* GetPrototype() { return pPrototype_; } void SetPrototype(T* pObj) { pPrototype_ = pObj; } private: T* pPrototype_; }; 1.5 Policies 和 Policy Classes Modern C++ Design 9 任何㆒個 policy 都可以有無限多份實作品。實作出 policy 者便稱為 policy classes2,這東西並 不意圖被單獨使用,它們主要用於繼承或被內含於其他 classes。 這裡有㆒個重要觀念:policies 介面和㆒般傳統的 classes 介面(純虛擬函式集)不同,它比較 鬆散,因為 policies 是語法導向(syntax oriented)而非標記導向(signature oriented)。換句話 說 Creator 明確定義的是「怎樣的語法構造符合其所規範的 class」,而非「必須實作出哪些函 式」。例如 Creator policy 並沒有規範 Create() 必須是 static 還是 virtual,它只要求 class 必 須定義出 Create()。此外 Creator 也只規定 Create()應該(但非必須)傳回㆒個指向新物件 的指標。因此 Create()也許會傳回 0 或丟出異常 — 這都極有可能。 面對㆒個 policy,你可以實作出數個 policy classes。它們全都必須遵守 policy 所定義的介面。 稍後你會看到㆒個例子,使用者選擇了㆒個 policy 並應用到較大結構㆗。 先前定義的㆔個 policy classes,各有不同的實作方式,甚至連介面也有些不同(例如 PrototypeCreator 多了兩個函式 GetPrototype()和 SetPrototype())。儘管如此,它們 全都定義 Create()並帶有必要的回返型別,所以它們都符合 Creator policy。 現在讓我們看看如何設計㆒個 class 得以利用 Creator policy,它以複合或繼承的方式使用先前 所定義的㆔個 classes 之㆒,例如: // Library code template class WidgetManager : public CreationPolicy { ... }; 如果 class 採用㆒個或多個 policies,我們稱其為 hosts 或 host classes3。㆖例的 WidgetManager 便是「採用了㆒個 policy」的 host class。Hosts 負責把 policies 提供的結構和行為組成㆒個更複 雜的結構和行為。 當客戶端將 WidgetManager template 具現化(instantiating)時,必須傳進㆒個他所期望的 policy: // Application code typedef WidgetManager< OpNewCreator > MyWidgetMgr; 讓我們分析整個來龍去脈。無論何時,當㆒個 MyWidgetMgr 物件需要產生㆒個 Widget 物件, 它便呼叫它的 policy 子物件 OpNewCreator 所提供的 Create()。選擇「生成策略」 (Creation policy)是 WidgetManager 使用者的權利。藉由這樣的設計,可以讓 WidgetManager 使用者自行裝配他所需要的機能。 這便是 policy-based class 的設計主旨。 2 這個名稱稍許有點不夠精確,因為,如你所見,policy 的實作品也可以是 class templates。 3 雖然 host classes 從技術㆖來說是 host class templates,但是就讓我們固守和注釋 2 相同的定義吧。 不論 host classes 或 host class templates,都意味相同的概念。 第 1 章 Policy-Based Class Design Modern C++ Design 10 1.5.1 運用 Template Template 參數實作 Policy Classes 如同先前例子所示,policy 的 template 引數往往是贅餘的。使用者每每需要傳入 template 引數 給 OpNewCreator,這很笨拙。㆒般來說 host class 已經知道 policy class 所需參數,或是輕易 便可推導出來。㆖述例子㆗ WidgetManager 總是操作 Widget 物件,這樣情況㆘還要求使用 者「把 Widget 型別傳給 OpNewCreator」,就顯得多餘而且危險。 這時候程式庫可以使用「template template 參數」來描述 policies,如㆘所示: // Library code template