Wiki
std::variant
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
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.使用建議
- 替代
union:variant 型別安全,知道當前存的是什麼。 - 用
std::visit+ 泛型 lambda 是最簡潔的訪問方式。 - 需要"知道當前是哪種型別"時用
std::get_if:返回指標,安全高效。 - 用 variant + visit 實現訊息/事件分發:模式匹配的雛形。
- variant 的大小是所有型別中最大的 + 索引欄位:不要存太多大型別。
- 型別種類有限時用 variant 更清晰:如果型別需要隨外掛擴充套件,繼承和虛擬函式通常更合適。
13.小結
std::variant<T1, T2, ...>是型別安全的聯合體。std::get<T>(v)直接獲取(不安全),std::get_if<T>(&v)返回指標(安全)。std::visit(visitor, v)是最推薦的方式,強制覆蓋所有型別。- 適用於訊息分發、可選配置、狀態機等場景。