合)
訪問者Visitor模式想解決的是有一族結構相對穩(wěn)定的類型上面要掛多種易變的操作求值、打印、序列化……又不想在每個類里無限加成員函數(shù)也不想用一長串if (dynamic_cast...)。std::variant把「只能是這幾種之一」寫進類型系統(tǒng)std::visit在運行期根據(jù)當前激活的是哪一種調(diào)用訪問者里對應的分支——語義上就是Visitor 分派但常常不需要公共基類和虛函數(shù)表。本文以設計模式視角串起variant/visit/monostate/ 常見坑文末用一小節(jié)點到與 CRTP 筆記 里「異構容器」話題的弱關聯(lián)不是主線。1. 訪問者模式在解決什么兩類變化維度維度典型變化設計上的期望元素類型結構新增一種 AST 結點、一種協(xié)議消息體相對穩(wěn)定或「閉集」可枚舉對元素的操作新增一種 pass、一種校驗、一種 dump更頻繁希望集中寫在一處訪問者的意圖把「對每種類型的操作」收攏到一個訪問者對象里通過分派把「當前到底是哪種元素」與「要執(zhí)行哪段邏輯」接起來經(jīng)典寫法里常叫二次分派。2. 經(jīng)典 OO 訪問者 vsvariantvisit2.1 經(jīng)典寫法示意每個具體元素實現(xiàn)accept(Visitor)內(nèi)部回調(diào)visitor.visit(*this)訪問者類里 overload 一組visit(const ConcreteA),visit(const ConcreteB)……優(yōu)點類型開放——以后可再派生子類配合虛函數(shù)。代價繼承層次、vptr、文件分散新增一種元素類型要改多個類的visit重載。2.2variantvisit閉集訪問者若你能明確說元素只會是A | B | C這幾種閉集可以把元素建模為std::variantA,B,Celem;對「某一種操作」寫成一個可調(diào)用對象函數(shù)對象 / lambda 集交給std::visitstd::visit(my_visitor,elem);語義仍是「帶著操作走遍各種具體類型」——Visitor 的形態(tài)。實現(xiàn)分派由標準庫實現(xiàn)可生成跳轉(zhuǎn)表等不必手寫accept也不必公共基類。代價類型集寫死在variantA,B,...里新增一種類型要改variant模板參數(shù)并重新編譯所有visit調(diào)用點閉集的天然限制。3.std::variant基礎3.1 定義與賦值#includevariant#includestring#includeiostreamstructCircle{doubler;};structRectangle{doublew,h;};intmain(){std::variantCircle,Rectangle,intv;vCircle{1.0};// 當前持有 CirclevRectangle{2,3};// 切換為 Rectanglev42;// 第三種 alternativeintstd::coutstd::holds_alternativeint(v)\n;// 1}同一時間只有一個alternative 是「激活」的。賦值會銷毀舊值、構造新值可能拋異常見 §7。3.2std::monostate第一個類型不能默認構造時variant默認構造時會默認構造第一個類型。若第一個類型沒有默認構造函數(shù)用std::monostate占位作為第一個模板參數(shù)structNoDefault{explicitNoDefault(int){}};usingVstd::variantstd::monostate,NoDefault,int;// V v; // 默認構造 monostate表示「尚未放置有效值」這在「先聲明、后寫入」的解析器、狀態(tài)機里很常見。4.std::visit把「訪問者」交給標準庫4.1 手寫訪問者類C17 起即可最清晰#includevariant#includeiostreamstructCircle{doubler;};structRectangle{doublew,h;};structAreaVisitor{doubleoperator()(constCirclec)const{return3.1415926*c.r*c.r;}doubleoperator()(constRectangler)const{returnr.w*r.h;}};intmain(){std::variantCircle,RectanglevCircle{2.0};std::coutstd::visit(AreaVisitor{},v)\n;// 約 12.56}AreaVisitor就是典型的Visitor每種operator()對應一種元素類型。4.2 多variant一起 visitstd::visit可以接受多個variant訪問者需要能處理笛卡爾積里每一種組合或模板化常用于「兩個聯(lián)合類型配對處理」// 偽代碼示意visit(visitor, v1, v2);// visitor 需對 (v1 的每種) × (v2 的每種) 有對應調(diào)用 —— 組合爆炸時要謹慎設計實際工程里更常見的是visit一個variant或把多域數(shù)據(jù)放進一個struct再variant...。4.3overload慣用法C20lambda 包成一塊「訪問者」把多個 lambda 合成一個可調(diào)用對象省去寫具名 struct#includevariant#includeiostream#includestring// C20變參繼承 推導指引templateclass...Tsstructoverloaded:Ts...{usingTs::operator()...;};templateclass...Tsoverloaded(Ts...)-overloadedTs...;intmain(){std::variantint,std::stringvhello;std::visit(overloaded{[](intx){std::coutint x\n;},[](conststd::strings){std::coutstr s\n;},},v);}C17沒有上述推導指引時仍推薦手寫struct訪問者或顯式指定overloaded...的模板實參可讀性往往好于強行塞宏。5. 取值與檢查get/get_if/holds_alternative/indexstd::variantint,std::stringv7;if(autopstd::get_ifint(v)){std::cout*p\n;// 安全不是 int 則 p nullptr}try{autosstd::getstd::string(v);// 類型不對則拋 std::bad_variant_access}catch(conststd::bad_variant_access){// ...}switch(v.index()){// 0 int, 1 stringcase0:/* ... */break;case1:/* ... */break;}能visit就優(yōu)先visit新增 alternative 時編譯器會逼你補全訪問者分支若用overload全覆蓋。用index()switch容易漏 case或與模板參數(shù)順序強耦合維護成本高。6. 與「虛函數(shù) Visitor」的對照維度虛接口 經(jīng)典 Visitorvariantvisit類型集開放可再派生閉集寫死在variant...分派vtable標準庫實現(xiàn)跳轉(zhuǎn)表 / 鏈式 if 等由實現(xiàn)定義堆與緩存常配合unique_ptrBase對象可內(nèi)聯(lián)在variant存儲里新增操作給 Visitor 加接口所有元素類配合accept給visit的訪問者加重載 / lambda新增類型派生 實現(xiàn)accept 各 Visitor overload改variant模板參數(shù) 所有visit分支選型一句話閉集、值語義、少堆分配 →variantvisit要插件式擴展類型、二進制 ABI 邊界 →虛函數(shù)體系更合適。7. 常見坑7.1bad_variant_access對錯誤的std::getT/std::getI會拋std::bad_variant_access。get_if指針版不拋。7.2valueless_by_exception若在賦值/emplace過程中拋出異常且無法保證舊值恢復variant可能進入valueless狀態(tài)此時index()為variant_npos多數(shù)操作未定義行為前應檢查v.valueless_by_exception()。7.3 自賦值與大型對象variant里放大對象時賦值可能觸發(fā)分配/搬移熱點路徑可評估是否用unique_ptrT作 alternative 減拷貝成本權衡堆分配。7.4visit的可調(diào)用對象必須無歧義覆蓋所有類型overload里若兩個 lambda 都能匹配同一類型會編譯失敗與 Visitor 里重載決議規(guī)則一致。8. 與其它現(xiàn)代特性的組合點到為止std::optional/std::expected常和「解析結果」一起出現(xiàn)不要用optionalvariant...盲目套娃——有時variantMonostate, A, B或expectedvariant..., E更清晰。C26 模式匹配inspect/match若標準化落地會是visit的語法糖方向當前生產(chǎn)環(huán)境仍以visitoverload為主流。9. 和CRTP 筆記 的關系弱交叉非主線crtp.md 在討論「編譯期多態(tài)接不住運行期異構容器」時把variantvisit列為異構方案之一與虛函數(shù)、手寫類型擦除并列。那里的重點是CRTP 解決的是另一類問題variant不是 CRTP 的「替代品」而是當你明確只有閉集類型、又想在vector里值語義存放時的一種訪問者式實現(xiàn)路徑。主線仍應是Visitor 意圖 →variant建模閉集 →visit寫操作。與 CRTP 的交叉保持「知道去哪延伸閱讀」即可。10. 一句話總結訪問者把「對多類型的操作」聚到一處通過分派連接類型與邏輯。std::variant用類型系統(tǒng)表達閉集聯(lián)合std::visit對該聯(lián)合做類型安全的多路分派。與經(jīng)典虛 Visitor互補閉集 值語義優(yōu)先variant開放擴展優(yōu)先虛接口。優(yōu)先visit而非裸index()switch更利于窮舉檢查與維護。記住variant回答「是哪一種」visit回答「對這一種做什么」——合在一起就是現(xiàn)代 C 里的訪問者骨架。