能裝任何類(lèi)型的容器)
std::any是 C17 引入的「萬(wàn)能容器」能在運(yùn)行時(shí)持有任意可拷貝構(gòu)造的類(lèi)型靠any_cast取出。它適合「類(lèi)型在編譯期不確定、要到運(yùn)行時(shí)才知道」的場(chǎng)景比如配置系統(tǒng)、消息隊(duì)列、插件接口。代價(jià)是堆分配 類(lèi)型擦除 取值必須知道確切類(lèi)型。多數(shù)時(shí)候std::variant是更好的選擇 —— 這篇會(huì)講清楚怎么選。1. 引子函數(shù)返回值類(lèi)型不確定怎么辦設(shè)想一個(gè)場(chǎng)景你要寫(xiě)一個(gè)配置讀取函數(shù)配置項(xiàng)可能是int、std::string、double…… 返回類(lèi)型怎么寫(xiě)傳統(tǒng)寫(xiě)法會(huì)撞墻// 問(wèn)題返回值類(lèi)型不確定int / string / double 都可能是 // ??? getConfig(timeout) // 到底返回什么類(lèi)型C17 之前這類(lèi)需求要么用繼承 虛函數(shù)Base*指針要么用void* 手寫(xiě)類(lèi)型標(biāo)記要么套模板。前兩者類(lèi)型不安全后者把類(lèi)型問(wèn)題推給了調(diào)用方。std::any就是為「編譯期不知道類(lèi)型運(yùn)行時(shí)才知道」準(zhǔn)備的??磦€(gè)最小例子#include any #include cstdio #include string // 一個(gè)「能裝任何類(lèi)型」的配置項(xiàng)讀取器 std::any getConfig(const std::string key) { if (key timeout) return 30; // 裝 int if (key server) return std::string(api.example.com); // 裝 string if (key ratio) return 0.75; // 裝 double return {}; // 空 any表示「沒(méi)有這項(xiàng)配置」 } int main() { std::any a getConfig(timeout); printf(timeout %d\n, std::any_castint(a)); // 用 any_cast 取出 std::any b getConfig(server); printf(server %s\n, std::any_caststd::string(b).c_str()); std::any c getConfig(nonexistent); printf(c 有值嗎%s\n, c.has_value() ? 有 : 沒(méi)有); }timeout 30 server api.example.com c 有值嗎沒(méi)有官方文檔std::any這個(gè)例子展示了any的四個(gè)核心操作賦值塞值、any_cast取值、has_value()判空、空any。下面逐個(gè)拆開(kāi)講。2. 核心概念any 到底是什么一句話(huà)std::any是一個(gè)類(lèi)型擦除type erasure容器它把值的具體類(lèi)型藏起來(lái)只保留「值本身」和「取出時(shí)用來(lái)校驗(yàn)類(lèi)型的typeid」。內(nèi)存布局大致是這樣概念圖非標(biāo)準(zhǔn)實(shí)現(xiàn)std::any 對(duì)象棧上約 16~24 字節(jié) ┌─────────────────────────────┐ │ 指向堆上真正數(shù)據(jù)的指針 ────┼──? 堆上存儲(chǔ)管理器 實(shí)際的值 └─────────────────────────────┘ 小對(duì)象可能用 SBO 優(yōu)化直接內(nèi)嵌關(guān)鍵約束放進(jìn)any的類(lèi)型必須是可拷貝構(gòu)造的CopyConstructible。否則編譯報(bào)錯(cuò)。#include any #include memory #include string int main() { std::any a 42; // ? int 可拷貝 std::any b std::string(hello); // ? string 可拷貝 // 下面這行會(huì)編譯失敗unique_ptr 不可拷貝 // std::any c std::make_uniqueint(42); // ? 編譯錯(cuò)誤 }官方文檔std::any 的類(lèi)型要求 —— 明確要求DecayCopyConstructible3. any_cast取值與「取錯(cuò)類(lèi)型」的兩種結(jié)果any_cast是取值唯一入口有兩種形式形式取錯(cuò)類(lèi)型時(shí)的行為返回any_castT(any)拋std::bad_any_cast異常Tany_castT(any*)返回nullptr不拋異常T*第一種最常見(jiàn)但必須保證類(lèi)型對(duì)得上否則拋異常#include any #include cstdio int main() { std::any a 100; // 實(shí)際是 int printf(int 取值: %d\n, std::any_castint(a)); // ? 類(lèi)型匹配 try { double d std::any_castdouble(a); // ? 類(lèi)型不匹配 printf(double %f\n, d); } catch (const std::bad_any_cast e) { printf(取錯(cuò)類(lèi)型拋出了 bad_any_cast: %s\n, e.what()); } }int 取值: 100 取錯(cuò)類(lèi)型拋出了 bad_any_cast: bad any_cast注意any_castdouble不會(huì)做int → double的隱式轉(zhuǎn)換必須類(lèi)型完全一致。想安全地試探類(lèi)型用指針形式#include any #include cstdio #include string int main() { std::any a std::string(hi); // 指針形式類(lèi)型不對(duì)返回 nullptr不拋異常 if (auto p std::any_caststd::string(a)) { printf(是 string: %s\n, p-c_str()); } if (std::any_castint(a) nullptr) { printf(不是 int返回了 nullptr\n); } }是 string: hi 不是 int返回了 nullptr官方文檔std::any_cast4. 查類(lèi)型與判空type() 和 has_value()has_value()判斷any是否為空默認(rèn)構(gòu)造或賦值了空any就是空type()返回const std::type_info用于運(yùn)行時(shí)判斷「裝的是不是某類(lèi)型」#include any #include cstdio #include string int main() { std::any a; // 默認(rèn)構(gòu)造空 printf(空 any: has_value%s\n, a.has_value() ? true : false); a 3.14; printf(賦值后: has_value%s\n, a.has_value() ? true : false); // type() 返回 type_info可和 typeid 比較 if (a.type() typeid(double)) { printf(a 的類(lèi)型是 double\n); } if (a.type() ! typeid(int)) { printf(a 的類(lèi)型不是 int\n); } // 清空reset() 或賦值空 any a.reset(); printf(reset 后: has_value%s\n, a.has_value() ? true : false); }空 any: has_valuefalse 賦值后: has_valuetrue a 的類(lèi)型是 double a 的類(lèi)型不是 int reset 后: has_valuefalse官方文檔std::any::type、std::any::has_value5. 傳值語(yǔ)義any 會(huì)拷貝還可能觸發(fā)深拷貝這是最容易被忽略的坑。std::any是值語(yǔ)義value semantics拷貝一個(gè)any會(huì)連里面的值一起拷貝。#include any #include cstdio #include string int main() { std::any a std::string(original); std::any b a; // 拷貝b 里的 string 是 a 的獨(dú)立副本 // 修改 b 里的 string不影響 a std::string sb std::any_caststd::string(b); sb changed; printf(a %s\n, std::any_caststd::string(a).c_str()); printf(b %s\n, std::any_caststd::string(b).c_str()); }a original b changed如果裝的類(lèi)型拷貝很貴比如大vector拷貝any就會(huì)很貴。想避免拷貝就傳引用或指針或者干脆別用any裝大對(duì)象。官方文檔std::any 的復(fù)制語(yǔ)義6. 高頻用法拿「類(lèi)型標(biāo)簽」實(shí)現(xiàn)類(lèi)型安全的下發(fā)any最實(shí)用的場(chǎng)景之一生產(chǎn)者往any里塞值消費(fèi)者用type()或any_cast判斷類(lèi)型再處理。比如一個(gè)消息分發(fā)器#include any #include cstdio #include string #include vector // 分發(fā)器根據(jù) any 里實(shí)際裝的類(lèi)型走不同分支 void dispatch(const std::any msg) { if (msg.type() typeid(int)) { printf(收到 int: %d\n, std::any_castint(msg)); } else if (msg.type() typeid(std::string)) { printf(收到 string: %s\n, std::any_caststd::string(msg).c_str()); } else if (msg.type() typeid(std::vectorint)) { auto v std::any_caststd::vectorint(msg); printf(收到 vector大小 %zu首元素 %d\n, v.size(), v[0]); } else { printf(未知類(lèi)型\n); } } int main() { dispatch(42); dispatch(std::string(hello)); dispatch(std::vectorint{1, 2, 3}); }收到 int: 42 收到 string: hello 收到 vector大小 3首元素 1官方文檔std::type_info7. any 到底開(kāi)銷(xiāo)在哪為什么別濫用any不是免費(fèi)的。兩個(gè)主要開(kāi)銷(xiāo)堆分配heap allocation值通常存在堆上除非實(shí)現(xiàn)做了小對(duì)象優(yōu)化Small Buffer Optimization類(lèi)型擦除的運(yùn)行時(shí)開(kāi)銷(xiāo)any_cast要做一次typeid比較取出后還有一次轉(zhuǎn)換對(duì)比表格特性std::anystd::variantT...std::optionalT能裝的類(lèi)型任意運(yùn)行時(shí)任意切換編譯期固定的一組單個(gè)T或空類(lèi)型集合確定時(shí)機(jī)運(yùn)行時(shí)編譯期編譯期取值是否需知道類(lèi)型必須知道確切類(lèi)型用std::get/visit直接*解引用類(lèi)型安全弱取錯(cuò)拋異常強(qiáng)編譯期枚舉強(qiáng)存儲(chǔ)通常堆分配棧上大小max 成員棧上適用場(chǎng)景插件、異構(gòu)容器、類(lèi)型完全未知類(lèi)型集合已知且有限單一「可能沒(méi)有」的值結(jié)論類(lèi)型集合編譯期已知→ 用variant單一「可能沒(méi)有的值」→ 用optional只有類(lèi)型真的到運(yùn)行時(shí)才確定時(shí)才輪到any。8. 完整示例一個(gè)用 any 實(shí)現(xiàn)的迷你配置中心把前面所有點(diǎn)串起來(lái)#include any #include cstdio #include map #include string class Config { std::mapstd::string, std::any items_; public: // 設(shè)置任意類(lèi)型的配置項(xiàng) template typename T void set(const std::string key, T value) { items_[key] std::any(std::move(value)); } // 按類(lèi)型讀取取不到或類(lèi)型不對(duì)返回 false template typename T bool get(const std::string key, T out) const { auto it items_.find(key); if (it items_.end()) return false; // 沒(méi)有這個(gè) key if (it-second.type() ! typeid(T)) return false; // 類(lèi)型對(duì)不上 out std::any_castT(it-second); return true; } }; int main() { Config cfg; cfg.set(timeout, 30); cfg.set(server, std::string(api.example.com)); cfg.set(ratio, 0.75); int timeout 0; std::string server; double ratio 0.0; if (cfg.get(timeout, timeout)) printf(timeout %d\n, timeout); if (cfg.get(server, server)) printf(server %s\n, server.c_str()); if (cfg.get(ratio, ratio)) printf(ratio %.2f\n, ratio); // 類(lèi)型故意讀錯(cuò)應(yīng)該返回 false 而不是崩 std::string wrong; bool ok cfg.get(timeout, wrong); printf(把 timeout 當(dāng) string 讀%s\n, ok ? 讀到了 : 讀不到類(lèi)型不符); }timeout 30 server api.example.com ratio 0.75 把 timeout 當(dāng) string 讀讀不到類(lèi)型不符9. 延伸閱讀std::any — cppreference最權(quán)威的成員清單和類(lèi)型要求寫(xiě)代碼時(shí)隨手查std::variant — cppreferenceany的頭號(hào)替代品類(lèi)型集合編譯期已知時(shí)用它std::bad_any_cast — cppreferenceany_cast失敗拋的異常了解它的繼承關(guān)系Type erasure類(lèi)型擦除— cppreferenceany背后的核心機(jī)制理解它對(duì)寫(xiě)更通用的代碼有幫助本知識(shí)庫(kù)內(nèi)的相關(guān)篇目《std::variant 完全指南類(lèi)型安全的 union 與 std::visit 用法》 —— std::variantC17是類(lèi)型安全的 union《std::optional 完全指南別再用 -1 和 nullptr 表達(dá)「沒(méi)有值」》 —— std::optionalC17表示「可能有一個(gè) T《noexcept 為什么移動(dòng)構(gòu)造必須標(biāo)從 vector 擴(kuò)容退化成拷貝說(shuō)起》 —— noexcept 有兩種用法——修飾函數(shù)和當(dāng)運(yùn)算符用它不代表「不會(huì)拋異常」10. 一句話(huà)總結(jié)std::any用「堆分配 類(lèi)型擦除」換來(lái)「運(yùn)行時(shí)裝任意類(lèi)型」的自由代價(jià)是取值必須知道確切類(lèi)型、取錯(cuò)會(huì)拋異常、還有隱藏的拷貝成本 —— 只有類(lèi)型真的到運(yùn)行時(shí)才確定時(shí)才用它否則優(yōu)先variant或optional。