当前位置: 首页 > news >正文

C++笔记:std::variant

由于最近接触了MFC,被里面的消息机制给恶心到吐了。这种陈年老屎山确实该丢进垃圾堆里了。

其中尤为严重的就是WPARAM和LPARAM的包罗万象,各种强转看的人直皱眉头。

现代C++17引入了std::variant,表示一个类型安全的联合体(union),或 “代替基类指针的现代方案”。

std::variant 是什么?

std::variant 是一个 可以存放多种类型中之一的类型安全容器

可以理解为:

union { int, double, std::string }  // 但带类型安全 + 自动管理对象生命周期

比如:

#include <variant>
#include <string>
#include <iostream>std::variant<int, double, std::string> data;

与联合体在聚合初始化中的行为一致, 若 variant 保有某个对象类型 T 的值,则直接于 variant 的对象表示中分配 T 的对象表示。不允许 variant 分配额外的(动态)内存。

variant 不容许保有引用、数组,或类型 void 。空 variant 亦为病式(可用 std::variant<std::monostate> 代替)。

同联合体,默认构造的 variant 保有其首个选项的值,除非该选项不是可默认构造的(该情况下 variant 亦非可默认构造:能用辅助类 std::monostate 使这种 variant 可默认构造)。

用法一:存放不同类型的值

data = 42;           // 现在是 int
data = 3.14;         // 现在是 double
data = "hello"s;     // 现在是 std::string

std::holds_alternative

#include <variant>
template< class T, class... Types >
constexpr bool holds_alternative( const std::variant<Types...>& v ) noexcept;

检查 variant v 是否保有可选项 T 。若 T 不在 Types... 中出现一次,则此调用返回false。

参数

v 要检验的 variant

返回值

variant 当前保有可选项 T 则为 true ,否则为 false

示例:

#include <variant>
#include <string>
#include <iostream>
int main()
{std::variant<int, std::string> v = "abc";std::cout << std::boolalpha<< "variant holds int? "<< std::holds_alternative<int>(v) << '\n'<< "variant holds string? "<< std::holds_alternative<std::string>(v) << '\n';
}

输出:

variant holds int? false
variant holds string? true

std::get_if

template< std::size_t I, class... Types >
constexpr std::add_pointer_t<std::variant_alternative_t<I, std::variant<Types...>>>
get_if( std::variant<Types...>* pv ) noexcept;   template< std::size_t I, class... Types >
constexpr std::add_pointer_t<const std::variant_alternative_t<I, variant<Types...>>>
get_if( const std::variant<Types...>* pv ) noexcept; 
template< class T, class... Types >
constexpr std::add_pointer_t<T> get_if( std::variant<Types...>* pv ) noexcept;template< class T, class... Types >
constexpr std::add_pointer_t<const T> get_if( const std::variant<Types...>* pv ) noexcept; 

参数

I要查找的下标
T要查找的类型
pv指向 variant 的指针

返回值

指向存储于被指向的 variant 中值的指针,错误时为空指针。

示例:

#include <variant>
#include <iostream>int main()
{std::variant<int, float> v{12};//pval的类型是std::add_pointer_t<int>,就是int *if(auto pval = std::get_if<int>(&v))std::cout << "variant value: " << *pval << '\n'; else std::cout << "failed to get value!" << '\n'; 
}

std::visit

由于 variant 内部可能有多种类型,访问时我们需要一个安全的“访问者”。

这就是 std::visit 的作用:

template <class _Callable, class... _Variants, class = void_t<_As_variant<_Variants>...>>
constexpr _Variant_visit_result_t<_Callable, _As_variant<_Variants>...>
visit(_Callable&& _Obj, _Variants&&... _Args);
  • template <class _Callable, class... _Variants, class = ...>
    这是函数模板,_Callable 是传入的访问者(visitor),_Variants... 是一个或多个 std::variant<...>(或能被视作 variant 的类型)。

  • class = void_t<_As_variant<_Variants>...>
    这是 SFINAE 检查:内部使用 _As_variant<T>(库内部 trait),目的是只启用当每个 _Variants 都能被视为 std::variant 的情形。换句话说:std::visit 只在你传入的参数是 variant(或“可视为 variant”的包装类型)时可用。

返回类型:

_Variant_visit_result_t<_Callable, _As_variant<_Variants>...>
  • 这是一个实现细节别名(library-internal alias)。语义上:返回值类型是对 _Callable 应用到 variant 中备选类型组合后的结果类型的折叠/归一化(见下面详细说明)。

  • 参数:visit(_Callable&& _Obj, _Variants&&... _Args)
    接受一个访问者对象(通常是 lambda 或函数对象),然后是一或多个 variant(可以是左值/右值/const等,std::visit 会完美转发它们)。

std::visit 的返回类型

假设调用是:

std::visit(visitor, v1, v2, ...);

编译器在编译期要推导出返回类型 R,这个 R 必须在所有可能的“variant 活动类型组合”下,都能产生一致的结果类型

std::variant<int, double> 其实可以理解为:

union {int i;double d;
};
size_t index; // 当前哪一个类型激活(0 -> int, 1 -> double)

那么 std::visit(visitor, v) 的行为就像是:

switch (v.index()) {case 0: return visitor(std::get<0>(v)); // visitor(int)case 1: return visitor(std::get<1>(v)); // visitor(double)
}

于是:

  • visitor(int) 的返回类型 = R1

  • visitor(double) 的返回类型 = R2

编译器要知道最终函数 visit 返回什么类型,它就得要求:
👉 R1R2 必须是相同类型,或者可以隐式转换到一个共同类型

std::variant<int, double> v = 42;// ✅ 所有分支返回相同类型(int)
std::visit([](auto&& x) -> int { return (int)x; }, v);// ❌ 分支返回类型不一致(一个 int,一个 std::string)
std::visit(overloaded{[](int) { return 1; },[](double) { return std::string("pi"); }
}, v);
// 报错:无法推导出唯一返回类型

✅ 解决方法:手动归一类型

auto res = std::visit(overloaded{[](int i) -> std::string { return std::to_string(i); },[](double d) -> std::string { return std::to_string((int)d); }
}, v);// ✅ 显式指定返回类型 -> std::string

res作为返回值,结果就是lambda表达式返回的内容。

但是返回值用的比较少,很多访问只是为了执行动作,而不是计算结果。比如打印日志、更新 GUI、修改状态机,不需要返回值。

std::visit 做了什么(高层行为)

  • std::visit 会:

    1. 读取每个 variant 当前持有的备选类型(每个 variant 在运行时有一个 active alternative)。

    2. 以这些 active alternative 的实际类型作为参数,调用你提供的访问者 _Callable

      • 若只有一个 variant,就调用 invoke(_Callable, T)

      • 若有两个 variant,就调用 invoke(_Callable, T1, T2)(T1 来源于第一个 variant 当前的 alternative,T2 来源于第二个)。

    3. 将该调用结果作为 visit 的返回值返回(返回类型按下文规则推导)。

  • 关键点:访问者 必须所有可能的类型组合 都是有效可调用(或所有结果类型可转换为某个共同类型),否则编译会报错。也就是说编译时要能保证“任意组合下调用访问者可成立或可转换为统一结果类型”。

如何编写 _Callable(访问者)——实战与注意事项

1) 单个 variant:最简单

std::variant<int, std::string> v = 42;std::visit([](auto&& x){// x 的类型是 variant 当前持有的类型(int 或 std::string)std::cout << x << '\n';
}, v);
  • auto&& x通用引用,接收左值/右值、保留 cv/ref 属性(完美转发风格)。

  • 如果 vconst std::variant<...>&,则 x 会是 const T&

2) 多个 variant:访问者签名必须能接受所有组合

当有多个 variant 时,访问者会被调用为 visitor(T1, T2, ...)

示例:

std::variant<int, float> v1;
std::variant<std::string, char> v2;std::visit([](auto&& a, auto&& b){// 这里 a 是 int 或 float,b 是 std::string 或 char// 你必须能以任意组合调用该 lambdausing A = std::decay_t<decltype(a)>;using B = std::decay_t<decltype(b)>;// 处理...
}, v1, v2);

重要:如果你写的访问者只处理 (int, std::string) 的组合,而 v1 也可能是 floatv2 也可能是 char,那么编译会失败 —— 因为 visit 需要覆盖所有组合。

常见做法是使用 overloaded(见下)把多个具体处理函数合并,或写一个泛型 lambda 并在内部 if constexpr 区分类型。

3) 想控制参数类型(按引用/按值/按 const)

  • 传入 variant 的值类别与 visit 参数类型有关:

    • std::variant<Ts...>& → 访问者参数类型为 T&(若用 auto&&,就得到 T&

    • const std::variant<Ts...>&const T&

    • std::variant<Ts...>(右值) → T&&

  • 常用写法是 [](auto&& x),这样 visit 对任何传入(左值/右值/const)都合适。如果你想确保不修改元素,用 const auto&

overloaded 工具(把多个 lambda 合并为一个重载对象)

overloaded 是一个非常常见的轻量模板技巧,用来把若干不同签名的 lambda 或可调用对象合并成一个对象,其 operator() 是多个重载的集合。实现如下(C++17 写法):

// overloaded helper
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...;   // fold expression: 把每个基类的 operator() 导入
};template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // C++17 的类模板参数推导

用法示例

std::variant<int, std::string> v = "hi";std::visit(overloaded{[](int i) { std::cout << "int: " << i << '\n'; },[](const std::string& s) { std::cout << "string: " << s << '\n'; }
}, v);

overloaded{...} 的类型是 overloaded<lambda1_type, lambda2_type>,它继承自两个 lambda 的闭包类型,并合并它们的 operator(),因此 std::visit 可以通过重载解析到合适分支。

注意:若多个 lambda 的重载集不能覆盖 variant 的每个备选类型,会编译失败(或者 std::visit 会在某些组合上找不到匹配)。

具体示例(覆盖单/多 variant、多返回类型场景)

示例 1:单 variant,统一返回 void

#include <variant>
#include <iostream>
#include <string>int main(){std::variant<int, std::string> v = 10;std::visit([](auto&& val) {std::cout << "value: " << val << '\n';}, v);
}

示例 2:使用 overloaded,返回统一类型 std::string

#include <variant>
#include <iostream>
#include <string>template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;int main(){std::variant<int, double, std::string> v = 3.14;auto res = std::visit(overloaded{[](int i) -> std::string { return "int:" + std::to_string(i); },[](double d) -> std::string { return "double:" + std::to_string(d); },[](const std::string& s) -> std::string { return "str:" + s; }}, v);std::cout << res << '\n';
}

示例 3:两个 variant,使用overloaded

#include <iostream>
#include <variant>
#include <string>template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;int main() {std::variant<int, double> v1 = 3;std::variant<std::string, bool> v2 = true;std::visit(overloaded{[](int i, const std::string& s) {std::cout << "int + string: " << i << "," << s << "\n";},[](int i, bool b) {std::cout << "int + bool: " << i << "," << b << "\n";},[](double d, const std::string& s) {std::cout << "double + string: " << d << "," << s << "\n";},[](double d, bool b) {std::cout << "double + bool: " << d << "," << b << "\n";}}, v1, v2);
}

示例 4:两个 variant,处理所有组合(用泛型 + 条件分支)

#include <variant>
#include <iostream>
#include <string>int main(){std::variant<int, float> v1 = 7;std::variant<char, std::string> v2 = 'x';std::visit([](auto&& a, auto&& b){using A = std::decay_t<decltype(a)>;using B = std::decay_t<decltype(b)>;if constexpr (std::is_same_v<A,int> && std::is_same_v<B,char>) {std::cout << "int+char\n";} else if constexpr (std::is_same_v<A,int> && std::is_same_v<B,std::string>) {std::cout << "int+string\n";} else if constexpr (std::is_same_v<A,float> && std::is_same_v<B,char>) {std::cout << "float+char\n";} else {std::cout << "float+string\n";}}, v1, v2);
}

这里用 if constexpr 在编译期选择分支,确保每一种组合都是可编译的。

std::variant的用法大全

旧时代的 union

union Data {int i;double d;
};
  • 优点:节省空间;

  • 缺点:类型信息丢失,你必须手动记住现在存的是什么,否则访问错误类型就 UB(未定义行为)。

std::variant 的改进:

std::variant<int, double, std::string> v;
v = 3.14;
v = "Hello"s;
  • 自动记录当前活跃类型;

  • 编译器会帮你类型检查;

  • 访问不匹配类型时会抛出异常或触发静态错误;

  • 能与 std::visit 配合实现安全的“类型模式匹配”。

std::variant 的核心能力

功能示例说明
存储不同类型std::variant<int, std::string> v;类似 union
类型安全的取值std::get<int>(v) / std::get_if<int>(&v)若类型不匹配会抛异常
查询当前类型v.index()返回当前活跃类型的索引(从0开始)
安全访问std::visit(visitor, v)根据当前类型自动调用正确函数

1️⃣ 代替 void*LPARAM/WPARAM —— 类型安全的万能参数

在 MFC、Win32 那种写法里经常会看到:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

问题是:wParamlParam 是裸的整数或指针,没有类型安全。

可以改为:

using MessageParam = std::variant<int, std::string, MyStruct>;void onMessage(MessageParam param) {std::visit(overloaded{[](int i){ std::cout << "int: " << i; },[](const std::string& s){ std::cout << "string: " << s; },[](const MyStruct& st){ std::cout << "struct"; }}, param);
}

优点:

  • 不用强制转型;

  • 自动检查类型;

  • 没有内存对齐或 UB 问题;

  • IDE 自动提示类型。

2️⃣ 替代继承层次(无虚函数、无堆分配)

服务器、游戏、UI 框架常见:

struct EventLogin { std::string user; };
struct EventLogout { int id; };
struct EventChat { int from; std::string msg; };using Event = std::variant<EventLogin, EventLogout, EventChat>;

因为很多时候你并不需要多态,只是要能装不同类型的“操作”:

分发逻辑:

void handleEvent(const Event& e) {std::visit(overloaded{[](const EventLogin& e){ std::cout << "login: " << e.user; },[](const EventLogout& e){ std::cout << "logout: " << e.id; },[](const EventChat& e){ std::cout << e.from << ": " << e.msg; }}, e);
}

3️⃣ 作为解析结果的返回类型(成功/错误)

std::variant<Result, Error> parseData(std::string_view text);
auto res = parseData("...");
std::visit(overloaded{[](const Result& r){ std::cout << "OK: " << r.value; },[](const Error& e){ std::cerr << "Error: " << e.msg; }
}, res);

相当于现代 C++ 版的 Rust Result<T, E>,类型安全又优雅。

4️⃣ 表达多种配置项 / JSON-like 数据结构

比如某个配置项可以是整数、字符串或布尔:

using ConfigValue = std::variant<int, bool, std::string>;
std::unordered_map<std::string, ConfigValue> config;config["port"] = 8080;
config["debug"] = true;
config["name"] = "server_1";//访问
std::visit([](auto&& val){ std::cout << val; }, config["name"]);

✅ 类型安全、轻量、比 std::any 更高性能。

总结

在以下场景非常推荐使用:

  • 事件分发系统(代替 switch case)

  • 解析器或状态机(多分支结果)

  • GUI / 网络消息(多种 payload)

  • 异步任务结果

  • 替代多态的轻量数据结构

🚫 不太适合:

  • 类型数量极多(上百种);

  • 类型不固定(动态插件系统);

  • 存储不确定类型(那用 std::any 更好)。

http://www.dtcms.com/a/557085.html

相关文章:

  • day03(11.1)——leetcode面试经典150
  • 《算法通关指南:数据结构和算法篇 --- 顺序表相关算法题》---移动零,颜色分类
  • 视觉差网站制作百度站长统计
  • 求职专栏-【面试-自我介绍】
  • Chroma向量数据库详解:高效向量检索在AI应用中的实践指南
  • 【开题答辩全过程】以 风聆精酿啤酒销控一体系统的设计与实现为例,包含答辩的问题和答案
  • 二.docker安装与常用命令
  • 珠海网红打卡景点网站排名优化首页
  • 计算机网络Day01
  • QCES项目Windows平台运行指南
  • 多线程编程:条件变量、同步、竞态条件与生产者消费者模型
  • 怎么做高端品牌网站设计潍坊市住房和城乡建设网站
  • 哪个协会要做网站建设啊甘肃做网站哪家专业
  • springcloud : 理解Sentinel 熔断与限流服务稳定性的守护神
  • Webpack Tree Shaking 原理与实践
  • 一文讲透 npm 包版本管理规范
  • Qt 绘画 Widget 详解:从基础到实战
  • 【计算机网络】深入理解网络层:IP地址划分、CIDR与路由机制详解
  • 力扣3281. 范围内整数的最大得分
  • 力扣hot100----15.三数之和(java版)
  • 网站建设最重要的是什么什么是网站的主页
  • 影视传媒网站源码成华区建设局网站
  • 快速搭建网站 开源网络营销推广的目的是什么
  • 超越传统:大型语言模型在文本分类中的突破与代价
  • 【洛谷算题】顺序,分支,循环结构部分题目分享
  • Jmeter吞吐量控制器详解
  • 最全网站源码分享哈尔滨建设发展集团有限责任公司
  • 机器学习-KNN算法示例
  • 【随机访问介质访问控制-1】为什么纯 ALOHA 效率不到 20%?3 大随机访问 MAC 协议拆解
  • 有关电子商务网站建设与维护的书籍具有价值的响应式网站