CC++ & Algorithm

variant变体类型与visit访问:一个盒子,但只能放一种类型——像个变形金刚

极难3
语言版本:通用
概述:学习C++中的variant,它可以存储多种类型中的一种,就像一个能变形但一次只能变一种形态的玩具,用visit优雅地访问。

变形金刚与万能口袋:认识C++17里的variant变体类型

你有没有见过那种可以变成机器人、汽车、恐龙的多功能玩具?一次只能变一种形态,但想变哪种就变哪种。在编程里,有时也需要一个变量,它能表示多种不同种类的东西——比如你的零花钱可能是整数(100元),也可能是字符(“红包”),还可能是小数(5.5元)。但在一段程序运行中,它只能取其中一种值。这种“多选一”的需求,C++17引入了一个新工具: variant(变体类型)

想象你有一个随身口袋,里面可以放“零花钱”、“零食名称”或“作业分数”,但一次只能放一样。variant就像这个口袋,它知道现在口袋里装的是什么,而且绝不会搞错。


1. 生活中的例子:为什么需要variant?

  • 游戏道具:一个宝箱可能开出金币(整数数量)、武器(字符串名字)或药水(浮点数回复量)。游戏里需要用一个变量来表示这个道具,但它的类型不固定。
  • 考试分数:老师记录成绩,有些科目是百分制(整数90),有些是等级制(字符串“优秀”),有些是小数点分(浮点9.5)。用variant<int, string, double>就能存放任意一种。
  • 零食清单:妈妈让你记下想买的零食,可能是“薯片”(字符串),也可能是“3包”(整数表示数量),或者是“5.5元”(浮点价格)。同一个笔记本里就能混合记录。

在C++中,以前可以用union(联合体)实现类似效果,但union不记得当前存放的是哪个类型,很容易用错导致程序崩溃。variant是类型安全的升级版,它自动记录当前类型,并且在访问时检查是否匹配。


2. 核心概念:variant是什么,怎么用?

(1)定义与基本操作

variant定义在头文件<variant>中。它的写法像函数模板:

variant<类型1, 类型2, ... , 类型N> 变量名;

例如,一个能存放整数、字符串或浮点数的变量:

#include <variant>
variant<int, string, double> pocket; // 口袋,默认放第一个类型 int,初始值为0

常用操作汇总

操作说明示例
v = 值赋值,自动切换类型pocket = 100; // 放整数100
get<int>(v)按类型获取值(如果类型唯一)int money = get<int>(pocket);
get<索引>(v)按下标获取值(索引从0开始)string name = get<1>(pocket);
get_if<int>(&v)安全获取指针,不匹配返回空指针if( auto* p = get_if<int>(&pocket) )
holds_alternative<int>(v)检查当前是否存储int类型if( holds_alternative<int>(pocket) )
visit(函数对象, v)根据实际类型自动调用对应函数visit(...)
v.index()返回当前类型在模板参数中的位置(0起始)size_t idx = pocket.index();

注意:如果试图用get<int>获取一个当前存储的是string的variant,程序会抛出bad_variant_access异常。这就像你伸手去口袋拿零花钱,结果里面放的是薯片,手会被卡住——所以要先确认类型。

(2)visit——万能访问器

visit是处理variant最强大的工具。它像一个智能管家:你给他一个“处理各种情况的函数”,它自动检查variant现在是什么类型,然后调用对应的处理逻辑。你不用写一堆if-else判断类型。

语法

visit( lambda表达式或函数对象 , variant变量 );

其中lambda必须能处理所有可能的类型。推荐使用“泛型lambda”(参数写auto),配合if constexpr在编译时选择分支。

例如,打印任意类型:

visit([](auto value) {
    using T = decltype(value);
    if constexpr (is_same_v<T, int>) {
        cout << "整数: " << value;
    } else if constexpr (is_same_v<T, string>) {
        cout << "字符串: " << value;
    } else if constexpr (is_same_v<T, double>) {
        cout << "浮点数: " << value;
    }
}, pocket);

为什么用if constexpr
因为lambdaauto会生成一个函数模板,但所有代码都会被编译。如果直接写cout << value;,当valuestring时可以,但如果是int也可以——实际上所有类型都能<<输出,所以可以简单写。但如果有不同类型需要不同处理(比如整数加1,字符串首字母大写),就必须用if constexpr来让编译器只保留对应分支的代码。

visit的妙用

  • 计算总价值:每个类型价值不同,但统一用visit返回double值。
  • 根据类型执行不同动作:如整数打印数量,字符串打印名称,浮点数打印分数。

(3)如何实现“空状态”?

有时variant可能没有值(比如还没开宝箱)。C++提供了monostate(空状态类型),它是一个特殊的占位类型,没有任何实际数据。定义:

variant<monostate, int, string> emptyPocket;
// 默认空状态:holds_alternative<monostate>(emptyPocket) 为 true

然后可以赋值切换为其他类型。monostate就像一个“空”的盒子,方便表示“暂无内容”。

(4)关于性能

variant的大小等于它所能存储的最大类型的大小,再加上一点管理标志。例如variant<int, string>中,string占24字节左右(取决于编译器),所以整个variant大小大约是24字节加少量开销。对于简单类型(int、float),性能和普通变量很接近;对于复杂类型(string、vector),拷贝和赋值会有开销,但通常可以接受。


3. 新手容易犯的错误

  1. 忘记处理所有可能类型
    如果用visit,lambda必须能处理所有类型,否则编译报错。比如定义variant<int, string, double>,但lambda只处理了int和string,未处理double,编译器会提示“调用歧义”或“没有匹配函数”。

  2. 使用get不判断类型直接访问
    容易导致bad_variant_access异常。应当先用holds_alternativeget_if检查。

  3. 赋值时类型错误

    variant<int, string> v;
    v = 3.14;   // double不能隐式转换,报错
    

    需要明确类型:v = 3.14f;(float)或v = string("hello");

  4. visit的lambda里用了if constexpr但写错条件
    if constexpr的条件必须是编译期常量表达式,比如is_same_v<T, int>。如果写成if (typeid(value) == typeid(int)),那是运行时判断,if constexpr不适用。

  5. 认为get<索引>get<类型>可以混用
    get<索引>直接按顺序,get<类型>当类型唯一时才可用。如果有重复类型(如variant<int, int>),编译器会报错,因为类型不唯一。

  6. 忘记包含头文件
    <variant>是必须的。同时<iostream><string>等也要包含。


4. 完整可运行代码:零花钱管理

下面模拟一个“零花钱记录器”:今天妈妈给你的可能是一张整数钞票(金额),也可能是一句叮嘱(字符串),还可能是一个水果(浮点数重量?)。用variant存储,并用visit处理。

#include <iostream>
#include <variant>
#include <string>
#include <vector>

using namespace std;

// 定义零花钱类型:整数(金额)、字符串(消息)、浮点数(重量水果)
using Pocket = variant<int, string, double>;

// 使用visit打印每种情况
void showPocket(const Pocket& p) {
    visit([](const auto& value) {
        using T = decay_t<decltype(value)>;
        if constexpr (is_same_v<T, int>) {
            cout << "给你 " << value << " 元零花钱";
        } else if constexpr (is_same_v<T, string>) {
            cout << "妈妈留言: \"" << value << "\"";
        } else if constexpr (is_same_v<T, double>) {
            cout << "送你一个 " << value << " 斤的大西瓜";
        }
    }, p);
    cout << "!" << endl;
}

// 估价:钱就是钱,留言价值0.5元(鼓励你努力学习),西瓜每斤2元
double valueOf(const Pocket& p) {
    return visit([](const auto& value) -> double {
        using T = decay_t<decltype(value)>;
        if constexpr (is_same_v<T, int>) {
            return static_cast<double>(value);  // 直接返回钱数
        } else if constexpr (is_same_v<T, string>) {
            return 0.5;  // 留言暖心价
        } else if constexpr (is_same_v<T, double>) {
            return value * 2.0;  // 每斤2元
        } else {
            return 0.0;
        }
    }, p);
}

int main() {
    // 1. 创建几个零花钱实例
    Pocket day1 = 50;                    // 整数:50元
    Pocket day2 = string("考100分!");    // 字符串:留言
    Pocket day3 = 3.5;                   // 浮点数:3.5斤西瓜

    // 2. 错误的访问示范(会抛出异常)
    try {
        int money = get<int>(day2);      // day2是string,出错
    } catch (const bad_variant_access& e) {
        cout << "错误: " << e.what() << endl; // 输出说明
    }

    // 3. 安全访问:使用holds_alternative判断
    if (holds_alternative<int>(day1)) {
        cout << "day1中有整数: " << get<int>(day1) << "元" << endl;
    }

    // 4. 使用get_if避免异常
    if (auto* p = get_if<string>(&day2)) {
        cout << "day2的消息: " << *p << endl;
    }

    // 5. 赋值切换类型
    day1 = "加餐!买了一包薯条";   // 现在day1变成string
    cout << "\nday1变成了: ";
    showPocket(day1);

    // 6. 用向量存储一堆零花钱记录,用visit统一处理
    vector<Pocket> week = {day1, day2, day3, Pocket(10), Pocket(2.0)}; // 最后两个是整数10和浮点2.0
    cout << "\n本周零花钱记录:\n";
    for (const auto& p : week) {
        showPocket(p);
    }

    // 7. 计算总价值
    double total = 0.0;
    for (const auto& p : week) {
        total += valueOf(p);
    }
    cout << "\n本周总价值: " << total << " 元" << endl;

    // 8. 利用index()获取类型索引
    cout << "day2的索引: " << day2.index() << endl; // int=0, string=1, double=2

    // 9. 用monostate实现“空口袋”
    variant<monostate, int, string> emptyPocket;
    if (holds_alternative<monostate>(emptyPocket)) {
        cout << "emptyPocket是空的" << endl;
    }
    emptyPocket = 100;  // 放入100元
    if (holds_alternative<int>(emptyPocket)) {
        cout << "现在里面有: " << get<int>(emptyPocket) << "元" << endl;
    }

    return 0;
}

代码解释

  • Pocket类型为variant<int, string, double>
  • showPocket用visit输出友好提示。
  • valueOf用visit计算每个口袋的价值,返回double。
  • 错误访问被try-catch捕获,展示如何安全处理。
  • 最后用monostate演示空状态。

运行结果示例(部分):

错误: bad variant access
day1中有整数: 50元
day2的消息: 考100分!

day1变成了: 妈妈留言: "加餐!买了一包薯条"!

本周零花钱记录:
妈妈留言: "加餐!买了一包薯条"!
妈妈留言: "考100分!"!
送你一个 3.5 斤的大西瓜!
给你 10 元零花钱!
送你一个 2 斤的大西瓜!

本周总价值: 56.5 元
day2的索引: 1
emptyPocket是空的
现在里面有: 100元

5. Python中的等效实现(动态类型版本)

Python没有专门的variant,但动态类型本来就允许变量随时改变类型。不过,为了代码清晰,我们通常使用类型注解(Union)和isinstance判断。Python 3.10引入了match/case(模式匹配),语法与C++的visit非常相似。

from typing import Union, List

# 定义Pocket类型:可以是int, str, float
Pocket = Union[int, str, float]

def show_pocket(p: Pocket) -> None:
    # 用match语句(Python 3.10+)实现类似visit的效果
    match p:
        case int(money):
            print(f"给你 {money} 元零花钱!")
        case str(msg):
            print(f"妈妈留言: \"{msg}\"!")
        case float(weight):
            print(f"送你一个 {weight} 斤的大西瓜!")
        case _:
            print("未知类型")

def value_of(p: Pocket) -> float:
    match p:
        case int(money):
            return float(money)
        case str(_):
            return 0.5
        case float(weight):
            return weight * 2.0
        case _:
            return 0.0

def main():
    day1: Pocket = 50
    day2: Pocket = "考100分!"
    day3: Pocket = 3.5

    # 切换类型
    day1 = "加餐!买了一包薯条"

    week: List[Pocket] = [day1, day2, day3, 10, 2.0]
    print("本周零花钱记录:")
    for p in week:
        show_pocket(p)

    total = sum(value_of(p) for p in week)
    print(f"\n本周总价值: {total} 元")

    # 模拟空口袋:使用 None
    empty: Union[Pocket, None] = None
    if empty is None:
        print("empty是空的")
    empty = 100
    if isinstance(empty, int):
        print(f"现在里面有: {empty} 元")

if __name__ == "__main__":
    main()

说明:Python的match/case比一串if isinstance更整洁。但注意,Union注解只起提示作用,运行时若不检查类型直接操作可能出错。


6. 小练习:用variant实现一个“答题卡”

题目:考试时,一道题的答案可能是一个整数(选择题编号)、一个字符串(填空题答案)、或一个小数(计算题结果)。给一个variant存储答案,并用visit判断对错。再计算总分(每题1分)。

提示:定义答案正确值对照,用visit返回是否匹配。


7. 延伸学习

  • std::any:如果连类型列表都不确定,可以用any(万能类型),但它需要显式转换才能取出值,性能也比variant低。
  • std::optional:表示“有值或没有值”,用于可能空的情况,与variant<monostate,T>相似但更轻量。
  • 模式匹配(C++23):未来的C++版本可能会有更原生的模式匹配语法,让visit更简洁。

variant是C++17带来的一大利器,它让“多类型变量”变得安全、高效、易读。下次你写程序时,如果遇到“一个变量可能有多种形态”,记得试试这个变形金刚——variant

例题精讲

1单选题

关于 C++17 中的 std::variant,下列哪个说法是正确的?

Astd::variant 可以同时存储其所有指定类型的值
Bstd::variant 的大小等于其最大可存储类型的大小加上一个整数标记位
Cstd::variant 在默认构造时会始终构造第一个类型的空值
D使用 std::get 访问 variant 的值时,若类型不符合会返回空指针
2判断题

如果一个 std::variant<int, double, string> 被默认构造,那么它存储的是 int 类型的默认值 0。

3填空题
使用 std::visit 访问 std::variant<int, double, string> 类型的变量 v,并打印其内容。补全以下代码。

#include <variant>
#include <iostream>
int main() {
    std::variant<int, double, std::string> v = 3.14;
    std::visit(___, v);
}
4单选题

已知 std::variant<int, double> v = 42; 若执行 int* p = std::get_if<int>(&v); 则 p 的值为?

A42
B指向 42 的指针
Cnullptr
D指向 variant 内部数据的指针
5填空题
补全代码,使用 std::visit 和重载的 lambda 实现对 std::variant<int, double, std::string> 类型变量的访问:若为 int 则输出 "int: " 加值;若为 double 则输出 "double: " 加值;若为 string 则输出 "string: " 加值。

#include <variant>
#include <iostream>
#include <string>

int main() {
    std::variant<int, double, std::string> v = "hello";
    std::visit(___ {
        std::cout << "int: " << arg;
    }, ___ {
        std::cout << "double: " << arg;
    }, ___ {
        std::cout << "string: " << arg;
    }, v);
}