[2026] C++ variant | Type-safe union Complete Guide

[2026] C++ variant | Type-safe union Complete Guide

이 글의 핵심

std::variant is a type-safe union introduced in C++17. It can store a value of one of several types, and keeps track of which type it is currently storing. Unlike C's union, it provides type safety and automatic life cycle management.

Entering

std::variant is a type safe union introduced in C++17. It can store a value of one of several types, and keeps track of which type it is currently storing. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
#include <string>
int main() {
    // std::variant<int, double, std::string>:
    // A type that can store one of int, double, and string values.
    // When creating the default, initialized to the first type (int) → 0
    std::variant<int, double, std::string> v;
    
    // Value assignment: automatic conversion according to type
    v = 42;           // Store int (automatically destroy old value)
    // std::get<type>: Extract stored value as type
    // std::bad_variant_access exception if types do not match
    std::cout << std::get<int>(v) << std::endl;
    
    v = 3.14;         // Save double (destroy int, create double)
    std::cout << std::get<double>(v) << std::endl;
    
    v = "hello";      // Save string (double destruction, string creation)
    // String literal → std::string automatic conversion
    std::cout << std::get<std::string>(v) << std::endl;
    
    return 0;
}

Why do you need it?:

  • Type safety: Unlike union, it keeps track of the current type.
  • Automatic Lifecycle: Automatic call of destructor
  • Exception Safety: Safe exception handling when value changes.
  • Pattern matching: Handle all types with std::visit

1. variant vs union

Comparison table

Featuresunionstd::variant
Type Safe❌ None✅ Available
Type Tracking❌ Manual✅ Automatic
Non-trivial type❌ Not possible✅ Available
Destructor❌ Manual✅ Automatic
Exception Safe❌ None✅ Available
Copy/Move❌ Manual✅ Automatic

Code comparison

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <iostream>
#include <string>
#include <variant>
// ❌ C union: type unsafe, manual management
union OldUnion {
    int i;
    double d;
    // std::string s;  // Error: Non-trivial type not possible
};
void testUnion() {
    OldUnion u;
    u.i = 42;
    std::cout << u.i << std::endl;  // 42
    
    u.d = 3.14;
    // std::cout << u.i << std::endl;  // UB (don't know what type it is)
    std::cout << u.d << std::endl;  // 3.14
}
// ✅ std::variant: type safe, automatically managed
void testVariant() {
    std::variant<int, double, std::string> v;
    
    v = 42;
    std::cout << std::get<int>(v) << std::endl;  // 42
    
    v = 3.14;  // Automatic destruction of old int, creation of double
    std::cout << std::get<double>(v) << std::endl;  // 3.14
    
    v = "hello";  // Double destruction, string creation
    std::cout << std::get<std::string>(v) << std::endl;  // hello
    
    // Check type
    if (std::holds_alternative<std::string>(v)) {
std::cout << "Current type: string" << std::endl;
    }
}
int main() {
    std::cout << "=== union ===" << std::endl;
    testUnion();
    
    std::cout << "\n=== variant ===" << std::endl;
    testVariant();
    
    return 0;
}

output of power: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

=== union ===
42
3.14
=== variant ===
42
3.14
hello
Current type: string

2. Basic use

Creation and allocation

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
#include <string>
int main() {
    // Basic creation (first type)
    std::variant<int, double, std::string> v1;  // int{} = 0
std::cout << "Index: " << v1.index() << std::endl;  // 0
std::cout << "Value: " << std::get<0>(v1) << std::endl;  // 0
    
    // Created by value
    std::variant<int, double, std::string> v2 = 42;
std::cout << "Index: " << v2.index() << std::endl;  // 0
    
    // change value
    v2 = 3.14;
std::cout << "Index: " << v2.index() << std::endl;  // 1
    
    v2 = "hello";
std::cout << "Index: " << v2.index() << std::endl;  // 2
    
    return 0;
}

output of power: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

Index: 0
Value: 0
Index: 0
Index: 1
Index: 2

Value access

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
int main() {
    std::variant<int, double> v = 42;
    
    // get: by type
    int x = std::get<int>(v);
    std::cout << "get<int>: " << x << std::endl;
    
    // get: by index
    int y = std::get<0>(v);
    std::cout << "get<0>: " << y << std::endl;
    
    // get_if: returns pointer (safe)
    if (auto* ptr = std::get_if<int>(&v)) {
        std::cout << "get_if<int>: " << *ptr << std::endl;
    }
    
    if (auto* ptr = std::get_if<double>(&v)) {
        std::cout << "get_if<double>: " << *ptr << std::endl;
    } else {
std::cout << "not a double" << std::endl;
    }
    
    // holds_alternative
    if (std::holds_alternative<int>(v)) {
std::cout << "int type" << std::endl;
    }
    
    return 0;
}

output of power: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

get<int>: 42
get<0>: 42
get_if<int>: 42
not a double
int type

3. std::visit

Basic visit

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
#include <string>
int main() {
    std::variant<int, double, std::string> v = 42;
    
    // perform appropriate processing depending on the current type of std::visit: variant
    // First argument: visitor function (handles all possible types)
    // Second argument: variant object
    std::visit( {
        // auto&&: universal reference (accepts all types)
        // using T = std::decay_t<decltype(arg)>:
        // Extract the actual type of arg (remove the reference, const)
        using T = std::decay_t<decltype(arg)>;
        
        // if constexpr: compile-time branching
        // Only branches matching the current type are compiled.
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "int: " << arg << std::endl;
        } else if constexpr (std::is_same_v<T, double>) {
            std::cout << "double: " << arg << std::endl;
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "string: " << arg << std::endl;
        }
    }, v);
    
    // Visit again after changing the value
    v = 3.14;
    std::visit( {
        using T = std::decay_t<decltype(arg)>;
        
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "int: " << arg << std::endl;
        } else if constexpr (std::is_same_v<T, double>) {
            std::cout << "double: " << arg << std::endl;
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "string: " << arg << std::endl;
        }
    }, v);
    
    return 0;
}

output of power:

int: 42
double: 3.14

Overload pattern

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
#include <string>
// Overload helper: combine multiple lambdas into one function object
// If you provide a different lambda for each type, the compiler will choose the appropriate one.
template<class....Ts>
struct overloaded : Ts....{
    // using Ts::operator()...: Inheriting operator() from all base classes
    // Enables all call operators of each lambda
    using Ts::operator()...;
};
// Deduction guide: Inferring template type from constructor arguments
// overloaded{Lambda1, Lambda2, ...} → overloaded<Lambda1 type, Lambda2 type, ...>
template<class....Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main() {
    std::variant<int, double, std::string> v = "hello";
    
    // overloaded pattern: Provides different lambdas for each type
    // The compiler selects a lambda that matches the type of the current variant.
    // Readable and type safe (enforces all type handling)
    std::visit(overloaded{
         { std::cout << "int: " << x << std::endl; },
         { std::cout << "double: " << x << std::endl; },
         { std::cout << "string: " << x << std::endl; }
    }, v);
    
    v = 42;
    std::visit(overloaded{
         { std::cout << "int: " << x << std::endl; },
         { std::cout << "double: " << x << std::endl; },
         { std::cout << "string: " << x << std::endl; }
    }, v);
    
    return 0;
}

output of power:

string: hello
int: 42

4. Practical example

Example 1: State Machine

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
#include <string>
// overload helper
template<class....Ts>
struct overloaded : Ts....{
    using Ts::operator()...;
};
template<class....Ts>
overloaded(Ts...) -> overloaded<Ts...>;
struct Idle {};
struct Running { int progress; };
struct Completed { std::string result; };
using State = std::variant<Idle, Running, Completed>;
class Task {
    State state = Idle{};
    
public:
    void start() {
        state = Running{0};
std::cout << "start operation" << std::endl;
    }
    
    void update(int progress) {
        if (auto* running = std::get_if<Running>(&state)) {
            running->progress = progress;
std::cout << "Progress: " << progress << "%" << std::endl;
            
            if (progress >= 100) {
state = Completed{"Success"};
std::cout << "Operation completed" << std::endl;
            }
        }
    }
    
    void printState() const {
        std::visit(overloaded{
{ std::cout << "Status: Waiting" << std::endl; },
{ std::cout << "Status: In progress (" << r.progress << "%)" << std::endl; },
{ std::cout << "status: completed (" << c.result << ")" << std::endl; }
        }, state);
    }
};
int main() {
    Task task;
    
    task.printState();
    task.start();
    task.printState();
    task.update(50);
    task.printState();
    task.update(100);
    task.printState();
    
    return 0;
}

output of power: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

Status: Pending
start work
Status: In Progress (0%)
Progress: 50%
Status: In Progress (50%)
Progress: 100%
job done
Status: Completed (success)

Example 2: Error handling

#include <variant>
#include <string>
#include <iostream>
template<class....Ts>
struct overloaded : Ts....{
    using Ts::operator()...;
};
template<class....Ts>
overloaded(Ts...) -> overloaded<Ts...>;
template<typename T, typename E>
using Result = std::variant<T, E>;
struct Error {
    std::string message;
};
Result<int, Error> divide(int a, int b) {
    if (b == 0) {
return Error{"Cannot divide by 0"};
    }
    return a / b;
}
Result<int, Error> squareRoot(int x) {
    if (x < 0) {
return Error{"Cannot find square root of negative number"};
    }
    return static_cast<int>(std::sqrt(x));
}
int main() {
    auto result1 = divide(10, 2);
    std::visit(overloaded{
{ std::cout << "Result: " << value << std::endl; },
{ std::cout << "Error: " << err.message << std::endl; }
    }, result1);
    
    auto result2 = divide(10, 0);
    std::visit(overloaded{
{ std::cout << "Result: " << value << std::endl; },
{ std::cout << "Error: " << err.message << std::endl; }
    }, result2);
    
    auto result3 = squareRoot(16);
    std::visit(overloaded{
{ std::cout << "square root: " << value << std::endl; },
{ std::cout << "Error: " << err.message << std::endl; }
    }, result3);
    
    return 0;
}

output of power:

Result: 5
Error: cannot divide by 0
square root: 4

Example 3: Command pattern

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
#include <string>
#include <vector>
template<class....Ts>
struct overloaded : Ts....{
    using Ts::operator()...;
};
template<class....Ts>
overloaded(Ts...) -> overloaded<Ts...>;
struct CreateCommand {
    std::string name;
};
struct UpdateCommand {
    int id;
    std::string newValue;
};
struct DeleteCommand {
    int id;
};
using Command = std::variant<CreateCommand, UpdateCommand, DeleteCommand>;
class CommandProcessor {
public:
    void execute(const Command& cmd) {
        std::visit(overloaded{
             {
std::cout << "Create: " << c.name << std::endl;
            },
             {
std::cout << "Update: ID=" << c.id << ", value=" << c.newValue << std::endl;
            },
             {
std::cout << "Delete: ID=" << c.id << std::endl;
            }
        }, cmd);
    }
};
int main() {
    CommandProcessor processor;
    
    std::vector<Command> commands = {
        CreateCommand{"user1"},
        UpdateCommand{1, "new_value"},
        DeleteCommand{1}
    };
    
    for (const auto& cmd : commands) {
        processor.execute(cmd);
    }
    
    return 0;
}

output of power:

Created by: user1
Update: ID=1, value=new_value
Delete: ID=1

5. Frequently occurring problems

Problem 1: Invalid type

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
int main() {
    std::variant<int, double> v = 42;
    
    // ❌ Wrong type
    try {
        double d = std::get<double>(v);  // std::bad_variant_access
    } catch (const std::bad_variant_access& e) {
std::cout << "Type mismatch: " << e.what() << std::endl;
    }
    
    // ✅ Access after confirmation
    if (std::holds_alternative<int>(v)) {
        int x = std::get<int>(v);
        std::cout << "int: " << x << std::endl;
    }
    
    // ✅ Use get_if (safe)
    if (auto* ptr = std::get_if<double>(&v)) {
        std::cout << "double: " << *ptr << std::endl;
    } else {
std::cout << "not a double" << std::endl;
    }
    
    return 0;
}

output of power:

Type mismatch: std::bad_variant_access
int: 42
not a double

Issue 2: Base Creation

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
int main() {
    // Create default with first type
    std::variant<int, double> v1;  // int{} = 0
    std::cout << "v1: " << std::get<int>(v1) << std::endl;  // 0
    
    // explicit initialization
    std::variant<int, double> v2 = 3.14;  // double
    std::cout << "v2: " << std::get<double>(v2) << std::endl;  // 3.14
    
    // in_place_type
    std::variant<int, double> v3(std::in_place_type<double>, 2.71);
    std::cout << "v3: " << std::get<double>(v3) << std::endl;  // 2.71
    
    return 0;
}

output of power:

v1: 0
v2: 3.14
v3: 2.71

Issue 3: References

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <iostream>
#include <functional>
int main() {
    int x = 42;
    
    // ❌ Cannot save reference
    // std::variant<int&> v{x};
    
    // ✅ Use reference_wrapper
    std::variant<std::reference_wrapper<int>> v1{std::ref(x)};
    v1.get().get() = 100;
    std::cout << "x: " << x << std::endl;  // 100
    
    // ✅ Use pointers
    std::variant<int*> v2{&x};
    *std::get<int*>(v2) = 200;
    std::cout << "x: " << x << std::endl;  // 200
    
    return 0;
}

output of power:

x: 100
x: 200

6. Practical example: JSON value expression

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <variant>
#include <vector>
#include <map>
#include <iostream>
#include <string>
template<class....Ts>
struct overloaded : Ts....{
    using Ts::operator()...;
};
template<class....Ts>
overloaded(Ts...) -> overloaded<Ts...>;
class JsonValue {
public:
    using Value = std::variant<
        std::nullptr_t,
        bool,
        int64_t,
        double,
        std::string
    >;
    
    Value value_;
    
    JsonValue() : value_(nullptr) {}
    JsonValue(Value v) : value_(std::move(v)) {}
    
    template<typename T>
    bool is() const {
        return std::holds_alternative<T>(value_);
    }
    
    template<typename T>
    const T& as() const {
        return std::get<T>(value_);
    }
    
    void print() const {
        std::visit(overloaded{
             { std::cout << "null"; },
             { std::cout << (b ? "true" : "false"); },
             { std::cout << i; },
             { std::cout << d; },
             { std::cout << '"' << s << '"'; }
        }, value_);
    }
};
int main() {
    std::vector<JsonValue> values = {
        JsonValue(nullptr),
        JsonValue(true),
        JsonValue(int64_t(42)),
        JsonValue(3.14),
        JsonValue(std::string("hello"))
    };
    
std::cout << "JSON values: [";
    for (size_t i = 0; i < values.size(); ++i) {
        if (i > 0) std::cout << ", ";
        values[i].print();
    }
    std::cout << "]" << std::endl;
    
    // Check type
    if (values[2].is<int64_t>()) {
std::cout << "values[2] is int64_t: " << values[2].as<int64_t>() << std::endl;
    }
    
    return 0;
}

output of power:

JSON values: [null, true, 42, 3.14, "hello"]
values[2] is int64_t: 42

organize

Key takeaways

  1. variant: type safe union
  2. Type tracking: Automatically tracking the current type
  3. std::visit: Handles all types
  4. Overload pattern: Lambda by type
  5. Practical: State machines, error handling, instruction patterns

variant vs union

Featuresunionstd::variant
Type Safe
Type Tracking
Non-trivial type
DestructorManualautomatic
Copy/MoveManualautomatic

Practical tips

Principle of use:

  • One of several types
  • Requires type safety
  • state machine
  • Error handling Performance:
  • Stack Allocation
  • Size: largest type + index
  • Check runtime type
  • visit overhead (small) caution:
  • Type confirmation required
  • Cannot save reference
  • Duplicate types are not allowed
  • First type default

Next steps


Good article to read together (internal link)

Here’s another article related to this topic.

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intent of the code clear?
  • Are there enough test cases?
  • Is it documented? Use this checklist to reduce mistakes and improve code quality.

Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, variant, union, visit, C++17, etc.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3