C++笔记:std::variant

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

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

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

std::variant 是什么?

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

可以理解为:

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

比如:

cpp 复制代码
#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 可默认构造)。

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

cpp 复制代码
data = 42;           // 现在是 int
data = 3.14;         // 现在是 double
data = "hello"s;     // 现在是 std::string

std::holds_alternative

cpp 复制代码
#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

示例:

cpp 复制代码
#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';
}

输出:

bash 复制代码
variant holds int? false
variant holds string? true

std::get_if

cpp 复制代码
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; 
cpp 复制代码
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 中值的指针,错误时为空指针。

示例:

cpp 复制代码
#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 的作用:

cpp 复制代码
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"的包装类型)时可用。

返回类型:

cpp 复制代码
_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 的返回类型

假设调用是:

cpp 复制代码
std::visit(visitor, v1, v2, ...);

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

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

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

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

cpp 复制代码
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 必须是相同类型 ,或者可以隐式转换到一个共同类型

cpp 复制代码
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);
// 报错:无法推导出唯一返回类型

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

cpp 复制代码
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:最简单

cpp 复制代码
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, ...)

示例:

cpp 复制代码
std::variant<int, float> v1;
std::variant<std::string, char> v2;

std::visit([](auto&& a, auto&& b){
    // 这里 a 是 int 或 float,b 是 std::string 或 char
    // 你必须能以任意组合调用该 lambda
    using 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 写法):

cpp 复制代码
// overloaded helper
template<class... Ts> struct overloaded : Ts... { 
    using Ts::operator()...;   // fold expression: 把每个基类的 operator() 导入
};

template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // C++17 的类模板参数推导

用法示例

cpp 复制代码
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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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,处理所有组合(用泛型 + 条件分支)

cpp 复制代码
#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

cpp 复制代码
union Data {
    int i;
    double d;
};
  • 优点:节省空间;

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

std::variant 的改进:

cpp 复制代码
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 那种写法里经常会看到:

cpp 复制代码
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

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

可以改为:

cpp 复制代码
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 框架常见:

cpp 复制代码
struct EventLogin { std::string user; };
struct EventLogout { int id; };
struct EventChat { int from; std::string msg; };

using Event = std::variant<EventLogin, EventLogout, EventChat>;

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

分发逻辑:

cpp 复制代码
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️⃣ 作为解析结果的返回类型(成功/错误)

cpp 复制代码
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 数据结构

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

cpp 复制代码
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 更好)。

相关推荐
T***u3332 小时前
PHP在电商中的会员管理
开发语言·wireshark·php·ue4·jina
张丶大帅2 小时前
JS案例合集
开发语言·javascript·笔记
2301_795167203 小时前
Python 高手编程系列八:缓存
开发语言·python·缓存
极地星光3 小时前
C++链式调用设计:打造优雅流式API
服务器·网络·c++
8***29313 小时前
Go基础之环境搭建
开发语言·后端·golang
Yue丶越3 小时前
【C语言】自定义类型:联合体与枚举
c语言·开发语言
小陈要努力3 小时前
Visual Studio 开发环境配置指南
c++·opengl
程序猿本员3 小时前
5. 实现
c++
csbysj20204 小时前
DOM 节点
开发语言
Bona Sun4 小时前
单片机手搓掌上游戏机(十五)—pico运行fc模拟器之编译环境
c语言·c++·单片机·游戏机