跳到正文

Wiki

std::variant

約 8 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

1.本節解決什麼問題

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

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

2.這個特性是什麼

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

3.C++ 標準版本

C++17

4.需要的標頭檔案

#include <variant>

5.基本語法

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

6.常用用法

操作 說明
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 型別

7.示例程式碼

7.1.示例 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

7.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

7.3.示例 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

7.4.示例 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!

7.5.std::visit 與 Visitor 機制

上面的程式碼中:

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

這一句剛開始看起來可能比較奇怪。

先記住 std::visit 最基本的形式:

std::visit(visitor, variant对象);

其中:

  • 第二個引數是 std::variant
  • 第一個引數是一個可呼叫物件(Callable)
  • std::visit 會根據 variant 當前儲存的資料型別,呼叫對應的處理函式

例如:

Message msg;

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

此時 msg 內部儲存的是:

TextMessage

因此 std::visit 會把這個 TextMessage 取出來,並交給 MessageHandler 處理。


7.5.1.MessageHandler{} 是什麼?

這裡:

MessageHandler{}

不是函式,也不是特殊語法。

它就是建立了一個臨時的 MessageHandler 物件。

例如:

MessageHandler handler;

和:

MessageHandler{}

建立的物件型別是一樣的,只不過後者是臨時物件。

因此:

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

也可以寫成:

MessageHandler handler;
std::visit(handler, msg);

7.5.2.為什麼一個結構體可以像函式一樣呼叫?

因為 MessageHandler 過載了:

operator()

例如:

struct MessageHandler
{
    void operator()(const TextMessage& msg) const
    {
        std::cout << "Text: " << msg.text << "\n";
    }
};

建立物件以後:

MessageHandler handler;

就可以直接這樣呼叫:

handler(TextMessage{"Hello"});

看起來像是在呼叫一個函式。

實際上等價於:

handler.operator()(TextMessage{"Hello"});

所以這種過載了:

operator()

的物件也叫:

函式物件(Function Object / Functor)


7.5.3.為什麼這裡有三個 operator()

因為:

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

Message 可能儲存三種不同的資料:

TextMessage
NumberMessage
QuitMessage

因此 MessageHandler 分別準備了三個處理函式:

void operator()(const TextMessage& msg) const
{
    std::cout << "Text: " << msg.text << "\n";
}

處理:

TextMessage

void operator()(const NumberMessage& msg) const
{
    std::cout << "Number: " << msg.number << "\n";
}

處理:

NumberMessage

void operator()(const QuitMessage&) const
{
    std::cout << "Quit!\n";
}

處理:

QuitMessage

它們雖然函式名都是:

operator()

但是引數型別不同,因此屬於函式過載


7.5.4.std::visit 到底做了什麼?

例如:

msg = TextMessage{"Hello World"};

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

此時 msg 儲存的是:

TextMessage

可以粗略理解為 std::visit 做了:

MessageHandler{}(TextMessage{"Hello World"});

於是編譯器找到:

void operator()(const TextMessage& msg) const

最終輸出:

Text: Hello World

再例如:

msg = NumberMessage{42};

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

可以粗略理解成:

MessageHandler{}(NumberMessage{42});

於是呼叫:

void operator()(const NumberMessage& msg) const

輸出:

Number: 42

最後:

msg = QuitMessage{};

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

則會選擇:

void operator()(const QuitMessage&) const

輸出:

Quit!

整個過程可以理解為:

                         std::variant

              ┌───────────────┼───────────────┐
              │               │               │
       TextMessage      NumberMessage     QuitMessage
              │               │               │
              ▼               ▼               ▼
 operator(TextMessage) operator(NumberMessage) operator(QuitMessage)

std::visit 的作用就是:

檢視 variant 當前儲存的是哪一種型別,然後自動呼叫 visitor 中能夠處理這個型別的函式。


7.5.5.std::visit 第一個引數必須是結構體嗎?

不是。

std::visit 第一個引數本質上只要求是一個:

可呼叫物件(Callable)

也就是說,只要一個東西能夠像下面這樣呼叫:

对象(参数);

就可以作為 visitor。

常見的可呼叫物件包括:

普通函数
Lambda
函数对象(重载 operator() 的 class / struct)
std::function

上面的例子使用的是:

MessageHandler{}

它屬於:

结构体对象

重载 operator()

函数对象 Functor

可以作为 std::visit 的 Visitor

7.5.6.Lambda 為什麼也能作為 Visitor?

例如:

std::visit(
    [](const auto& msg)
    {
        std::cout << "收到一条消息\n";
    },
    msg
);

這裡第一個引數就是一個 Lambda。

Lambda 本身也是一種可呼叫物件。

例如:

auto f = [](int x)
{
    std::cout << x << "\n";
};

f(10);

能夠像函式一樣呼叫:

f(10);

因此它也可以傳給:

std::visit

實際上,可以把 Lambda 粗略理解成編譯器自動生成了一個匿名的函式物件:

struct 某个匿名类型
{
    void operator()(int x) const
    {
        std::cout << x << "\n";
    }
};

所以從思想上來說:

Lambda

和:

重载了 operator() 的 struct/class

非常相似。


7.5.7.為什麼這個例子更適合用結構體 Visitor?

如果只有一種簡單操作,Lambda 很方便:

[](const auto& value)
{
    // ...
}

但是這裡需要針對不同型別執行完全不同的邏輯:

TextMessage
NumberMessage
QuitMessage

使用多個 operator() 過載會非常直觀:

struct MessageHandler
{
    void operator()(const TextMessage& msg) const
    {
        // 处理文本消息
    }

    void operator()(const NumberMessage& msg) const
    {
        // 处理数字消息
    }

    void operator()(const QuitMessage&) const
    {
        // 处理退出消息
    }
};

這樣每種訊息型別都有自己獨立的處理邏輯。


7.5.8.核心理解

可以把:

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

拆成兩個部分理解。

首先:

MessageHandler{}

表示:

创建一个 MessageHandler 临时对象

由於它過載了:

operator()

所以它是一個可呼叫物件。

然後:

std::visit(..., msg);

負責:

查看 msg 当前保存的类型

取出对应的数据

调用 MessageHandler 对应的 operator()

因此:

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

可以概括成一句話:

根據 msg 當前儲存的資料型別,讓 MessageHandler 自動選擇對應的 operator() 進行處理。

也可以記成:

std::visit(visitor, variant);

即:

visit

 ├── 看 variant 当前是什么类型

 ├── 把里面的数据取出来

 └── 用这个数据调用 visitor

其中 visitor 並不是某一種固定語法,而是任何能夠被呼叫的物件。

在本例中:

MessageHandler{}

就是一個通過過載 operator() 實現的函式物件。

8.執行結果

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

9.示例中的關鍵語法解釋

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

10.variant 適合"有限幾種型別之一"

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

場景 推薦
訊息只有 Text / Number / Quit 三類 std::variant
狀態只有 Idle / Running / Error 幾類 std::variant
解析結果可能是 int / double / string std::variant
型別種類很多且需要執行時擴充套件外掛 繼承 + 虛擬函式
所有物件共享一套介面 多型介面更自然

10.1.示例 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 欄位”這類問題。

11.常見錯誤

錯誤 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。

12.使用建議

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

13.小結

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