2026/2/17 22:29:15
网站建设
项目流程
怎么做网站门户,wordpress搬家lnmp,天河做网站哪家强,滁州公司做网站std::variant
std::variant 是 C17 标准库中加入的一个类模板#xff0c;它代表一个类型安全的联合体#xff08;union#xff09;。它可以持有其模板参数列表中指定的任何一种类型的值。我们也不叫他联合体了#xff0c;常说的便是“变体”
#x1f517; 参考#xff…std::variantstd::variant是 C17 标准库中加入的一个类模板它代表一个类型安全的联合体union。它可以持有其模板参数列表中指定的任何一种类型的值。我们也不叫他联合体了常说的便是“变体” 参考https://en.cppreference.com/w/cpp/header/variant.html传统的 Cunion不是类型安全的。我们需要自己记住当前存储的是哪种类型如果访问错了比如在一个存储int的union上读取float会导致未定义行为就好比内存里实际是int 10的二进制数据但你要求编译器按照float的格式解析这段二进制 ——float和int的二进制编码规则完全不同比如float是 IEEE 754 浮点格式int是补码解析出来的结果是无意义的 “垃圾值”而且它无法处理非平凡类型如std::string其中非平凡类型指那些有自定义构造函数 / 析构函数、自定义拷贝 / 移动语义、虚函数的类型比如std::string、std::vector、std::map等这类类型的对象需要编译器自动管理资源比如std::string会在堆上分配内存存储字符析构时释放。详细的对比就是C11 之前union 完全禁止包含非平凡类型 —— 因为 union 的构造 / 析构函数是编译器自动生成的它只会分配内存但不会调用成员的构造 / 析构函数。比如// C11 前编译报错 union BadUnion { int i; std::string s; // std::string 有自定义构造/析构/拷贝 };如果允许BadUnion的析构函数不知道该调用int还是std::string的析构函数std::string的堆内存会泄漏导致资源管理崩溃。C11 及之后允许包含非平凡类型但需要手动管理构造 / 析构极其繁琐且容易出错#include iostream #include string union StringUnion { int i; std::string s; // 空构造不初始化任何成员必须手动构造 StringUnion() {} // 空析构不销毁任何成员必须手动销毁 ~StringUnion() {} // 手动构造 std::string 成员 void init_string(const std::string str) { // 定位 new在 s 对应的内存地址上构造 std::string 对象 new (s) std::string(str); } // 手动销毁 std::string 成员 void destroy_string() { // 显式调用 std::string 的析构函数释放堆内存 s.~basic_string(); } }; int main() { // 场景1正确使用先构造、再使用、最后销毁 StringUnion u1; // 1. 手动构造 string 成员必须先构造才能访问 u1.init_string(Hello Union); // 2. 使用 string 成员 std::cout u1.s u1.s std::endl; // 输出Hello Union // 3. 手动销毁 string 成员必须销毁否则内存泄漏 u1.destroy_string(); // 场景2切换成员先销毁旧成员再构造新成员 StringUnion u2; // 先使用 int 成员int 是平凡类型无需手动构造/销毁 u2.i 100; std::cout u2.i u2.i std::endl; // 输出100 // 切换到 string 成员int 无需销毁但必须先构造 string u2.init_string(Switch to string); std::cout u2.s u2.s std::endl; // 输出Switch to string u2.destroy_string(); // 用完必须销毁 // 场景3错误示例忘记销毁导致内存泄漏 // StringUnion u3; // u3.init_string(Memory Leak); // // 忘记调用 destroy_string()std::string 的堆内存永远不会释放 // 场景4错误示例未构造就访问未定义行为 // StringUnion u4; // std::cout u4.s std::endl; // 未构造就访问程序崩溃/垃圾值 return 0; }细节点new (s) std::string(str)调用定位 new不在堆上分配新内存而是直接在s这个地址上构造std::string对象执行std::string的构造函数初始化其内部的指针、长度等成员。这种写法需要你手动记住 “当前活跃的是哪个成员”手动调用构造 / 析构 —— 不仅代码复杂还回到了 “记类型” 的问题一旦漏调用析构就会导致内存泄漏调用错了又是未定义行为。std::variant的优势是它解决了所有这些问题它知道当前存储的是哪种类型并确保对象被正确构造和析构我们可以把它想象成一个 “智能的”、“类型丰富的”union。️ 定义和赋值修改#include variant #include string #include iostream // 示例定义和赋值 int main() { // 定义一个 variant它可以存储一个 int一个 double或一个 std::string std::variantint, double, std::string v; v 42; // 现在持有 int std::cout int: std::getint(v) std::endl; v 3.14; // 现在持有 double std::cout double: std::getdouble(v) std::endl; v hello; // 现在持有 std::string std::cout string: std::getstd::string(v) std::endl; // 赋值时如果找不到对应类型的值则报错 // v std::pairint, int{}; // Error // 使用index()获取当前持有的类型索引 std::cout Current index: v.index() std::endl; std::variantstd::string, std::string v2; // v2 abc; // Error }std::variant是 C17 引入的类型安全联合体在定义时必须指定它能存储的所有类型列表且这些类型会按顺序分配索引从 0 开始。// 格式std::variant类型1, 类型2, 类型3, ... 变量名; std::variantint, double, std::string v; // 可存储int(索引0)、double(索引1)、string(索引2)定义时至少要指定一种类型空的std::variant是非法的不允许重复定义相同类型如std::variantstd::string, std::string这类定义无意义且会导致编译错误未显式赋值时std::variant会默认初始化第一个类型如上面的v初始持有值为0的int。所以我们就单纯定义的话也会调用构造所以要求第一个参数必须要有默认构造细节不要错如果都没有默认构造我们可以第一个传入一个该类提供的一个空类 --- std::monostatestd::variant支持直接赋值但只能赋值为定义时指定的类型赋值后会自动切换内部存储的类型。std::variantint, double, std::string v; // 1. 赋值为int类型索引0 v 42; std::cout int值: std::getint(v) std::endl; // 输出int值: 42 // 2. 赋值为double类型索引1 v 3.14; std::cout double值: std::getdouble(v) std::endl; // 输出double值: 3.14 // 3. 赋值为std::string类型索引2 v hello; // 字面量自动转换为std::string std::cout string值: std::getstd::string(v) std::endl; // 输出string值: hello❌ 不能赋值为定义时未指定的类型如v std::pairint, int{}会直接编译报错❌ 重复类型的std::variant如std::variantstd::string, std::string无法赋值因为编译器无法区分重复类型✅ 赋值时会自动处理类型转换如const char*字面量可赋值给std::string类型的变体。通过index()成员函数可获取当前存储类型的索引验证赋值是否成功切换类型std::variantint, double, std::string v; v hello; // 切换为string类型索引2 std::cout 当前类型索引: v.index() std::endl; // 输出当前类型索引: 2索引从 0 开始与定义时的类型顺序严格对应若std::variant处于 “空状态”如异常情况下index()会返回std::variant_npos通常是size_t最大值。 访问值1. 使用std::getT或std::getN我们可以通过类型或索引来类型模板参数/非类型模板参数直接获取值。但如果当前variant存储的不是我们请求的类型 / 索引它会抛出std::bad_variant_access异常。int main() { std::variantint, double v 42; try { std::cout std::getint(v) std::endl; std::cout std::getdouble(v) std::endl; // 抛出异常 } catch (const std::bad_variant_access e) { std::cout Error: e.what() std::endl; } }2. 使用std::get_ifTstd::get_if不会抛出异常。它接受一个指针参数如果variant当前存储的是指定类型则返回一个指向该值的指针否则返回nullptr。int main() { std::variantint, double, std::string v hello; // 使用std::get_if尝试获取值 if (auto pval std::get_ifint(v)) { std::cout int value: *pval std::endl; } else if (auto pval std::get_ifdouble(v)) { std::cout double value: *pval std::endl; } else if (auto pval std::get_ifstd::string(v)) { std::cout string value: *pval std::endl; } }3. 使用std::visit推荐最安全强大std::visit类模板允许你提供一个 “访问者”visitor来根据当前存储的类型执行相应的操作这是最类型安全、最清晰的方式。第一个参数访问者是一个可调用对象通常是一个重载了operator()的类或者使用 lambda 表达式结合overloaded技巧std::visit会把std::variant对象中存储的值取出来作为参数传给 visitor 可调用对象。所以在处理std::variant时std::visit是更现代、更安全也更强大的选择相比传统的std::get和std::get_if它在代码的健壮性、可维护性和表达力上都有明显优势。#include iomanip #include iostream #include string #include type_traits #include variant #include vector // the variant to visit using value_t std::variantint, double, std::string; struct VisitorOP { void operator()(int i) const { std::cout int: i \n; } void operator()(double d) const { std::cout double: d \n; } void operator()(const std::string s) const { std::cout string: s \n; } }; // helper type for the visitor #4 templateclass... Ts struct overloaded : Ts... { using Ts::operator()...; }; // explicit deduction guide (not needed as of C20) templateclass... Ts overloaded(Ts...) - overloadedTs...; int main() { std::vectorvalue_t vec { 10, 1.5, hello }; for (auto v : vec) { std::visit(VisitorOP(), v); } std::cout \n; for (auto v : vec) { // 1. void visitor, only called for side-effects (here, for I/O) std::visit([](auto arg) { std::cout arg; }, v); // 2. value-returning visitor, demonstrates the idiom of returning // another variant value_t w std::visit([](auto arg) - value_t { return arg arg; }, v); // 3. type-matching visitor: a lambda that handles each type // differently std::cout . After doubling, variant holds ; std::visit([](auto arg) { using T std::decay_tdecltype(arg); if constexpr (std::is_same_vT, int) std::cout int with value arg \n; else if constexpr (std::is_same_vT, double) std::cout double with value arg \n; else if constexpr (std::is_same_vT, std::string) std::cout string with value \ std::quoted(arg) \\n; else static_assert(false, non-exhaustive visitor!); }, w); } std::cout \n; for (auto v : vec) { // 4. another type-matching visitor: a class with 3 overloaded // operator()s std::visit(overloaded{ [](int arg) { std::cout int: arg ; }, [](double arg) { std::cout double: arg ; }, [](const std::string arg) { std::cout string: std::quoted(arg) ; } }, v); } }std::get/std::get_if需要你手动保证访问的类型或索引与variant当前存储的类型一致。如果类型不匹配std::get会抛出异常std::get_if会返回nullptr但这些错误都发生在运行时。std::visit编译器会强制你处理variant中所有可能的类型。如果漏掉了任何一种类型代码会在编译期就报错比如代码里的static_assert(false, non-exhaustive visitor!)从根源上杜绝了运行时错误。而且性能开销visit 是比较小的没有运行时的检查类型1. 基础背景代码的核心目标这段代码先定义了一个能存储int/double/string的variant类型value_t然后创建了包含这三种类型值的向量vec。整个程序的核心就是用std::visit遍历这个向量对每个variant里的不同类型值执行不同操作 —— 本质是 “根据variant存储的实际类型自动调用对应逻辑”。2. 第一种用法用自定义结构体做 “访问者”代码里的VisitorOP是一个结构体它重载了 3 次operator()分别处理int/double/string类型。std::visit(VisitorOP(), v)就是把v里存储的值传给VisitorOP的对象编译器会自动匹配值的类型调用对应的operator()比如v存的是int就调用处理int的那个函数最终打印对应类型和值。这是std::visit最基础的用法用 “重载函数调用运算符的类” 封装所有类型的处理逻辑。3. 第二种 第三种用法用 lambda 做访问者进阶第二段循环里第一个std::visit直接传了一个泛型 lambda[](auto arg) { std::cout arg; }因为 lambda 是泛型的能接收任意类型的参数所以可以直接处理variant里的所有类型第二个std::visit更巧妙它先返回一个新的variant把原值翻倍比如 int 10 变 20string hello 变 hellohello然后又用一个带if constexpr的泛型 lambda在编译期判断参数类型分别打印不同的提示语 —— 核心是 “用泛型 lambda 编译期判断替代结构体重载”。4. 第四种用法用overloaded组合多个 lambda最优实践代码里的overloaded是一个模板技巧它能把多个不同的 lambda “合并” 成一个对象每个 lambda 处理一种类型。std::visit(overloaded{处理int的lambda, 处理double的lambda, 处理string的lambda}, v)就会根据v的实际类型调用对应的 lambda—— 这是实际开发中最常用的写法不用写结构体直接用 lambda 组合代码更简洁。不过部分的大家呢这个会比较看不懂这里解释一下其实它是 C 里一个极其巧妙但核心简单的模板技巧这段代码的目的是把多个不同的 lambda或函数对象“合并” 成一个对象让这个对象拥有所有 lambda 的operator()重载版本这样就能用它作为std::visit的访问者匹配variant的不同类型。其实就是上面的方法就是写法的差别而已templateclass... Ts struct overloaded : Ts... { using Ts::operator()...; };拆解成 3 个关键部分templateclass... Ts这是 C11 的变参模板Ts...表示 “任意数量、任意类型的模板参数”比如传 3 个 lambdaTs就是这 3 个 lambda 的类型。struct overloaded : Ts...overloaded结构体公有继承了所有Ts里的类型也就是继承了所有传入的 lambda。lambda 本质是匿名的函数对象每个 lambda 都有自己的operator()继承后overloaded就 “拥有” 了这些operator()。using Ts::operator()...;这是 C17 的包展开语法作用是 “把所有基类Ts的operator()都引入到overloaded的作用域中”。❌ 不加这行的问题C 中子类继承多个基类的同名函数这里都是operator()时基类的函数会被 “隐藏”编译器不知道该调用哪个✅ 加这行的作用显式把所有基类的operator()暴露出来让编译器能根据参数类型匹配对应的重载版本。这才是重点templateclass... Ts overloaded(Ts...) - overloadedTs...;这是 C17 的类模板推导指南作用是当我们使用overloaded{lambda1, lambda2, lambda3}这种方式创建对象时编译器能自动推导模板参数Ts就是这 3 个 lambda 的类型比如我们写的overloaded{[](int){}, [](double){}}编译器会推导Ts是 “处理 int 的 lambda 类型 处理 double 的 lambda 类型”自动生成overloadedlambda1_type, lambda2_type的对象备注C20 起编译器能自动推导这行可以省略但为了兼容通常会保留。overloaded是一个继承了多个类型Ts...的变参模板结构体当你用overloaded{lambda1, lambda2}这种 “聚合初始化” 的方式创建对象时C17 的编译器默认只会 “从结构体的成员变量” 推导模板参数不会从 “基类列表Ts...” 推导而overloaded结构体本身没有任何成员变量只有继承的基类所以编译器会直接报错“无法推导 overloaded 的模板参数”。第一步先看 “有成员变量” 的正常情况编译器能推导假设我们写一个简单的模板结构体里面有成员变量// 模板结构体有一个成员变量类型是 T templateclass T struct MyStruct { T value; // 成员变量 }; int main() { // 用 {10} 初始化编译器能推导 // 1. 看到成员变量 value 被赋值为 10int类型 // 2. 所以模板参数 T int自动生成 MyStructint MyStruct s{10}; return 0; }这个场景编译器能正常推导因为它能从成员变量的赋值里找到模板参数的匹配关系。第二步再看 overloaded 的情况编译器推导失败回到我们的overloaded结构体它的定义是templateclass... Ts struct overloaded : Ts... { // 只有基类 Ts...没有任何成员变量 using Ts::operator()...; };当你写overloaded{lambda1, lambda2}时问题就来了编译器的思考过程C17“我要推导 overloaded 的模板参数 Ts...首先找它的成员变量…… 哦它没有成员变量”“那我该从哪找 Ts... 的类型基类列表不行规则说我只看成员变量不看基类”“完了找不到匹配的模板参数报错”通俗比喻这就像你去买奶茶店员只认 “菜单上的选项”成员变量不认 “赠品”基类。你指着赠品说 “我要这个”店员会说 “我不知道这是什么没法下单”—— 编译器就是这个店员它只看成员变量不认基类所以推导失败。第三步推导指南的作用给编译器 “开特例”我们写的推导指南templateclass... Ts overloaded(Ts...) - overloadedTs...;本质是给编译器加了一条 “特例规则”“当有人用overloaded{参数1, 参数2,...}创建对象时不管你有没有成员变量直接把这些参数的类型当成模板参数 Ts...”加上这条规则后编译器再看到overloaded{lambda1, lambda2}“哦有推导指南不用看成员变量了。”“参数 1 是 lambda1类型 L1参数 2 是 lambda2类型 L2。”“所以 Ts... L1, L2模板参数就定了生成 overloadedL1, L2”所以C17 编译器推导模板参数时 “眼里只有成员变量”而overloaded没有成员变量、只有继承的基类所以编译器猜不到模板参数推导指南的作用就是 “绕开成员变量规则”直接告诉编译器用初始化参数的类型作为模板参数。你可以把这个过程记成正常情况成员变量类型 → 模板参数编译器会overloaded 情况初始化参数类型 → 模板参数需要推导指南教编译器 综合案例简化#include iostream #include list #include set #include string #include type_traits #include variant #include vector templateclass... Ts struct overloaded : Ts... { using Ts::operator()...; }; templateclass... Ts overloaded(Ts...) - overloadedTs...; // 实现一个哈希表桶可以是一个链表也可以是一个红黑树 class HashTable { private: using Value std::variantstd::listint, std::setint; std::vectorValue _tables; public: HashTable(size_t len) : _tables(len) {} void insert(const int key) { size_t hash key % _tables.size(); // 扩容 if (std::holds_alternativestd::listint(_tables[hash])) { auto list std::getstd::listint(_tables[hash]); // 小于则插入到链表 if (list.size() 8) { list.push_back(key); } else { // 大于则转换到红黑树 std::setint s(list.begin(), list.end()); s.insert(key); _tables[hash] move(s); } } else { auto set std::getstd::setint(_tables[hash]); set.insert(key); } } bool find(const int key) { size_t hash key % _tables.size(); // 查找 auto findInList [key](std::listint list) - bool { return std::find(list.begin(), list.end(), key) ! list.end(); }; auto findInSet [key](std::setint set) - bool { return set.count(key); }; return std::visit(overloaded{ findInList, findInSet }, _tables[hash]); } }; int main() { HashTable ht(10); for (int i 0; i 10; i) { ht.insert(i * 10); } std::cout ht.find(3) std::endl; std::cout ht.find(30) std::endl; return 0; }这个哈希表案例中std::variantstd::listint, std::setint被用来定义哈希桶的类型让每个桶既能存储链表std::list也能存储红黑树std::set—— 插入元素时先通过std::holds_alternative判断当前桶是链表还是红黑树若链表元素数超过 8 则自动转为红黑树查找元素时利用std::visit结合overloaded技巧根据桶的实际类型链表 / 红黑树自动调用对应的查找逻辑链表用std::find、红黑树用countstd::variant在这里的核心价值是用类型安全的方式替代传统 union既能灵活存储两种不同的容器类型又能通过配套的std::holds_alternative/std::get/std::visit安全地处理不同类型的逻辑避免了手动管理类型标识的繁琐和出错风险实现了 “一个容器位置存储多种类型、且每种类型执行专属逻辑” 的需求。所以在这里std::variant承载的核心价值就是通过 “链表 / 红黑树的切换规则元素数≥8” 这个数学阈值平衡哈希表的时间 / 空间消耗—— 链表std::list的优势是插入快、空间开销小但查找慢O (n)适合元素少的场景红黑树std::set的优势是查找快O (logn)但插入 / 空间开销大适合元素多的场景。开发时通过 “8 个元素” 这个数学阈值作为切换条件用std::variant让每个哈希桶根据元素数量动态切换存储类型元素少的时候用链表省空间、快插入元素多的时候用红黑树提查找效率最终实现 “低元素量时控空间消耗高元素量时控时间消耗” 的平衡而std::variant则是实现这种 “动态类型切换” 的类型安全载体避免了传统方式如手动标记类型、强制类型转换的出错风险。简单实现原理std::visit本质是 “编译期生成类型分发表 运行时查表调用”把variant的类型索引映射到对应的处理函数。下面我用 “通俗原理 简化实现” 的方式给你讲透它的底层逻辑一、std::visit核心实现原理大白话版编译期准备生成 “类型 - 函数” 映射表编译器会先分析variant的类型列表比如本例中是listint和setint以及你传入的访问者overloaded组合的两个 lambda为每个类型生成对应的 “处理函数地址”并按类型索引0 对应 list、1 对应 set整理成一张 “分发表”。运行时执行查表 调用程序运行时std::visit先获取variant当前存储类型的索引通过variant.index()然后到 “分发表” 里找到该索引对应的处理函数最后把variant里的实际值传给这个函数执行 —— 整个过程就像 “根据类型编号找对应的工具干活”。二、简化版实现帮你理解核心逻辑我们用伪代码模拟std::visit的核心逻辑你一看就懂// 模拟 std::variant 的核心结构 templateclass... Ts struct MyVariant { size_t index; // 存储当前类型的索引 // 存储实际值的内存简化版实际是对齐的内存块 alignas(Ts...) char data[max_sizeof(Ts...)]; // 获取当前类型索引 size_t get_index() const { return index; } // 按索引获取值的指针简化版 void* get_data() { return data; } }; // 模拟 std::visit 的核心实现 templateclass Visitor, class... Ts auto my_visit(Visitor visitor, MyVariantTs... var) { // 编译期生成类型索引 → 处理函数 的映射表 using FuncTable void* (*)[]; static FuncTable table { // 对每个类型 Ts生成“把 var 的值传给 visitor”的函数 [](MyVariantTs... v) { return visitor(*static_castTs*(v.get_data())); }... }; // 运行时根据索引查表调用对应函数 size_t idx var.get_index(); return table[idx](var); }这个简化版里编译期table数组会被编译器生成每个元素对应一个类型的处理函数运行时只需要根据index取数组元素调用函数即可没有多余的if-else分支。三、结合哈希表案例的具体执行流程在你的哈希表find函数中return std::visit(overloaded{findInList, findInSet}, _tables[hash]);编译期编译器生成一张表索引 0 对应findInList处理 list、索引 1 对应findInSet处理 set运行时先获取_tables[hash]的索引0 或 1若索引是 0 → 调用findInList把variant里的 list 传给它若索引是 1 → 调用findInSet把variant里的 set 传给它最终返回查找结果。总结std::visit的核心是编译期生成分发表运行时快速查表调用比手动写if-else get_if更高效它的原理本质是 “把类型判断从运行时的分支提前到编译期的表生成”既保证类型安全又不损失性能在哈希表案例中它的作用就是 “根据 variant 的实际类型list/set自动调用对应的查找函数”不用手动写分支判断。总结std::variant作为 C17 的类型安全联合体核心只允许存放可析构、可移动 / 拷贝、可实例化的非引用值类型绝对不能存放引用类型需用std::reference_wrapper包装、void类型、不完整类型未定义的结构体、抽象类含纯虚函数同时不建议存放重复类型如variantint, int会导致std::get编译报错、无合法移动 / 拷贝语义的复杂类型易资源泄漏、超大内存类型徒增variant内存开销而 C17 中还要求其第一个类型必须有默认构造函数C20 放宽此限制你的哈希表案例中listint和setint因满足 “可默认构造、可移动、尺寸适中” 的要求是variant的典型合理用法。所以std::variant禁存引用、void、不完整类型、抽象类不建议存重复类型、无合法移动 / 拷贝的类型、超大类型核心要求存放类型需是可析构、可移动 / 拷贝、可实例化的值类型。