본문으로 건너뛰기
Previous
Next
C++ variant | Type-safe union Complete Guide

C++ variant | Type-safe union Complete Guide

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.

#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

#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:

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

2. Basic use

Creation and allocation

#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:

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

Value access

#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:

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

3. std::visit

Basic visit

#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

#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

#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:

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

#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

#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

#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

#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

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


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 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 … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, variant, union, visit, C++17 등으로 검색하시면 이 글이 도움이 됩니다.