第 18.15 節

std::variant

0瀏覽次數0訪問次數--跳出率--平均停留

本節解決什麼問題

有時候一個變量需要存儲"可能是 int,也可能是 string,也可能是 double"的值。傳統的做法是 union(C 語言),但它不類型安全——你不知道當前存的是哪種類型,訪問錯了就崩潰。

std::variant 是類型安全的聯合體,能存儲多種類型之一,並且知道當前存的是哪種類型

這個特性是什麼

std::variant<T1, T2, ...> 是 C++17 引入的類型安全的聯合體。在同一時刻,它只存儲其中一種類型的值。訪問時編譯器會幫你檢查,不會出現"訪問了錯誤類型"的問題。

C++ 標準版本

C++17

需要的頭文件

#include <variant>

基本語法

std::variant<int, double, std::string> v;

v = 42;                      // 存 int
v = 3.14;                    // 存 double
v = std::string("hello");   // 存 string

// 访问方式 1:std::get<T>(v) —— 类型不对抛异常
int n = std::get<int>(v);

// 访问方式 2:std::get_if<T>(&v) —— 类型不对返回 nullptr
if (auto* p = std::get_if<int>(&v)) { ... }

// 访问方式 3:std::visit —— 用 visitor 模式处理所有可能的类型
std::visit([](auto&& val) { ... }, v);

// 查询当前存储的类型的索引
size_t idx = v.index();  // 0-based

常用用法

操作說明
v = value;賦值(自動切換類型)
v.emplace<T>(args...)原地構造
std::get<T>(v)獲取值(類型不對拋 std::bad_variant_access
std::get_if<T>(&v)安全獲取(類型不對返回 nullptr)
v.index()返回當前類型的索引(0-based)
std::visit(visitor, v)用 visitor 模式處理
std::holds_alternative<T>(v)判斷是否持有 T 類型

示例代碼

示例 1:variant 基本用法——存不同類型的值

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

int main()
{
    // v 可以存 int、double 或 string
    std::variant<int, double, std::string> v;

    v = 42;
    std::cout << "int: " << std::get<int>(v) << "\n";

    v = 3.14;
    std::cout << "double: " << std::get<double>(v) << "\n";

    v = std::string("hello");
    std::cout << "string: " << std::get<std::string>(v) << "\n";

    // 查看当前类型索引
    std::cout << "current index: " << v.index() << "\n";  // 2 (string)

    return 0;
}

運行結果

int: 42
double: 3.14
string: hello
current index: 2

示例 2:在示例 1 基礎上,用 get_if 安全訪問

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

void print_value(const std::variant<int, double, std::string>& v)
{
    // 安全方式:逐个尝试,get_if 返回指针
    if (auto* p = std::get_if<int>(&v))
    {
        std::cout << "int: " << *p << "\n";
    }
    else if (auto* p = std::get_if<double>(&v))
    {
        std::cout << "double: " << *p << "\n";
    }
    else if (auto* p = std::get_if<std::string>(&v))
    {
        std::cout << "string: " << *p << "\n";
    }
}

int main()
{
    std::variant<int, double, std::string> v;

    v = 42;
    print_value(v);

    v = 3.14159;
    print_value(v);

    v = std::string("C++17");
    print_value(v);

    return 0;
}

運行結果

int: 42
double: 3.14159
string: C++17

示例 3:在示例 2 基礎上,用 std::visit 處理所有類型

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

int main()
{
    std::variant<int, double, std::string> v;

    // std::visit 配合泛型 lambda 优雅处理所有类型
    auto printer = [](const auto& val) {
        std::cout << "value: " << val << "\n";
    };

    v = 42;
    std::visit(printer, v);

    v = 2.718;
    std::visit(printer, v);

    v = std::string("hello variant");
    std::visit(printer, v);

    // 也可以返回不同类型的值
    auto to_double = [](const auto& val) -> double {
        if constexpr (std::is_same_v<std::decay_t<decltype(val)>, std::string>)
        {
            return 0.0;  // string 不能转 double
        }
        else
        {
            return static_cast<double>(val);
        }
    };

    v = 10;
    std::cout << "to_double: " << std::visit(to_double, v) << "\n";

    return 0;
}

運行結果

value: 42
value: 2.718
value: hello variant
to_double: 10

示例 4:在示例 3 基礎上,用 variant 表示消息類型

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

// 定义消息类型
struct TextMessage { std::string text; };
struct NumberMessage { int number; };
struct QuitMessage {};

using Message = std::variant<TextMessage, NumberMessage, QuitMessage>;

// 处理消息的 visitor
struct MessageHandler
{
    void operator()(const TextMessage& msg) const
    {
        std::cout << "Text: " << msg.text << "\n";
    }
    void operator()(const NumberMessage& msg) const
    {
        std::cout << "Number: " << msg.number << "\n";
    }
    void operator()(const QuitMessage&) const
    {
        std::cout << "Quit!\n";
    }
};

int main()
{
    Message msg;

    msg = TextMessage{"Hello World"};
    std::visit(MessageHandler{}, msg);

    msg = NumberMessage{42};
    std::visit(MessageHandler{}, msg);

    msg = QuitMessage{};
    std::visit(MessageHandler{}, msg);

    return 0;
}

運行結果

Text: Hello World
Number: 42
Quit!

運行結果

見上方每個示例的"運行結果"。

示例中的關鍵語法解釋

示例講了什麼新出現的語法爲什麼這樣寫注意事項
示例 1基本賦值和 getstd::variant<int, double, string>std::get<T>(v)variant 類型安全,賦值時自動切換類型get<T> 類型不對會拋異常
示例 2get_if 安全訪問std::get_if<T>(&v)返回指針,類型不對返回 nullptr比 get 更安全,推薦使用
示例 3visit 模式std::visit(lambda, v)visit 強制覆蓋所有類型,是 variant 的最佳訪問方式泛型 lambda + visit 是最簡潔的組合
示例 4消息分發模式struct visitor + variant用 variant + visitor 實現類型安全的消息處理visitor 必須爲每種類型都提供 operator()

variant 適合"有限幾種類型之一"

variant 不是爲了替代所有繼承和多態。它最適合這種情況:類型種類有限,而且你希望編譯器提醒你把每種情況都處理掉。

場景推薦
消息只有 Text / Number / Quit 三類std::variant
狀態只有 Idle / Running / Error 幾類std::variant
解析結果可能是 int / double / stringstd::variant
類型種類很多且需要運行時擴展插件繼承 + 虛函數
所有對象共享一套接口多態接口更自然

示例 5:用 variant 表示狀態機

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

struct Idle {};
struct Running
{
    int task_id;
};
struct Error
{
    std::string message;
};

// variant 表示一个变量可以在多个候选类型中保存其中一种。
using State = std::variant<Idle, Running, Error>;

void print_state(const State& state)
{
    // visit 会根据 variant 当前保存的类型调用对应处理逻辑。
    std::visit([](const auto& s) {
        using T = std::decay_t<decltype(s)>;

        if constexpr (std::is_same_v<T, Idle>)
        {
            std::cout << "state: idle\n";
        }
        else if constexpr (std::is_same_v<T, Running>)
        {
            std::cout << "state: running task " << s.task_id << "\n";
        }
        else if constexpr (std::is_same_v<T, Error>)
        {
            std::cout << "state: error " << s.message << "\n";
        }
    }, state);
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    State state = Idle{};
    print_state(state);

    state = Running{42};
    print_state(state);

    state = Error{"motor timeout"};
    print_state(state);

    return 0;
}

運行結果

state: idle
state: running task 42
state: error motor timeout

這裏的狀態永遠只能是三種之一。相比用 int state_code 加一堆額外字段,variant 能把每種狀態需要的數據放在對應類型裏,減少“錯誤狀態卻還讀 running 字段”這類問題。

常見錯誤

錯誤 1:get 用錯類型拋異常

std::variant<int, double> v = 42;
std::cout << std::get<double>(v);  // ❌ 抛出 std::bad_variant_access!

正確做法:先用 std::holds_alternative<double>(v) 檢查,或用 std::get_if

錯誤 2:variant 中沒有默認類型時默認構造

std::variant<int, std::string> v;  // 默认构造第一个类型的默认值(int = 0)

這種情況是合法的,但如果第一種類型沒有默認構造函數,則編譯失敗。

錯誤 3:visit 的 visitor 沒有覆蓋所有類型

struct Visitor {
    void operator()(int) {}
    // 缺少 double 和 string 的 operator()
};
std::variant<int, double, std::string> v;
std::visit(Visitor{}, v);  // ❌ 编译错误!

正確做法:visit 的 visitor 必須爲 variant 中所有類型提供 operator(),或者用泛型 lambda。

使用建議

  1. 替代 union:variant 類型安全,知道當前存的是什麼。
  2. std::visit + 泛型 lambda 是最簡潔的訪問方式
  3. 需要"知道當前是哪種類型"時用 std::get_if:返回指針,安全高效。
  4. 用 variant + visit 實現消息/事件分發:模式匹配的雛形。
  5. variant 的大小是所有類型中最大的 + 索引字段:不要存太多大類型。
  6. 類型種類有限時用 variant 更清晰:如果類型需要隨插件擴展,繼承和虛函數通常更合適。

小結

  • std::variant<T1, T2, ...> 是類型安全的聯合體。
  • std::get<T>(v) 直接獲取(不安全),std::get_if<T>(&v) 返回指針(安全)。
  • std::visit(visitor, v) 是最推薦的方式,強制覆蓋所有類型。
  • 適用於消息分發、可選配置、狀態機等場景。
音乐页