Andrei Alexandrescu:modern C++ Design——generic Programming And Design Patterns Applied@2001
以下是根據您提供的資料,提取主要論點並進行詳盡解釋的 API 回覆:
Modern C++ Design: 泛型編程與設計模式應用
本書由 Andrei Alexandrescu 撰寫,出版於 2001 年,被 Herb Sutter 等 C++ 專家高度推薦,被譽為一本開創性的著作。它核心探討了如何結合 C++ 模板、泛型編程與設計模式,創造出表達力強、靈活且高度可重用的軟體設計。書中引入了「泛型組件」(generic components)的概念,這些是可重用的設計模板,能為編譯器生成樣板程式碼(boilerplate code),促進從設計到程式碼的平滑過渡,並以最少的重複編碼實現設計結構的重用。
全書分為兩個主要部分:技術(Techniques)和組件(Components)。第一部分介紹了實現泛型編程和泛型組件所使用的 C++ 技術;第二部分則在此基礎上,實作了一系列工業級別的泛型組件,解決 C++ 開發者日常工作中遇到的常見問題。
核心論點與主要技術:
1. Policy-Based Class Design (基於策略的類別設計)
這是本書最核心的設計理念之一。它旨在解決軟體設計中的多樣性與組合爆炸問題。傳統上,為軟體組件提供靈活性常導致龐大的「全能介面」或指數級增長的專用類別。基於策略的設計則將一個複雜類別的不同行為或結構方面分解到小的、專注的類別(稱為策略,Policies)中。一個使用策略的主類別(Host Class)通常是模板類別,它繼承或包含一個或多個策略類別作為模板參數。
策略類別定義了一個介面(可以是型別定義、成員函式或成員變數的組合),但這個介面是鬆散的、語法導向的,而非嚴格的簽名導向。策略的實作可以有多種形式,只要符合策略定義的介面即可。主類別負責協調這些策略的功能。透過組合不同的策略類別,可以產生大量不同的行為組合,以少量基本組件覆蓋廣泛的設計空間。這種設計模式利用了模板的編譯期程式碼生成能力與多重繼承的結構組合能力。它在編譯期進行綁定,相較於執行期多型(如虛擬函式),具有更高的效率。策略設計還允許「豐富策略」(Enriched Policies),即策略類別可以提供主類別不需要但使用者可能需要的額外功能,且不會影響主類別的基本功能。更進一步,透過 C++ 的不完全實例化(Incomplete Instantiation)特性,如果主類別中使用策略的某個成員函式未被實際呼叫,即使該策略沒有提供該函式,程式碼也能編譯通過,實現了可選功能(Optional Functionality)。基於策略的設計是 Loki 庫中許多組件的基石,特別是 SmartPtr 和 SingletonHolder。
2. Compile-Time Techniques (編譯期技術)
本書廣泛使用了各種 C++ 編譯期技術來實現程式碼生成、靜態斷言和型別操作:
-
靜態斷言 (Compile-Time Assertions):提供一種在編譯期檢查條件的方法,而不是像
assert宏那樣在執行期檢查。透過利用編譯器在某些無效結構(如長度為零的陣列)上發出錯誤的特性,結合模板,可以在編譯期驗證常量表達式,並發出比傳統方式更具描述性的錯誤訊息。STATIC_CHECK宏是其典型應用,它利用未定義的模板特化來引發編譯器錯誤。 -
部分模板特化 (Partial Template Specialization):允許為模板類別的模板參數子集提供特化版本,使得可以為符合特定模式的型別系列提供不同的實作。這是實現許多編譯期型別判斷和選擇技術的關鍵工具,例如
Select模板和TypeTraits。雖然函式模板不支援部分特化,但可以透過函式重載和型別轉型技術(如Int2Type,Type2Type)來模擬類似的功能。 - 本地類別 (Local Classes):定義在函式內部的類別。它們可以訪問外部模板函式的模板參數,雖然不是啟用新範例的必需品,但可以提高程式碼的局部性並簡化某些實作,特別是函式回彈(trampoline functions)的生成。
-
型別-值映射 (Mapping Integral Constants to Types, Int2Type):
Int2Type<v>模板根據一個整數常量v生成一個獨特的型別。這使得可以基於編譯期整數常量進行靜態函式分派(Static Dispatch),解決了某些編譯器對死程式碼分支的嚴格編譯檢查問題。 -
型別-型別映射 (Type-to-Type Mapping, Type2Type):
Type2Type<T>模板根據一個型別T生成一個獨特的型別。它主要用於作為函式重載的分辨標記,模擬函式模板的部分特化,使得可以為不同的型別參數選擇不同的函式實作。 -
型別選擇 (Type Selection, Select):
Select<flag, T, U>模板根據一個編譯期布林值flag選擇兩個型別T或U中的一個。如果flag為真,則結果型別為T;否則為U。這是一個方便的編譯期條件型別判斷工具。 -
編譯期可轉換性和繼承性偵測 (Detecting Convertibility and Inheritance at Compile Time):介紹了一種利用
sizeof運算子結合函式重載,在編譯期判斷一個型別是否可以隱式轉換為另一個型別,進而判斷兩個型別之間是否存在公有、無歧義的繼承關係 (SUPERSUBCLASS宏)。這項技術對於泛型程式碼根據型別關係選擇不同實作至關重要,避免了執行期dynamic_cast的開銷。 -
型別資訊包裝 (TypeInfo):標準 C++ 的
std::type_info類別提供了執行期型別資訊,但其設計(禁止複製、指標比較問題)使其使用不便。TypeInfo是對std::type_info的包裝,提供了值語義(可複製、可賦值)和正確的比較運算,使得std::type_info對象可以方便地用於標準容器的鍵值。 -
輔助型別 (NullType, EmptyType):
NullType作為型別列表的結束標記或表示「無效型別」。EmptyType作為模板參數的預設值,表示「不關心」或作為線性繼承中的基類。 -
型別特性 (Type Traits, TypeTraits):
TypeTraits<T>模板提供了一系列針對任意型別T的編譯期特性判斷,例如是否為指標、是否為參考、是否為常數、是否為算術型別等。它還提供了一些導出的型別定義,如PointeeType,ReferencedType,NonConstType,ParameterType(用於確定函式參數的最佳傳遞方式)等。這些特性幫助泛型程式碼根據型別的能力進行優化或調整行為。
3. Typelists (型別列表)
型別列表是一種在編譯期表示和操作型別集合的技術。它通過遞歸模板和部分特化實現了對型別的「列表」操作。一個基本的型別列表單元 Typelist<T, U> 包含兩個型別,一個是頭部 Head (T),另一個是尾部 Tail (U),尾部通常是另一個 Typelist 或 NullType。
-
線性化創建: 提供宏 (
TYPELIST_n) 來簡化型別列表的創建,避免了 LISP 風格的嵌套語法。 -
編譯期算法: 實現了許多類似於值列表操作的編譯期算法,如計算長度 (
Length)、按索引訪問 (TypeAt,TypeAtNonStrict)、搜尋 (IndexOf)、追加 (Append)、刪除 (Erase,EraseAll)、去重 (NoDuplicates)、替換 (Replace,ReplaceAll)、以及根據繼承關係進行部分排序 (MostDerived,DerivedToFront)。這些算法都是通過遞歸模板和特化在編譯期完成的。 -
類別生成 (Class Generation): 型別列表最有力的應用之一是自動生成類別層次結構。
-
散佈式層次 (Scattered Hierarchy, GenScatterHierarchy):根據一個型別列表和一個單一模板參數的模板類別,生成一個多重繼承的類別層次,最終類別繼承了模板類別應用於型別列表中每個型別的結果。這對於將多個獨立功能或數據成員注入到一個類別中非常有用,如
Holder模板的應用。它提供了按型別或按索引訪問繼承基類的方法。 -
線性層次 (Linear Hierarchy, GenLinearHierarchy):根據一個型別列表和一個雙參數模板類別(第二個參數是基類),生成一個單一繼承的鏈式類別層次。這對於定義共享虛擬表、優化大小的介面非常有用,如
EventHandler模板的應用。
-
散佈式層次 (Scattered Hierarchy, GenScatterHierarchy):根據一個型別列表和一個單一模板參數的模板類別,生成一個多重繼承的類別層次,最終類別繼承了模板類別應用於型別列表中每個型別的結果。這對於將多個獨立功能或數據成員注入到一個類別中非常有用,如
型別列表是實現通用抽象工廠(Abstract Factory)和訪問者(Visitor)等設計模式的關鍵工具,使得這些模式的實現可以在編譯期進行大量自動化。
4. Small-Object Allocation (小型物件分配器)
書中詳細介紹並實現了一個針對小型物件(數十到數百位元組)優化的記憶體分配器。標準庫提供的全局 operator new 和 operator delete 通常對小型物件效率低下,存在速度慢和空間開銷大的問題。
-
分層結構: 分配器採用分層設計:
Chunk管理固定大小區塊的記憶體,FixedAllocator管理多個Chunk來處理固定大小的分配請求,SmallObjAllocator管理多個FixedAllocator來處理不同大小的小型物件分配請求,而SmallObject基類則透過重載operator new和operator delete來封裝分配器,使得繼承它的類別自動使用優化分配。 -
Chunk的設計: 利用區塊內部的記憶體儲存可用區塊的索引,形成一個隱藏的單鏈表,實現固定時間的分配和釋放。這限制了每個Chunk的最大區塊數(通常為 255),但避免了額外的空間開銷和對齊問題。 -
FixedAllocator的設計: 使用std::vector管理Chunk,並通過allocChunk_和deallocChunk_指標作為快取,以優化最常見的分配和釋放模式(如批量分配、順序或逆序釋放)。 -
SmallObjAllocator的設計: 使用std::vector儲存FixedAllocator,按大小排序,並通過pLastAlloc_和pLastDealloc_指標進行快速查找,對大於預設小型物件大小的請求轉發給全局分配器。 -
SmallObject基類: 透過重載帶有大小參數的operator delete,利用編譯器在執行期提供的物件大小資訊,無需額外儲存大小,提高了效率。結合SingletonHolder管理SmallObjAllocator的單一實例。 - 多執行緒考慮: 分配器本身通過模板參數支持不同的執行緒模型(如單執行緒、類別級鎖定、物件級鎖定),確保在多執行緒環境下的安全使用。
這個小型物件分配器是 Loki 庫許多高效組件的基礎,如通用函子和智能指針,使得它們在動態分配時也能保持高性能。
5. Generalized Functors (通用函子)
通用函子是對 Command 設計模式的泛化實作。Command 模式的核心是將一個請求封裝到一個物件中,實現發起者與接收者的解耦以及請求的延遲執行。通用函子旨在封裝 C++ 中任何可呼叫實體(Callable Entities)——包括簡單函式、成員函式指標、函子物件,甚至其他通用函子——以及它們的部分或全部參數,形成一個型別安全的、具有值語義的物件。
- Command 模式: 解釋了 Command 模式的目的(封裝請求)、結構(Command 介面、ConcreteCommand、Invoker、Receiver)和優勢(介面解耦、時間分離)。區分了轉發命令(Forwarding Commands,僅轉發呼叫)和主動命令(Active Commands,包含額外邏輯)。通用函子主要針對轉發命令。
-
C++ 可呼叫實體: 列舉了 C++ 中所有可直接使用
operator()呼叫的實體。 -
Functor模板設計: 設計了Functor<ResultType, TList>模板,使用型別列表TList表示參數型別列表。它使用 Handle-Body 慣用法,內部持有一個FunctorImpl介面的智能指針。FunctorImpl是一個多型基類,針對不同參數數量提供不同的純虛擬operator()重載。 -
處理不同可呼叫實體: 透過模板化的構造函式和繼承自
FunctorImpl的具體處理類別 (FunctorHandler處理函子和簡單函式,MemFunHandler處理成員函式指標),Functor可以接收並封裝各種可呼叫實體。編譯器會根據傳入的實體型別推導模板參數並選擇正確的處理類別。利用 C++ 的不完全實例化特性,Functor可以定義多個operator()重載,但只有被使用的會被實例化。 - 參數與返回值轉換: 通用函子支持參數的隱式轉換和返回值的轉換,就像普通函式呼叫一樣,這得益於處理類別直接將呼叫轉發給被封裝的實體,由編譯器處理轉換規則。
-
成員函式指標: 特別討論了成員函式指標及其相關運算子
.*和->*的特性,它們的結果沒有具體型別,這給封裝帶來挑戰。MemFunHandler設計時考慮了通用性,使其可以處理智能指針作為物件指標。 -
參數綁定 (Binding): 實現了
BindFirst函式,可以將函子的第一個參數綁定到一個固定值,返回一個參數數量減少的函子。這使得函子可以儲存部分呼叫環境。 -
請求鏈接 (Chaining): 實現了
Chain函式,可以將多個函子鏈接起來,形成一個宏命令,依序執行。 -
性能問題: 討論了函子轉發函數可能引入的參數複製開銷,並提出使用
TypeTraits::ParameterType來優化參數傳遞。探討了動態分配FunctorImpl物件的開銷,並通過結合小型物件分配器來解決。
通用函子極大地提升了 C++ 在處理回呼、延遲執行和命令模式時的表達力與靈活性。
6. Implementing Singletons (實作單例)
Singleton 模式旨在確保一個類別只有一個實例,並提供一個全局訪問點。書中深入探討了單例實作中的各種複雜問題,特別是其生命週期管理。
- 區分靜態數據與單例: 指出僅使用靜態成員數據和函式(Monostate 模式)不足以完全替代單例,因為無法實現虛擬函式且初始化/清理問題複雜。
-
基本實現: 通過將構造函式、複製構造函式設為私有,並提供一個靜態的
Instance函式來返回單一實例指標(通常是靜態指標)或參考,實現了單例的唯一性約束。 -
生命週期問題: 單例的創建時機通常是在第一次呼叫
Instance時,但清理(刪除)時機是個難題。如果單例持有資源,未在程式結束前正確清理會導致資源洩漏。 -
Meyers 單例: 使用函式內的靜態局部變量實現單例,利用 C++ 語言規則確保在第一次進入函式時構造,並在程式結束時(通過
atexit機制)自動銷毀。這是最簡單且推薦的默認實現。 -
死參考問題 (Dead Reference Problem): 在程式結束時,某些單例(或全局對象)的析構函式可能會在另一個已被銷毀的單例(或全局對象)上呼叫
Instance,導致未定義行為。使用靜態布林標記可以在單例被銷毀後檢測到此類呼叫。 -
Phoenix 單例: 解決死參考問題的一種方案。允許單例在被銷毀後再次被創建(通過放置 new),以服務那些生命週期更長的對象。雖然解決了未定義行為,但可能導致資源洩漏或狀態丢失(如果
atexit的行為在某些編譯器上不符合預期)。 -
具有壽命的單例 (Singletons with Longevity): 提供更細粒度的生命週期控制方案。通過
SetLongevity函式為動態分配的對象(包括單例實例)指定一個壽命值,確保在程式結束時按壽命值倒序銷毀對象。這避免了 Phoenix 單例的重複創建和狀態丢失問題,但也需要手動管理壽命值。 -
多執行緒問題: 單例實例作為共享資源,在多執行緒環境下存在競爭條件。
-
Double-Checked Locking (DCL): 一種試圖在保證安全性的同時最小化鎖定開銷的模式。它在進入鎖定區域前後都檢查單例指標是否為空。然而,DCL 在某些具有鬆散記憶體模型的多處理器架構上存在問題,需要使用
volatile或記憶體屏障。 -
鎖定解決方案: 最簡單安全的多執行緒方案是在
Instance函式內部使用鎖定(如 Mutex),但這會引入每次訪問的鎖定開銷。
-
Double-Checked Locking (DCL): 一種試圖在保證安全性的同時最小化鎖定開銷的模式。它在進入鎖定區域前後都檢查單例指標是否為空。然而,DCL 在某些具有鬆散記憶體模型的多處理器架構上存在問題,需要使用
-
SingletonHolder模板: 將上述各種單例實現策略(創建、生命週期、執行緒模型)抽象為策略類別,並通過SingletonHolder模板組合這些策略。SingletonHolder本身不是單例,而是提供單例行為的容器。這使得可以在編譯時選擇所需的單例特性,實現高度的靈活性和可配置性。
7. Smart Pointers (智能指針)
智能指針是模擬普通指標語法和語義的 C++ 物件,但能自動管理指標指向物件的生命週期。書中深入探討了設計智能指針的各種複雜問題。
-
價值語義: 智能指針的核心價值在於為動態分配的物件提供類似值型別的語義,特別是所有權管理,避免了手動
delete的麻煩和潛在錯誤。 -
策略驅動設計: 智能指針的許多特性(如所有權策略、檢查策略、轉換策略、存儲策略)都不是唯一的最佳方案,而是取決於具體需求。因此,將這些特性抽象為策略是實現通用智能指針的關鍵。
SmartPtr模板就是通過策略組合來實現的。 -
所有權管理策略 (Ownership-Handling Strategies): 這是智能指針最核心的功能,常見策略包括:
-
深度複製 (Deep Copy): 複製智能指針時,同時複製指向的物件。通常需要被指向物件提供虛擬複製方法(如
Clone)來避免切片問題。 - 複製寫入 (Copy on Write, COW): 優化策略,延遲複製直到物件被修改。智能指針層面難以有效實現,更適合在物件內部實現。
-
引用計數 (Reference Counting): 記錄指向同一個物件的智能指針數量,數量為零時自動刪除物件。非侵入式實現需要額外儲存引用計數器,侵入式實現則將計數器放在被指向物件內部。Loki 的
RefCounted使用小型物件分配器來優化非侵入式實現的性能。多執行緒環境需要原子操作 (RefCountedMT)。 - 引用鏈接 (Reference Linking): 將指向同一個物件的智能指針組織成雙向鏈表。列表為空時刪除物件。避免了自由存儲分配,但在每個智能指針中需要額外儲存前後指標。多執行緒環境需要鎖定。
-
破壞性複製 (Destructive Copy): 複製時將所有權轉移給目標,源智能指針變為空。典型代表是
std::auto_ptr。簡單高效,但不符合標準值語義,不能用於標準容器。
-
深度複製 (Deep Copy): 複製智能指針時,同時複製指向的物件。通常需要被指向物件提供虛擬複製方法(如
-
指標語法模擬:
-
operator->和operator*: 實現這兩個運算子是智能指針的基礎。operator->的遞歸特性允許智能指針層疊,實現如自動鎖定等功能。 -
operator&(地址運算子): 不建議重載,因為暴露內部指標會破壞智能指針的所有權管理,並與泛型程式碼衝突。
-
-
隱式轉換:
-
到原始指標 (operator T*): 方便但危險(可能導致重複
delete)。可以通過增加對void*的隱式轉換來製造歧義,阻止意外的delete。Loki 的SmartPtr通過策略控制是否允許這種隱式轉換。 -
到布林 (operator bool): 方便測試(如
if (sp))但可能導致意外的算術運算。Loki 的SmartPtr避免使用布林轉換,而是提供operator!或通過一個私有內部類別和轉換到該類別的指標來安全地支持if (sp)。
-
到原始指標 (operator T*): 方便但危險(可能導致重複
-
比較運算:
-
相等/不等 (
==,!=): 需要為與原始指標、不同型別智能指針的比較提供適當的重載,避免歧義。 -
順序 (
<,<=,>,>=): 通常不需要為任意智能指針提供順序比較,因為只有指向同一個陣列元素的指標才有明確的順序。Loki 的SmartPtr不默認實現順序運算子,但特化std::less以支持在基於指標地址的容器中使用。
-
相等/不等 (
-
檢查與錯誤報告: 智能指針可以實現初始化時檢查空指標、解引用前檢查空指標等功能。錯誤報告通常通過拋出異常實現。檢查行為通過策略 (
CheckingPolicy) 控制,允許不同等級的安全性和性能權衡。 -
const正確性: 智能指針需要支持指向const物件和const智能指針本身。 -
數組: 智能指針可以通過所有權策略支持
delete[],但不默認提供指標算術或operator[],因為std::vector通常是更好的選擇。 -
多執行緒:
-
物件層面: 可以通過智能指針層疊 (
LockedStorage策略) 在訪問物件時自動加鎖。 -
簿記數據層面: 引用計數和引用鏈接的內部數據(計數器或前後指標)是共享的,需要在複製、賦值、析構時進行原子操作或鎖定。Loki 的
RefCountedMT實現了引用計數的執行緒安全。
-
物件層面: 可以通過智能指針層疊 (
8. Object Factories (物件工廠)
物件工廠解決了在 C++ 中動態創建多型物件的需求,特別是在不了解具體型別或型別信息以非 C++ 型別形式存在時。
-
需求場景: 框架需要創建用戶定義的物件(如文檔編輯器中的
DocumentManager::NewDocument),或從持久化存儲(如文件)中根據類型標識符重建物件。C++ 中沒有虛擬構造函數,new運算符需要編譯時確定的類型名稱,這導致創建代碼的靜態綁定和依賴問題。 - 類型與值: 解釋了 C++ 中類別(編譯時實體)與對象(運行時實體)的分離,以及這如何使得直接將運行時類型信息(如字符串)轉換為對象創建成為難題。工廠模式通過類型標識符作為中間橋樑。
-
基本實現: 介紹了使用類型標識符(如整數或字符串)和回調函數(創建具體對象的函數指針)以及
std::map來實現可擴展的物件工廠。每個具體類別註冊一個創建函數及其對應的標識符。這比傳統的基於switch語句的工廠更具擴展性,將創建責任分散到各個具體類別。 -
泛化: 將工廠的關鍵元素抽象為模板參數,創建了一個通用的
Factory模板:Factory<AbstractProduct, IdentifierType, ProductCreator, FactoryErrorPolicy>。-
AbstractProduct: 抽象產品基類。 -
IdentifierType: 類型標識符的類型(需可排序,如整數或字符串)。 -
ProductCreator: 創建具體對象的可調用實體類型(默認為函數指針,也可使用Functor)。 -
FactoryErrorPolicy: 處理未知類型標識符的策略(如拋出異常、返回空)。
-
-
細節: 討論了使用
AssocVector優化內部映射表的性能。 -
克隆工廠 (CloneFactory): 解決了複製多型物件的問題,特別是針對無法修改的類別層次(即基類沒有提供虛擬
Clone方法)。它接收一個指向抽象產品的指針作為輸入(類型標識符),通過typeid獲取運行時類型信息,在內部映射表中查找對應的克隆創建器(通常是接受基類指針並返回基類指針的函數或函子)。Loki 的CloneFactory使用TypeInfo包裝std::type_info作為鍵值。 -
與其他組件結合: 演示了如何將
Factory與SingletonHolder和Functor等其他 Loki 組件結合使用,以實現更複雜且可配置的工廠單例。
9. Abstract Factory (抽象工廠)
抽象工廠是一種設計模式,為創建一個相關或依賴物件族的介面。它確保在整個系統中創建正確的具體物件組合,例如確保一個 Funky 按鈕只出現在 Funky 對話框上。
- 架構角色: 通過定義一個抽象工廠基類,其中包含為每個產品類型創建物件的純虛擬函式,並為每個產品族提供具體工廠實作,來強制執行物件族的一致性。但這種傳統方法依賴於所有抽象產品的定義,且具體工廠依賴於所有具體產品,存在依賴問題。
-
通用介面: 結合型別列表 (
Typelist) 和GenScatterHierarchy,可以自動生成抽象工廠介面。AbstractFactory<TList, Unit>會繼承Unit模板應用於TList中每個型別的結果。AbstractFactoryUnit<T>定義了創建單個產品T的純虛擬函式。這使得介面高度細粒度化,可以將工廠的子單元傳遞給不同模塊,降低耦合。 -
實作抽象工廠: 結合
GenLinearHierarchy和工廠單元策略 (FactoryUnit),可以自動生成具體工廠的實作。ConcreteFactory<AbstractFact, FactoryUnit, TList>接收抽象工廠介面、創建策略和具體產品型別列表。FactoryUnit策略(如OpNewFactoryUnit)負責實作創建邏輯,並通過協變返回型別返回具體產品指標。ConcreteFactory逆轉具體產品型別列表傳給GenLinearHierarchy,以匹配函式簽名。 -
基於原型的抽象工廠: 介紹另一種創建策略
PrototypeFactoryUnit,它通過克隆原型物件來創建產品。這減少了具體工廠對具體產品型別的直接依賴。與ConcreteFactory結合時,只需提供抽象產品型別列表即可。
這種通用方法將抽象工廠介面和實作的重複性工作自動化,並通過細粒度介面設計降低了耦合。
10. Visitor (訪問者)
Visitor 模式允許在不修改類別層次結構的情況下,向其添加新的虛擬函式(操作)。它將操作從類別中分離出來,集中到訪問者類別中。其適用於類別層次穩定但操作經常變動的場景。
-
基本概念: 傳統 OO 設計中,添加新類別容易,添加新虛擬函式難。Visitor 顛倒了這個權衡,使得添加新操作容易,但添加新類別變得困難。它通過在被訪問者類別中加入一個「跳轉」的虛擬函式 (
Accept),將控制流轉移到訪問者類別中對應的Visit函式,實現操作與物件結構的分離。 -
過載與 Catch-All: 利用 C++ 的函式過載可以簡化訪問者介面,所有
Visit函式都可以同名。可以添加一個接受基類引用的Visit過載作為 Catch-All 函式,處理未知具體型別。 - 循環訪問者 (Cyclic Visitor / GoF Visitor): 最原始的 Visitor 實現,訪問者基類需要知道所有被訪問者類別的定義,被訪問者類別也需要知道訪問者基類,導致循環依賴,維護性較差,尤其在添加新的被訪問者類別時。
-
無循環訪問者 (Acyclic Visitor): Robert Martin 提出的變體,通過引入一個「稻草人」的訪問者基類和在被訪問者類的
Accept實現中使用dynamic_cast,打破了訪問者與被訪問者之間的循環依賴。維護性更好,但dynamic_cast可能會帶來運行期開銷。 -
通用實現: Loki 提供了一套通用的 Visitor 組件來自動化 Visitor 的實作,包括 Acyclic Visitor 和 GoF Visitor 的變體。
-
Acyclic Visitor 組件:
BaseVisitor(稻草人基類),Visitor<T, R>(定義Visit函式),BaseVisitable<R, CatchAll>(被訪問者基類,定義Accept介面並通過策略處理 Catch-All),以及DEFINE_VISITABLE()宏(自動生成Accept實作)。這套組件減少了使用者需要手寫的重複代碼,並將dynamic_cast邏輯移入庫中。 -
GoF Visitor 組件:
CyclicVisitor<R, TList>(結合型別列表和GenScatterHierarchy自動生成 Visit 介面),DEFINE_CYCLIC_VISITABLE()宏(自動生成Accept實作)。這提供了無dynamic_cast開銷的 Visitor,但需要手動更新型別列表並重編譯。 -
Hooking Variations: 提供策略或預定義類別來處理 Catch-All 行為(當訪問者與被訪問者不匹配時),以及非嚴格訪問(允許訪問者不實現所有
Visit重載)。
-
Acyclic Visitor 組件:
通用的 Visitor 組件使得在 C++ 中使用 Visitor 模式更加方便,減少了重複勞動和潛在錯誤,並提供了在性能和維護性之間權衡的選項。
11. Multimethods (多重方法)
多重方法(Multimethods)或多重分派(Multiple Dispatch)是一種函式分派機制,其呼叫的函式依賴於多個物件的動態型別,而不僅僅是一個物件(如 C++ 虛擬函式)。書中重點討論了兩個物件的情況(雙重分派,Double Dispatch)。C++ 原生不支援多重方法,需要通過庫來模擬實現。
- 需求: 多重方法適用於操作行為取決於多個參與對象類型的情況,例如遊戲中的碰撞處理或繪圖程序中形狀之間的互動(如計算交集)。
-
雙重分派方案:
-
暴力雙重分派 (Brute-Force Double Switch-on-Type): 最直接的方案是使用嵌套的
if-else語句和dynamic_cast來判斷兩個對象的動態類型,然後呼叫對應的處理函式。簡單但代碼量大、依賴性高(需要知道所有類別及其繼承關係),且if語句的順序敏感,難以維護。 -
自動化暴力分派: 利用型別列表 (
Typelist) 和編譯期元編程 (StaticDispatcher) 自動生成if-else嵌套結構。使用者提供包含類別列表的型別列表,StaticDispatcher自動生成類型判斷和呼叫處理者的代碼。它可以處理類別層次的繼承關係(通過DerivedToFront)並自動排序列別。處理者通過重載的Fire函式提供具體實現。 -
對數雙重分派 (Logarithmic Double Dispatcher): 使用映射表(如
AssocVector,基於有序向量)來儲存 (TypeInfo1, TypeInfo2) 到處理者(函式或函子)的映射。通過typeid獲取運行時類型信息,進行對數時間的查找。相較於暴力分派,其代碼量小、依賴性低(只需依賴typeid和處理者),但查找速度較慢。它需要小心處理繼承關係(可能需要手動註冊所有基類組合)和處理者參數的向下轉型。可以使用本地結構體和函式指針特化作為跳板(Trampoline)來自動化參數轉型。 - 常數時間多重分派 (Constant-Time Multimethods): 通過在被分派的類別中引入一個虛擬函式和一個靜態索引成員,實現常數時間的分派。每個類別在初始化時獲得一個唯一的整數索引,這些索引用於索引一個二維矩陣,其中儲存著處理者。這是速度最快的方案,但侵入性最強(需要修改被分派的類別)。
-
暴力雙重分派 (Brute-Force Double Switch-on-Type): 最直接的方案是使用嵌套的
-
高層設施: 在基礎分派器之上,可以構建更方便使用的層:
-
自動化轉型: 使用跳板函式或特殊的函子適配器,自動將基類引用轉型為具體類型引用,減少使用者手寫
dynamic_cast。 - 對稱性: 對於操作順序不敏感的雙重分派(如碰撞),提供機制自動註冊 (T1, T2) 和 (T2, T1) 的處理者,並在呼叫時自動交換參數順序。FnDispatcher 和 FunctorDispatcher 支持此功能。
-
轉型策略 (CastingPolicy): 允許用戶指定是使用
dynamic_cast(安全,處理虛擬繼承和歧義) 還是static_cast(速度快,但對複雜繼承無效) 進行參數轉型。
-
自動化轉型: 使用跳板函式或特殊的函子適配器,自動將基類引用轉型為具體類型引用,減少使用者手寫
- 通用性: 文中討論的技術可以推廣到三個或更多對象的多重分派,但實作會更複雜。Loki 主要提供了雙重分派的組件。
Appendix A. A Minimalist Multithreading Library (極簡多執行緒庫)
附錄簡要介紹了多執行緒的概念、挑戰以及 Loki 為實現執行緒安全組件而提供的最小同步機制。
- 執行緒問題: 多執行緒允許多個執行點同時運行(在多處理器上)或交替運行(在單處理器上),這可以提高資源利用率。但共享資源(如記憶體中的變數)在多執行緒環境下存在競爭條件,導致數據損壞或未定義行為。即使簡單的整數增量也不是原子的讀-修改-寫操作。
-
原子操作: 許多平台提供原子操作(如原子增量、原子賦值),這些操作保證在執行時不會被其他執行緒中斷。Loki 通過
ThreadingModel<T>::IntType和AtomicAdd等靜態函式來抽象這些平台相關的原子操作。 -
互斥鎖 (Mutexes): 互斥鎖是一種基本的同步原語,確保在任何時候只有一個執行緒可以訪問被保護的共享資源。執行緒在訪問資源前先獲取鎖,訪問後釋放鎖。Loki 通過
ThreadingModel<T>::Lock類別來包裝互斥鎖的使用,利用 RAII(資源獲取即初始化)確保鎖的正確獲取和釋放(即使在異常發生時)。 -
鎖定語義: Loki 定義了兩種常見的物件鎖定策略:
- 物件級鎖定 (Object-Level Locking): 每個物件包含自己的互斥鎖。提供細粒度的並行性,但每個物件的空間開銷較大。
- 類別級鎖定 (Class-Level Locking): 同一類別的所有物件共享一個靜態互斥鎖。空間開銷小,但限制了同一類別不同物件的並行訪問。
-
ThreadingModel策略: Loki 將執行緒支持抽象為ThreadingModel策略。ThreadingModel<T>是一個模板,T是需要執行緒支持的類別。它提供IntType和Lock等內嵌定義。Loki 提供了SingleThreaded,ObjectLevelLockable,ClassLevelLockable等ThreadingModel的實現。通過讓類別繼承一個ThreadingModel實現,可以方便地為類別添加執行緒安全支持。 -
可選
volatile:ThreadingModel<T>::VolatileType根據選定的執行緒模型,為T增加或移除volatile修飾符,以提示編譯器關於變數的可見性。
這些同步原語和策略是 Loki 其他組件(如智能指針、單例、小型物件分配器)實現執行緒安全的基礎。
comments
comments for this post are closed