ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++ std::any 完全指南:一个能装任何类型的容器

C++ std::any 完全指南:一个能装任何类型的容器 std::any是 C17 引入的「万能容器」能在运行时持有任意可拷贝构造的类型靠any_cast取出。它适合「类型在编译期不确定、要到运行时才知道」的场景比如配置系统、消息队列、插件接口。代价是堆分配 类型擦除 取值必须知道确切类型。多数时候std::variant是更好的选择 —— 这篇会讲清楚怎么选。1. 引子函数返回值类型不确定怎么办设想一个场景你要写一个配置读取函数配置项可能是int、std::string、double…… 返回类型怎么写传统写法会撞墙// 问题返回值类型不确定int / string / double 都可能是 // ??? getConfig(timeout) // 到底返回什么类型C17 之前这类需求要么用继承 虚函数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」。内存布局大致是这样概念图非标准实现std::any 对象栈上约 16~24 字节 ┌─────────────────────────────┐ │ 指向堆上真正数据的指针 ────┼──► 堆上存储管理器 实际的值 └─────────────────────────────┘ 小对象可能用 SBO 优化直接内嵌关键约束放进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 会拷贝还可能触发深拷贝这是最容易被忽略的坑。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. 高频用法拿「类型标签」实现类型安全的下发any最实用的场景之一生产者往any里塞值消费者用type()或any_cast判断类型再处理。比如一个消息分发器#include any #include cstdio #include string #include vector // 分发器根据 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值通常存在堆上除非实现做了小对象优化Small Buffer Optimization类型擦除的运行时开销any_cast要做一次typeid比较取出后还有一次转换对比表格特性std::anystd::variantT...std::optionalT能装的类型任意运行时任意切换编译期固定的一组单个T或空类型集合确定时机运行时编译期编译期取值是否需知道类型必须知道确切类型用std::get/visit直接*解引用类型安全弱取错抛异常强编译期枚举强存储通常堆分配栈上大小max 成员栈上适用场景插件、异构容器、类型完全未知类型集合已知且有限单一「可能没有」的值结论类型集合编译期已知→ 用variant单一「可能没有的值」→ 用optional只有类型真的到运行时才确定时才轮到any。8. 完整示例一个用 any 实现的迷你配置中心把前面所有点串起来#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 有两种用法——修饰函数和当运算符用它不代表「不会抛异常」10. 一句话总结std::any用「堆分配 类型擦除」换来「运行时装任意类型」的自由代价是取值必须知道确切类型、取错会抛异常、还有隐藏的拷贝成本 —— 只有类型真的到运行时才确定时才用它否则优先variant或optional。
返回列表