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