본문으로 건너뛰기 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&& arg) {
        // 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([](auto&& arg) {
        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{
        [](int x) { std::cout << "int: " << x << std::endl; },
        [](double x) { std::cout << "double: " << x << std::endl; },
        [](const std::string& x) { std::cout << "string: " << x << std::endl; }
    }, v);
    
    v = 42;
    std::visit(overloaded{
        [](int x) { std::cout << "int: " << x << std::endl; },
        [](double x) { std::cout << "double: " << x << std::endl; },
        [](const std::string& x) { 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{
            [](const Idle&) { std::cout << "Status: Waiting" << std::endl; },
            [](const Running& r) { std::cout << "Status: In progress (" << r.progress << "%)" << std::endl; },
            [](const Completed& c) { 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{
        [](int value) { std::cout << "Result: " << value << std::endl; },
        [](const Error& err) { std::cout << "Error: " << err.message << std::endl; }
    }, result1);
    
    auto result2 = divide(10, 0);
    std::visit(overloaded{
        [](int value) { std::cout << "Result: " << value << std::endl; },
        [](const Error& err) { std::cout << "Error: " << err.message << std::endl; }
    }, result2);
    
    auto result3 = squareRoot(16);
    std::visit(overloaded{
        [](int value) { std::cout << "square root: " << value << std::endl; },
        [](const Error& err) { 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{
            [](const CreateCommand& c) {
                std::cout << "Create: " << c.name << std::endl;
            },
            [](const UpdateCommand& c) {
                std::cout << "Update: ID=" << c.id << ", value=" << c.newValue << std::endl;
            },
            [](const DeleteCommand& c) {
                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::nullptr_t) { std::cout << "null"; },
            [](bool b) { std::cout << (b ? "true" : "false"); },
            [](int64_t i) { std::cout << i; },
            [](double d) { std::cout << d; },
            [](const std::string& s) { 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

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.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

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. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, variant, union, visit, C++17.