C++ variant | Type-safe union 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
| Features | union | std::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
- variant: type safe union
- Type tracking: Automatically tracking the current type
- std::visit: Handles all types
- Overload pattern: Lambda by type
- Practical: State machines, error handling, instruction patterns
variant vs union
| Features | union | std::variant |
|---|---|---|
| Type Safe | ❌ | ✅ |
| Type Tracking | ❌ | ✅ |
| Non-trivial type | ❌ | ✅ |
| Destructor | Manual | automatic |
| Copy/Move | Manual | automatic |
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
- C++ optional
- C++ any
- C++ Union
Good article to read together (internal link)
Here’s another article related to this topic.
- C++ Union and Variant | “Type Safe Unions” Guide
- C++ optional | “Optional Values” Guide
- C++ any | “Type erasure” guide
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.
Related articles
- C++ std::variant vs union |
- C++ Union and Variant |
- C++ Algorithm Set |
- C++ any |
- Modern C++ (C++11~C++20) Core Grammar Cheat Sheet | A glance at frequently used items in the workplace