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 返回什么类型,它就得要求:
👉 R1 和 R2 必须是相同类型,或者可以隐式转换到一个共同类型。
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会:-
读取每个
variant当前持有的备选类型(每个 variant 在运行时有一个 active alternative)。 -
以这些 active alternative 的实际类型作为参数,调用你提供的访问者
_Callable:-
若只有一个
variant,就调用invoke(_Callable, T); -
若有两个
variant,就调用invoke(_Callable, T1, T2)(T1 来源于第一个 variant 当前的 alternative,T2 来源于第二个)。
-
-
将该调用结果作为
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 属性(完美转发风格)。 -
如果
v是const 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 也可能是 float,v2 也可能是 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);
问题是:wParam、lParam 是裸的整数或指针,没有类型安全。
可以改为:
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更好)。
