Skip to content

Wiki

std::variant

About 15 min read

Existing English translations were reused. 3 current source blocks remain in Simplified Chinese, without triggering paid translation.

1.What problem does this section solve?

Sometimes a variable needs to store a value that "might be an int, might be a string, or might be a double." The traditional approach is union (C language), but it is not type-safe—you do not know which type is currently stored, and accessing it incorrectly will cause a crash.

std::variant is a type-safe union that can store one of multiple types andknows which type is currently stored.

2.What is this feature?

std::variant<T1, T2, ...> is a type-safe union introduced in C++17. At any given time, it stores the value of only one of its types. When accessed, the compiler will check for you, so the problem of "accessing the wrong type" does not occur.

3.C++ Standard Edition

C++17

4.Required header files

#include <variant>

5.Basic syntax

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

Operation Description
v = value; Assignment (automatic type switching)
v.emplace<T>(args...) In-place construction
std::get<T>(v) Get value (throws if the type is incorrect: std::bad_variant_access)
std::get_if<T>(&v) Safe get (returns nullptr if the type is incorrect)
v.index() Return the index of the current type (0-based)
std::visit(visitor, v) Process using the visitor pattern
std::holds_alternative<T>(v) Determine whether it holds type T

7.Example code

7.1.Example 1: Basic usage of variant—storing values of different types

#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;
}

Running result:

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

7.2.Example 2: Based on Example 1, use get_if for safe access

#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;
}

Running result:

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

7.3.Example 3: Based on Example 2, use std::visit to handle all types

#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;
}

Running result:

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

7.4.Example 4: Based on Example 3, use variant to represent the message type

#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;
}

Running result:

Text: Hello World
Number: 42
Quit!

7.5.std::visit and the Visitor mechanism

In the above code:

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

This sentence may look rather strange at first.

First remember std::visit the most basic form:

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

Among them:

  • The second parameter is std::variant
  • The first parameter is acallable object (Callable)
  • std::visit and will, based on the variantcurrently stored data type, call the corresponding handler function

For example:

Message msg;

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

At this point, msg what is stored internally is:

TextMessage

Therefore, std::visit will extract this TextMessage and hand it over to MessageHandler for processing.


7.5.1.MessageHandler{}What is it?

Here:

MessageHandler{}

It is neither a function nor special syntax.

It just created a temporary MessageHandler object.

For example:

MessageHandler handler;

and:

MessageHandler{}

The object type created is the same, except that the latter is a temporary object.

Therefore:

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

It can also be written as:

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

7.5.2.Why can a struct be called like a function?

Because MessageHandler is overloaded:

operator()

For example:

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

After creating the object:

MessageHandler handler;

You can directly call it like this:

handler(TextMessage{"Hello"});

It looks like it is calling a function.

In fact, this is equivalent to:

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

So this kind of overloaded:

operator()

The objects of also called:

Function Object / Functor.


7.5.3.Why are there three operator() here?

Because:

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

Message may store three different types of data:

TextMessage
NumberMessage
QuitMessage

Therefore, MessageHandler three handler functions are prepared respectively:

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

Processing:

TextMessage

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

Processing:

NumberMessage

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

Processing:

QuitMessage

Although their function names are all:

operator()

However, the parameter types are different, so it isfunction overloading.


7.5.4.std::visit What exactly was done?

For example:

msg = TextMessage{"Hello World"};

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

At this point, msg stores:

TextMessage

It can be roughly understood as std::visit did:

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

Thus, the compiler finds:

void operator()(const TextMessage& msg) const

Final output:

Text: Hello World

For another example:

msg = NumberMessage{42};

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

It can be roughly understood as:

MessageHandler{}(NumberMessage{42});

Then call:

void operator()(const NumberMessage& msg) const

Output:

Number: 42

Finally:

msg = QuitMessage{};

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

then it will select:

void operator()(const QuitMessage&) const

Output:

Quit!

The entire process can be understood as:

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

std::visitThe function of is:

Check variant which type is currently stored, and then automatically call the function in the visitor that can handle this type.


7.5.5.std::visit Must the first parameter be a struct?

No.

std::visitThe first parameter essentially only requires being a:

Callable.

That is, as long as something can be called as follows:

对象(参数);

it can serve as a visitor.

Common callable objects include:

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

The above example uses:

MessageHandler{}

It belongs to:

结构体对象
    ↓
重载 operator()
    ↓
函数对象 Functor
    ↓
可以作为 std::visit 的 Visitor

7.5.6.Why can a Lambda also serve as a Visitor?

For example:

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

Here the first parameter is a Lambda.

A lambda itself is also a callable object.

For example:

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

f(10);

Can be called like a function:

f(10);

Therefore, it can also be passed to:

std::visit

In practice, Lambda can be roughly understood as the compiler automatically generating an anonymous function object:

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

So conceptually speaking:

Lambda

and:

重载了 operator() 的 struct/class

Very similar.


7.5.7.Why is this example more suitable for using a struct Visitor?

If there is only one simple operation, Lambda is very convenient:

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

However, here completely different logic needs to be executed for different types:

TextMessage
NumberMessage
QuitMessage

Using multiple operator() overloads can be very intuitive:

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

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

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

In this way, each message type has its own independent processing logic.


7.5.8.Core Understanding

You can:

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

Understand it by splitting it into two parts.

First:

MessageHandler{}

Indicates:

创建一个 MessageHandler 临时对象

Because it overloads:

operator()

So it is a callable object.

Then:

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

Responsible for:

查看 msg 当前保存的类型
        ↓
取出对应的数据
        ↓
调用 MessageHandler 对应的 operator()

Therefore:

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

It can be summarized in one sentence:

Based on msg the currently saved data type, let MessageHandler automatically select the corresponding operator() for processing.

It can also be written as:

std::visit(visitor, variant);

Namely:

visit
 │
 ├── 看 variant 当前是什么类型
 │
 ├── 把里面的数据取出来
 │
 └── 用这个数据调用 visitor

Here, visitor is not a fixed syntax, but any object that can be called.

In this example:

MessageHandler{}

就是一个通过重载 operator() 实现的函数对象。

8.Operation result

See the "run results" for each example above.

9.Key syntax explanation in the example

Example What was discussed Newly introduced syntax Why it is written this way Notes
Example 1 Basic assignment and get std::variant<int, double, string>、std::get<T>(v) variant is type-safe and automatically switches types on assignment get<T> an incorrect type will throw an exception
Example 2 get_if safe access std::get_if<T>(&v) Returns a pointer; returns nullptr if the type is incorrect Safer than get, recommended for use
Example 3 visit pattern std::visit(lambda, v) visit forces coverage of all types and is the best way to access a variant Generic lambda + visit is the most concise combination
Example 4 Message dispatch pattern struct visitor + variant Use variant + visitor to implement type-safe message handling The visitor must provide operator() for every type

10.variant suitable for "one of a finite number of types"

variantIt is not meant to replace all inheritance and polymorphism. It is best suited for situations where the number of type variants is limited and you want the compiler to remind you to handle every case.

Scenario Recommendation
Messages have only three types: Text / Number / Quit std::variant
States have only a few types: Idle / Running / Error std::variant
Parse results may be int / double / string std::variant
There are many type kinds and plugins need to be extended at runtime Inheritance + virtual functions
All objects share one interface Polymorphic interfaces are more natural

10.1.Example 5: Representing a state machine with 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;
}

Running result:

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

The state here can only ever be one of three. Compared to using int state_code plus a bunch of extra fields, variant can place the data required for each state within the corresponding type, reducing issues like "reading the running field even though the state is error."

11.Common Errors

Error 1: get called with the wrong type throws an exception

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

Correct approach: first use std::holds_alternative<double>(v) to check, or use std::get_if.

Error 2: Default construction when there is no default type in the variant

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

这种情况是合法的,但如果第一种类型没有默认构造函数,则编译失败。

Error 3: The visitor of visit does not cover all types

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

Correct approach: the visitor of visit must provide for all types in the variant a operator(), or use a generic lambda.

12.Usage Recommendations

  1. 替代 union:variant 类型安全,知道当前存的是什么。
  2. 用 std::visit + 泛型 lambda 是最简洁的访问方式。
  3. 需要"知道当前是哪种类型"时用 std::get_if:返回指针,安全高效。
  4. 用 variant + visit 实现消息/事件分发:模式匹配的雏形。
  5. variant 的大小是所有类型中最大的 + 索引字段:不要存太多大类型。
  6. 类型种类有限时用 variant 更清晰:如果类型需要随插件扩展,继承和虚函数通常更合适。

13.Summary

  • std::variant<T1, T2, ...> is a type-safe union.
  • std::get<T>(v) Direct access (unsafe),std::get_if<T>(&v) return pointer (safe).
  • std::visit(visitor, v) is the most recommended approach, enforcing coverage of all types.
  • Suitable for scenarios such as message dispatch, optional configuration, and state machines.