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
| 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&& 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
- 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
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
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.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ optional | ‘선택적 값’ 가이드
- C++ optional·variant·any | ‘nullptr 체크 지겹다’ C++17 타입 안전 처리
- C++ expected | ‘에러 처리’ 가이드
Keywords Covered in This Article (Related Search Terms)
This article covers C++, variant, union, visit, C++17.