现代嵌入式C++教程:std::variant

📅 发布时间:2026/7/8 9:48:51 👁️ 浏览次数:
现代嵌入式C++教程:std::variant
现代嵌入式C教程std::variant写这篇文章前请想像一个场景你有一个盒子有时候装int有时候装std::string有时候装别的东西。传统union是那种老派盒子——节省空间但没有标签容易把int当成std::string来读后果就是 undefined behavior。std::variant就是现代的智能盒子带标签、会照顾持有对象的构造/析构并且在你试图错误读取时会大声给你抛出异常。基本概念std::variantTs...表示“在一时刻仅持有Ts...中某一个类型”的类型安全联合体。它记录当前持有哪一个类型构造/析构正确处理并且提供访问、检查和访问者visitor机制。最简单的例子#includevariant#includestring#includeiostreamintmain(){std::variantint,std::stringv;// 默认构造持有 int索引0v42;// 现在持有 intstd::coutstd::getint(v)\n;// 42vstd::string(hello variant);// 现在持有 std::string// 安全访问if(std::holds_alternativestd::string(v)){std::coutstd::getstd::string(v)\n;}// 推荐方式使用 std::visit统一处理所有可能类型std::visit([](autox){std::coutx\n;},v);}默认构造会构造第一个备选类型上例中是int。std::getT(v)尝试以T访问若v当前不是T会抛std::bad_variant_access。std::holds_alternativeT(v)检查当前类型是否为T。std::visit(visitor, v)由 visitor可调用对象处理当前持有的值是最推荐也最安全的访问方式。为什么优于裸unionstd::variant自动管理对象生命周期会调用析构函数在访问时进行类型检查抛异常而不是 UB并与std::visit配合能写出清晰的模式匹配风格代码。它也比std::any更“精确”——std::any可以装任意类型但类型信息查找不如variant明确且没有visit的静态分支优势。推荐访问方式std::visit与重载集合std::visit是variant的中心舞台。要同时处理多种类型一个常见用法是利用“重载集”技巧来把多个 lambda 拼成一个可调用对象#includevariant#includeiostream#includestringtemplateclass...Tsstructoverloaded:Ts...{usingTs::operator()...;};templateclass...Tsoverloaded(Ts...)-overloadedTs...;// C17 方便的模板推导intmain(){std::variantint,double,std::stringv3.14;std::visit(overloaded{[](inti){std::coutint: i\n;},[](doubled){std::coutdouble: d\n;},[](conststd::strings){std::coutstring: s\n;}},v);}好处是清晰、类型安全编译器会在你漏写某个类型时给出提示视情况而定读代码的人也能一眼看出每个分支要干什么。常见误区与坑务必读不要用std::getT在不确定类型时访问—— 它会抛std::bad_variant_access。用std::get_ifT或std::visit更安全。默认构造选第一个类型——std::variantint, std::string v;会构造int值为 0不是空的。需要“空值”语义可用std::monostate作为第一种类型。不能直接做递归variant——std::variantint, std::variant...会引起不完整类型问题。用std::unique_ptr或std::shared_ptr包装递归类型structNode{std::variantint,std::unique_ptrNodenext;};引用类型问题——std::variantint, double是可写的但容易错。通常用std::reference_wrapperT或保存指针更明确。异常与析构—— 如果替换当前持有类型时新类型的构造抛异常variant保证要么保持原值要么变成valueless_by_exception可以用v.valueless_by_exception()检查。此状态下std::visit将抛出bad_variant_access。大小内存——variant的大小由最“宽”类型决定加上一点元数据有时比单个类型要大。权衡内存与便利性。进阶技巧安全获取指针std::get_ifT(v)返回T*或nullptr避免异常。按索引访问std::get0(v)直接按索引不推荐容易错位但在模板编程中有用。in-place 构造v.emplacestd::string(hello)可以避免拷贝/临时并直接在variant内构造目标类型。元编程查看类型std::variant_size_vdecltype(v)std::variant_alternative_tI, decltype(v)。处理无值状态调用v.valueless_by_exception()检查是否处于异常导致的无值状态极少见但处理代码中可以考虑。noexcept/移动语义variant的移动/拷贝/析构是否noexcept取决于备选类型的对应操作是否noexcept。注意移动/拷贝开销可能来自某个昂贵备选类型。把std::variant用到项目里去假设你在处理一个消息队列消息有三种心跳Heartbeat、文本Text和二进制Blob。std::variant非常适合#includevariant#includestring#includevector#includeiostreamstructHeartbeat{intid;};structText{std::string s;};structBlob{std::vectoruint8_tdata;};usingMessagestd::variantHeartbeat,Text,Blob;voidprocess(Messageconstm){std::visit(overloaded{[](Heartbeatconsth){std::coutHB h.id\n;},[](Textconstt){std::coutText: t.s\n;},[](Blobconstb){std::coutBlob size: b.data.size()\n;}},m);}优势是清晰、类型安全如果增加一种消息类型你需要在visit处显式处理这通常是好事。std::variant与std::any、boost::variant的比较std::variant类型列表固定、编译时已知、支持visit最适合有限且已枚举的类型集合。std::any可装任意类型运行时类型检查繁琐不适合做“有穷多种可能”的替代。boost::variantstd::variant的前身功能类似历史项目可能还在用但新代码优先选标准库的std::variant。