目录
1.简介
2.基础用法
3.高级技巧
4.注意事项
5.与其他访问方式对比
6.底层原理
6.1.原理分析
6.2.源码分析
6.2.1.从入参限制只允许传入 variant
6.2.2.std::visit 入口函数(编译期核心)
6.2.3._Visit_impl 策略选择器(性能核心)
6.2.4._Visit_strategy 分发策略实现
6.2.5.宏代码生成(最精妙的部分)
6.2.6.最终分发 _Variant_dispatcher
7.总结
1.简介
设计模式之访问者模式
C++三剑客之std::variant(一) : 使用
C++三剑客之std::variant(二):深入剖析
std::visit 是 C++17 引入的标准库函数模板,定义于 <variant> 头文件,用于安全、类型完备地访问 std::variant 中当前存储的值,是访问 variant 的推荐方式。它基于访问者模式 (Visitor Pattern) 实现,自动匹配 variant 活跃类型并调用对应处理逻辑,避免手动判断 type index 或冗长 switch-case,杜绝未定义行为。
语法格式:
// C++17 基础版本,返回类型自动推导template <class Visitor, class... Variants>constexpr /* 推导类型 */ visit(Visitor&& vis, Variants&&... vars);// C++20 显式指定返回类型版本template <class R, class Visitor, class... Variants>constexpr R visit(Visitor&& vis, Variants&&... vars);
- Visitor
:可调用对象(函数、lambda、函数对象),需支持所有 variant 类型组合的调用 - Variants
:一个或多个 std::variant 对象,visit 会组合所有 variant 的活跃类型调用 visitor - 返回值
:visitor 调用结果,C++20 可显式指定返回类型 R
2.基础用法
1.单 variant 访问(lambda 作为访问者)
#include<variant>#include<iostream>#include<string>intmain(){std::variant<int, double, std::string> var;var = 42;std::visit([](auto&& val) {using T = std::decay_t<decltype(val)>;if constexpr (std::is_same_v<T, int>)std::cout << "整数: " << val << "\n";else if constexpr (std::is_same_v<T, double>)std::cout << "浮点数: " << val << "\n";else if constexpr (std::is_same_v<T, std::string>)std::cout << "字符串: " << val << "\n";}, var);var = 3.14;std::visit([](auto val) { std::cout << "值: " << val << "\n"; }, var);var = "Hello C++17";std::visit([](const auto& val) { std::cout << "值: " << val << "\n"; }, var);return 0;}
2.多 variant 组合访问
#include<variant>#include<iostream>#include<string>using VarType = std::variant<int, double, std::string>;intmain(){VarType a = 10;VarType b = 3.14;VarType c = "Hello";// 组合两个 variant 的所有可能类型组合std::visit([](auto&& x, auto&& y) {std::cout << "x: " << x << ", y: " << y << "\n";}, a, b);// 组合三个 variantstd::visit([](auto&& x, auto&& y, auto&& z) {std::cout << "x: " << x << ", y: " << y << ", z: " << z << "\n";}, a, b, c);return 0;}
输出:

3.高级技巧
1.重载 lambda 访问者(推荐)
C++17 可通过继承多个 lambda 实现重载访问,需要定义辅助结构体:
#include <variant>#include <iostream>#include <string>// 辅助模板:继承多个 lambda 并 using 其 operator()template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };// 推导指引:让编译器自动推导模板参数template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;{}int main() {std::variant<int, double, std::string> var = 42.5;// 使用重载 lambda 处理不同类型std::visit(overloaded{[](int i) { std::cout << "整数: " << i << "\n"; },[](double d) { std::cout << "浮点数: " << d << "\n"; },[](const std::string& s) { std::cout << "字符串: " << s << "\n"; }}, var);return 0;}
2.函数对象作为访问者
#include<variant>#include<iostream>#include<string>struct PrintVisitor {voidoperator()(int i)const{ std::cout << "整数: " << i << "\n"; }voidoperator()(double d)const{ std::cout << "浮点数: " << d << "\n"; }voidoperator()(const std::string& s)const{ std::cout << "字符串: " << s << "\n"; }};intmain(){std::variant<int, double, std::string> var;var = "C++17";std::visit(PrintVisitor{}, var);return 0;}
3.带返回值的访问
#include<variant>#include<iostream>#include<string>#include<string_view>using VarType = std::variant<int, double, std::string>;// 返回值类型统一为 std::stringstd::string format_value(const VarType& var){return std::visit([](auto&& val) -> std::string {using T = std::decay_t<decltype(val)>;if constexpr (std::is_same_v<T, int>)return "整数: " + std::to_string(val);else if constexpr (std::is_same_v<T, double>)return "浮点数: " + std::to_string(val);else if constexpr (std::is_same_v<T, std::string>)return "字符串: " + val;}, var);}intmain(){VarType a = 42;VarType b = 3.14159;VarType c = "Hello World";std::cout << format_value(a) << "\n";std::cout << format_value(b) << "\n";std::cout << format_value(c) << "\n";return 0;}
4.C++20 显式指定返回类型
#include<variant>#include<iostream>#include<string>intmain(){std::variant<int, double, std::string> var = 3.14;// 显式指定返回类型为 doubledouble result = std::visit<double>([](auto val) {using T = std::decay_t<decltype(val)>;if constexpr (std::is_same_v<T, int>)return static_cast<double>(val);else if constexpr (std::is_same_v<T, double>)return val;else if constexpr (std::is_same_v<T, std::string>)return std::stod(val);}, var);std::cout << "转换为 double: " << result << "\n";return 0;}
4.注意事项
- 类型完备性检查
:visitor 必须处理 variant 所有可能类型,否则编译失败,这是编译期安全保障,避免遗漏类型处理 - constexpr 支持
:C++17 起 visit 可在常量表达式中使用,适用于编译期计算 - 值类别处理
:可通过完美转发( auto&&)保持原始值类别,避免不必要拷贝 - 性能特点
:visit 通常零开销抽象,生成的代码与手动编写的 switch-case 相当,某些编译器甚至更优 - 空 variant 处理
:若 variant 无活跃值(默认构造且未赋值),调用 visit 会抛出 std::bad_variant_access异常 - 多 variant 组合
:当传递多个 variant 时,visit 会生成所有可能类型组合的调用,visitor 必须支持所有组合
5.与其他访问方式对比
| std::visit | |||
| std::get_if | |||
| std::holds_alternative | |||
| 手动 switch (type ()) |
6.底层原理
6.1.原理分析
std::visit 本质是 「编译期生成类型分发逻辑 + 运行期索引跳转」,是零开销的静态多态实现,完全没有虚函数开销,也是 C++ 类型安全的核心保障。
编译期(核心:生成分发逻辑 + 类型安全检查)
- 提取 variant 所有类型
:从模板参数中拿到 <int, double, std::string>; - 生成全部分支
:为每一种类型,生成调用 visitor的代码(比如visitor(int)、visitor(double)); - 类型完备性检查
:强制要求 visitor能处理所有类型,少一个都直接编译报错(这是它类型安全的根源)。
最终编译器生成一张 **「类型索引 → 函数调用」的跳转表 **(函数指针数组 / 优化后的 switch-case)。
运行期(核心:索引查表,直接跳转)
读取 variant.index()获取当前类型编号;根据编号从跳转表中找到对应的函数,直接调用访问者逻辑。
用 20 行代码模拟标准库的核心原理,一看就懂:
#include <iostream>#include <string>#include <variant>// 1. 复用之前的重载lambda工具template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;// 2. 手写简化版 std::visit(核心原理!)template <typename Visitor, typename... Types>void my_visit(Visitor&& vis, std::variant<Types...>& var) {// 运行期:获取variant的类型索引const size_t idx = var.index();// 编译期生成的switch分发逻辑(核心!)switch(idx) {case 0: vis(std::get<0>(var)); break; // 匹配第一个类型case 1: vis(std::get<1>(var)); break; // 匹配第二个类型case 2: vis(std::get<2>(var)); break; // 匹配第三个类型default: throw std::bad_variant_access();}}// 测试int main() {std::variant<int, double, std::string> var = 3.14;// 用手写的my_visit,和标准库效果完全一致my_visit(overloaded{[](int) { std::cout << "int\n"; },[](double) { std::cout << "double\n"; },[](std::string) { std::cout << "string\n"; }}, var);}
✅ 输出:double
6.2.源码分析
以vs2022的std::visit实现为例进行分析,先上源码:
_EXPORT_STD 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) {// Invoke _Obj with the contained values of _Args...constexpr auto _Size = _Variant_total_states<_Remove_cvref_t<_As_variant<_Variants>>...>;using _ListOfIndexLists =_Meta_list<_Meta_as_list<make_index_sequence<1 + variant_size_v<_Remove_cvref_t<_As_variant<_Variants>>>>>...>;using _ListOfIndexVectors =_Meta_transform<_Meta_quote<_Meta_as_integer_sequence>, _Meta_cartesian_product<_ListOfIndexLists>>;using _Ret = _Variant_visit_result_t<_Callable, _As_variant<_Variants>...>;static_assert(_Variant_all_visit_results_same<_Ret, _Callable, _Meta_list<>, _As_variant<_Variants>...>,"visit() requires the result of all potential invocations to have the same type and value category ""(N4950 [variant.visit]/5).");return _STD _Visit_impl<_Size, _Ret, _ListOfIndexVectors>(static_cast<_Callable&&>(_Obj), static_cast<_Variants&&>(_Args)...);}template <size_t _Size, class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>constexpr _Ret _Visit_impl(_Callable&& _Obj, _Variants&&... _Args) {constexpr int _Strategy = _Size == 1 ? 0: _Size <= 4 ? 1: _Size <= 16 ? 2: _Size <= 64 ? 3: _Size <= 256 ? 4: -1;return _Visit_strategy<_Strategy>::template _Visit2<_Ret, _ListOfIndexVectors>(_STD _Variant_visit_index1(0, static_cast<_As_variant<_Variants>&>(_Args)...), static_cast<_Callable&&>(_Obj),static_cast<_As_variant<_Variants>&&>(_Args)...);}template <int _Strategy>struct _Visit_strategy;template <>struct _Visit_strategy<-1> {// Fallback strategy for visitations with too many total states for the following "switch" strategies.template <class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>static constexpr _Ret _Visit2(size_t _Idx, _Callable&& _Obj, _Variants&&... _Args) { // dispatch a visitation with many potential statesconstexpr size_t _Size = _Variant_total_states<_Remove_cvref_t<_Variants>...>;static_assert(_Size > 256);constexpr auto& _Array =_Variant_dispatch_table<_Ret, _ListOfIndexVectors, _Callable, _Meta_list<_Variants...>>::_Array;return _Array[_Idx](static_cast<_Callable&&>(_Obj), static_cast<_Variants&&>(_Args)...);}};template <>struct _Visit_strategy<0> {template <class _Ret, class, class _Callable>static constexpr _Ret _Visit2(size_t, _Callable&& _Obj) { // dispatch a visitation with 4^0 potential statesifconstexpr(is_void_v<_Ret>){return static_cast<void>(static_cast<_Callable&&>(_Obj)());} else {return static_cast<_Callable&&>(_Obj)();}}};#define _STL_CASE(n) \case (n): \if constexpr ((n) < _Size) { \using _Indices = _Meta_at_c<_ListOfIndexVectors, (n)>; \return _Variant_dispatcher<_Indices>::template _Dispatch2<_Ret, _Callable, _Variants...>( \static_cast<_Callable&&>(_Obj), static_cast<_Variants&&>(_Args)...); \} \_STL_UNREACHABLE; \[[fallthrough]]#define _STL_VISIT_STAMP(stamper, n) \constexpr size_t _Size = _Variant_total_states<_Remove_cvref_t<_Variants>...>; \static_assert(_Size > (n) / 4 && _Size <= (n)); \switch (_Idx) { \stamper(0, _STL_CASE); \default: \_STL_UNREACHABLE; \}template <>struct _Visit_strategy<1> {template <class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>static constexpr _Ret _Visit2(size_t _Idx, _Callable&& _Obj, _Variants&&... _Args) {// dispatch a visitation with 4^1 potential states_STL_STAMP(4, _STL_VISIT_STAMP);}};template <>struct _Visit_strategy<2> {template <class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>static constexpr _Ret _Visit2(size_t _Idx, _Callable&& _Obj, _Variants&&... _Args) {// dispatch a visitation with 4^2 potential states_STL_STAMP(16, _STL_VISIT_STAMP);}};template <>struct _Visit_strategy<3> {template <class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>static constexpr _Ret _Visit2(size_t _Idx, _Callable&& _Obj, _Variants&&... _Args) {// dispatch a visitation with 4^3 potential states_STL_STAMP(64, _STL_VISIT_STAMP);}};template <>struct _Visit_strategy<4> {template <class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>static constexpr _Ret _Visit2(size_t _Idx, _Callable&& _Obj, _Variants&&... _Args) {// dispatch a visitation with 4^4 potential states_STL_STAMP(256, _STL_VISIT_STAMP);}};#define _STL_STAMP4(n, x) \x(n); \x(n + 1); \x(n + 2); \x(n + 3)#define _STL_STAMP16(n, x) \_STL_STAMP4(n, x); \_STL_STAMP4(n + 4, x); \_STL_STAMP4(n + 8, x); \_STL_STAMP4(n + 12, x)#define _STL_STAMP64(n, x) \_STL_STAMP16(n, x); \_STL_STAMP16(n + 16, x); \_STL_STAMP16(n + 32, x); \_STL_STAMP16(n + 48, x)#define _STL_STAMP256(n, x) \_STL_STAMP64(n, x); \_STL_STAMP64(n + 64, x); \_STL_STAMP64(n + 128, x); \_STL_STAMP64(n + 192, x)#define _STL_STAMP(n, x) x(_STL_STAMP##n, n)
6.2.1.从入参限制只允许传入 variant
_EXPORT_STD 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){//。。。}
用到了std::void_t,不理解的可以参考下面内容:C++17之std::void_t
再来看一下_As_variant :
// 接收:普通左值 varianttemplate <class... _Types>variant<_Types...>& _As_variant_impl(variant<_Types...>&);// 接收:const 左值 varianttemplate <class... _Types>const variant<_Types...>& _As_variant_impl(const variant<_Types...>&);// 接收:普通右值 varianttemplate <class... _Types>variant<_Types...>&& _As_variant_impl(variant<_Types...>&&);// 接收:const 右值 varianttemplate <class... _Types>const variant<_Types...>&& _As_variant_impl(const variant<_Types...>&&);//declval 编译期生成一个临时的 _Ty 类型值(不创建真实对象)。template <class _Ty>using _As_variant = // Deduce variant specialization from a derived typedecltype(_STD _As_variant_impl(_STD declval<_Ty>()));
关键点:
- 只有声明,没有函数体
不需要实现,我们只在编译期用它推导类型( decltype)。 - 全覆盖所有值类别
左值、右值、const、非 const 全部匹配,不遗漏任何一种 variant 参数。 - 支持派生类
如果你写了一个类 class MyVar : public std::variant<int> {},它能隐式转换为基类variant,所以也能匹配这四个函数。
void_t<_As_variant<_Variants>...>
如果 _Variants是 variant / 派生类 → 推导成功,模板生效;如果传入 int/string等非 variant → 推导失败,直接编译报错。
✅ 这就是 std::visit只允许传入 variant 的底层原因。
6.2.2.std::visit 入口函数(编译期核心)
_EXPORT_STD template <class _Callable, // 访问者(lambda/函数/函数对象)class... _Variants, // 一个/多个 variantclass = void_t<_As_variant<_Variants>...> // 约束:参数必须是variant>constexpr _Variant_visit_result_t<_Callable, _As_variant<_Variants>...>visit(_Callable&& _Obj, _Variants&&... _Args) {// 1. 计算【所有variant类型组合的总状态数】(笛卡尔积)constexpr auto _Size = _Variant_total_states<_Remove_cvref_t<_As_variant<_Variants>>...>;// 2. 为每个variant生成索引列表:如variant<int,double> → [0,1]using _ListOfIndexLists = _Meta_list<_Meta_as_list<make_index_sequence<1 + variant_size_v<...>>>...>;// 3. 计算索引的【笛卡尔积】(多variant核心)using _ListOfIndexVectors = _Meta_transform<_Meta_quote<_Meta_as_integer_sequence>,_Meta_cartesian_product<_ListOfIndexLists>>;// 4. 推导返回值类型using _Ret = _Variant_visit_result_t<_Callable, _As_variant<_Variants>...>;// 5. 强制校验:所有类型分支的返回值 类型/值类别 必须完全一致(C++标准强制要求)static_assert(_Variant_all_visit_results_same<...>,"visit() requires the result of all potential invocations to have the same type...");// 6. 转发给实现函数return _STD _Visit_impl<_Size, _Ret, _ListOfIndexVectors>(static_cast<_Callable&&>(_Obj), // 完美转发static_cast<_Variants&&>(_Args)...);}
关键点:
_Variant_total_states总状态数 = 所有 variant类型数量的乘积例:variant<int,double>×variant<string,bool>→ 2×2=4 种状态
//[1]_EXPORT_STD template <class _Ty>struct variant_size; // undefinedtemplate <class _Ty>struct variant_size<const _Ty> : variant_size<_Ty>::type {};template <class _Ty>struct _CXX20_DEPRECATE_VOLATILE variant_size<volatile _Ty> : variant_size<_Ty>::type {};template <class _Ty>struct _CXX20_DEPRECATE_VOLATILE variant_size<const volatile _Ty> : variant_size<_Ty>::type {};_EXPORT_STD template <class _Ty>constexpr size_t variant_size_v = variant_size<_Ty>::value;template <class... _Types>struct variant_size<variant<_Types...>> : integral_constant<size_t, sizeof...(_Types)> {};//[2]template <class... _Variants>constexpr size_t _Variant_total_states =(size_t{1} * ... * (variant_size_v<_Variants> + 1)); // +1 to account for the valueless state
make_index_sequence编译期根据参数长度生成序列,不明白的可参考:
C++14之std::index_sequence和std::make_index_sequence的使用和原理分析
_Meta_cartesian_product(核心黑科技)编译期生成所有类型组合的索引: (0,0)、(0,1)、(1,0)、(1,1)- 静态断言
杜绝错误:比如访问者 int分支返回int,double分支返回string,直接编译报错。 - 完美转发
static_cast<T&&>= std::forward,保留左值 / 右值,零拷贝。
6.2.3._Visit_impl 策略选择器(性能核心)
template <size_t _Size, class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>constexpr _Ret _Visit_impl(_Callable&& _Obj, _Variants&&... _Args) {// 根据总状态数,自动选择最优分发策略constexpr int _Strategy =_Size == 1 ? 0 // 只有1种类型:直接调用: _Size <= 4 ? 1 // ≤4:switch 4个case: _Size <= 16 ? 2 // ≤16:switch 16个case: _Size <= 64 ? 3 // ≤64:switch 64个case: _Size <= 256 ? 4 // ≤256:switch 256个case: -1; // >256:函数指针数组(避免代码膨胀)// 计算【全局索引】:把多个variant的index合并为一个数字size_t _Idx = _Variant_visit_index1(0, static_cast<_As_variant<_Variants>&>(_Args)...);// 转发给对应策略return _Visit_strategy<_Strategy>::template _Visit2<_Ret, _ListOfIndexVectors>(_Idx, std::forward<_Callable>(_Obj), std::forward<_Variants>(_Args)...);}
关键点:
- 为什么分策略?
switch-case:CPU 分支预测拉满,速度最快(小状态数首选); - 函数指针数组
:避免 switch生成上万行代码,内存更优(大状态数首选)。 _Variant_visit_index1把多个 variant.index()压缩成一个全局索引,用于查表 /switch。例:var1.index()=1+var2.index()=0→ 全局索引 =2。
_NODISCARD constexpr size_t _Variant_visit_index1(const size_t _Acc) noexcept {return _Acc;}template <class _FirstTy, class... _RestTys>_NODISCARD constexpr size_t _Variant_visit_index1(size_t _Acc, const _FirstTy& _First, const _RestTys&... _Rest) noexcept {// calculate a canonical index from the biased indices of the variants _First and _Rest..._Acc += (_First.index() + 1) * _Variant_total_states<_RestTys...>;return _STD _Variant_visit_index1(_Acc, _Rest...);
6.2.4._Visit_strategy 分发策略实现
策略 -1:超大状态数(>256)→ 函数指针数组
template <>struct _Visit_strategy<-1> {template <class _Ret, class _ListOfIndexVectors, class _Callable, class... _Variants>static constexpr _Ret _Visit2(size_t _Idx, _Callable&& _Obj, _Variants&&... _Args) {static_assert(_Size > 256);// 编译期生成:函数指针数组(每个元素对应一种类型组合)constexpr auto& _Array = _Variant_dispatch_table<...>::_Array;// 运行期:O(1) 索引查表调用return _Array[_Idx](std::forward<_Callable>(_Obj), std::forward<_Variants>(_Args)...);}};
策略 0:只有 1 种状态 → 直接调用(无分发开销)
template <>struct _Visit_strategy<0> {template <class _Ret, class, class _Callable>static constexpr _Ret _Visit2(size_t, _Callable&& _Obj) {// 直接调用访问者,无任何跳转ifconstexpr(is_void_v<_Ret>)return static_cast<void>(std::forward<_Callable>(_Obj)());elsereturn std::forward<_Callable>(_Obj)();}};
6.2.5.宏代码生成(最精妙的部分)
STL 用宏批量生成 switch-case,不用手写重复代码。
1.单个 case 宏
#define _STL_CASE(n) \case (n): \if constexpr ((n) < _Size) { \using _Indices = _Meta_at_c<_ListOfIndexVectors, (n)>; \// 核心:根据索引调用std::get取值,转发给访问者return _Variant_dispatcher<_Indices>::template _Dispatch2<_Ret, ...>( \std::forward<_Callable>(_Obj), std::forward<_Variants>(_Args)...); \} \_STL_UNREACHABLE; \[[fallthrough]]
2.批量生成 case 宏(4/16/64/256 个)
// 生成4个case#define _STL_STAMP4(n, x) x(n);x(n+1);x(n+2);x(n+3)// 生成16个case(4×4)#define _STL_STAMP16(n, x) _STL_STAMP4(n,x);_STL_STAMP4(n+4,x);...// 生成64/256个case#define _STL_STAMP64 ...#define _STL_STAMP256 ...// 顶层宏:触发生成#define _STL_STAMP(n, x) x(_STL_STAMP##n, n)
3.策略 1~4:switch 分发(最常用)
// 策略1:≤4 个状态template <> struct _Visit_strategy<1> {template <...> static constexpr _Ret _Visit2(...) {_STL_STAMP(4, _STL_VISIT_STAMP); // 编译期展开:switch + 4个case}};// 策略2:≤16 → 16个case// 策略3:≤64 → 64个case// 策略4:≤256 → 256个case
6.2.6.最终分发 _Variant_dispatcher
这是最底层调用,等价于我们手写的:
case 2: vis(std::get<1>(var1), std::get<0>(var2));7.总结
- 优先使用重载 lambda 模式
:通过 overloaded结构体组合多个 lambda,代码清晰、类型安全 - 保持 visitor 简洁
:每个重载处理单一类型,避免复杂逻辑,提升可读性与可维护性 - 使用完美转发
:参数使用 auto&&避免不必要拷贝,提高性能 - 明确返回类型
:当返回类型不统一时,显式指定返回类型,避免编译错误 - 处理空 variant
:确保 variant 始终有活跃值,或在访问前检查
std::visit 是 C++17 为 std::variant 提供的核心访问机制,通过访问者模式实现类型安全、编译期完备性检查和高效的类型分发。它不仅简化了 variant 的使用,还大幅提升了代码的安全性和可维护性,是现代 C++ 中处理变体类型的首选工具。
夜雨聆风