C++ variant | "타입 안전 union" 가이드
이 글의 핵심
std::variant 는 C++17에서 도입된 타입 안전한 union입니다. 여러 타입 중 하나의 값을 저장할 수 있으며, 현재 어떤 타입을 저장하고 있는지 추적합니다. C의 union과 달리 타입 안전성과 자동 생명주기 관리를 제공합니다.
들어가며
**std::variant**는 C++17에서 도입된 타입 안전한 union입니다. 여러 타입 중 하나의 값을 저장할 수 있으며, 현재 어떤 타입을 저장하고 있는지 추적합니다.
#include <variant>
#include <iostream>
#include <string>
int main() {
// std::variant<int, double, std::string>:
// int, double, string 중 하나의 값을 저장할 수 있는 타입
// 기본 생성 시 첫 번째 타입(int)으로 초기화 → 0
std::variant<int, double, std::string> v;
// 값 할당: 타입에 맞게 자동 변환
v = 42; // int 저장 (이전 값 자동 소멸)
// std::get<타입>: 저장된 값을 타입으로 추출
// 타입이 맞지 않으면 std::bad_variant_access 예외
std::cout << std::get<int>(v) << std::endl;
v = 3.14; // double 저장 (int 소멸, double 생성)
std::cout << std::get<double>(v) << std::endl;
v = "hello"; // string 저장 (double 소멸, string 생성)
// 문자열 리터럴 → std::string 자동 변환
std::cout << std::get<std::string>(v) << std::endl;
return 0;
}
왜 필요한가?:
- 타입 안전: union과 달리 현재 타입을 추적
- 자동 생명주기: 소멸자 자동 호출
- 예외 안전: 값 변경 시 안전한 예외 처리
- 패턴 매칭:
std::visit로 모든 타입 처리
1. variant vs union
비교표
| 특징 | union | std::variant |
|---|---|---|
| 타입 안전 | ❌ 없음 | ✅ 있음 |
| 타입 추적 | ❌ 수동 | ✅ 자동 |
| 비trivial 타입 | ❌ 불가 | ✅ 가능 |
| 소멸자 | ❌ 수동 | ✅ 자동 |
| 예외 안전 | ❌ 없음 | ✅ 있음 |
| 복사/이동 | ❌ 수동 | ✅ 자동 |
코드 비교
#include <iostream>
#include <string>
#include <variant>
// ❌ C union: 타입 불안전, 수동 관리
union OldUnion {
int i;
double d;
// std::string s; // 에러: 비trivial 타입 불가
};
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 (어떤 타입인지 모름)
std::cout << u.d << std::endl; // 3.14
}
// ✅ std::variant: 타입 안전, 자동 관리
void testVariant() {
std::variant<int, double, std::string> v;
v = 42;
std::cout << std::get<int>(v) << std::endl; // 42
v = 3.14; // 이전 int 자동 소멸, double 생성
std::cout << std::get<double>(v) << std::endl; // 3.14
v = "hello"; // double 소멸, string 생성
std::cout << std::get<std::string>(v) << std::endl; // hello
// 타입 확인
if (std::holds_alternative<std::string>(v)) {
std::cout << "현재 타입: string" << std::endl;
}
}
int main() {
std::cout << "=== union ===" << std::endl;
testUnion();
std::cout << "\n=== variant ===" << std::endl;
testVariant();
return 0;
}
출력:
=== union ===
42
3.14
=== variant ===
42
3.14
hello
현재 타입: string
2. 기본 사용
생성과 할당
#include <variant>
#include <iostream>
#include <string>
int main() {
// 기본 생성 (첫 번째 타입)
std::variant<int, double, std::string> v1; // int{} = 0
std::cout << "인덱스: " << v1.index() << std::endl; // 0
std::cout << "값: " << std::get<0>(v1) << std::endl; // 0
// 값으로 생성
std::variant<int, double, std::string> v2 = 42;
std::cout << "인덱스: " << v2.index() << std::endl; // 0
// 값 변경
v2 = 3.14;
std::cout << "인덱스: " << v2.index() << std::endl; // 1
v2 = "hello";
std::cout << "인덱스: " << v2.index() << std::endl; // 2
return 0;
}
출력:
인덱스: 0
값: 0
인덱스: 0
인덱스: 1
인덱스: 2
값 접근
#include <variant>
#include <iostream>
int main() {
std::variant<int, double> v = 42;
// get: 타입으로
int x = std::get<int>(v);
std::cout << "get<int>: " << x << std::endl;
// get: 인덱스로
int y = std::get<0>(v);
std::cout << "get<0>: " << y << std::endl;
// get_if: 포인터 반환 (안전)
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 << "double이 아님" << std::endl;
}
// holds_alternative
if (std::holds_alternative<int>(v)) {
std::cout << "int 타입" << std::endl;
}
return 0;
}
출력:
get<int>: 42
get<0>: 42
get_if<int>: 42
double이 아님
int 타입
3. std::visit
기본 visit
#include <variant>
#include <iostream>
#include <string>
int main() {
std::variant<int, double, std::string> v = 42;
// std::visit: variant의 현재 타입에 따라 적절한 처리 수행
// 첫 번째 인자: 방문자 함수 (모든 가능한 타입을 처리)
// 두 번째 인자: variant 객체
std::visit( {
// auto&&: 유니버설 참조 (모든 타입 받음)
// using T = std::decay_t<decltype(arg)>:
// arg의 실제 타입 추출 (참조, const 제거)
using T = std::decay_t<decltype(arg)>;
// if constexpr: 컴파일 타임 분기
// 현재 타입에 맞는 분기만 컴파일됨
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
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;
}
출력:
int: 42
double: 3.14
오버로드 패턴
#include <variant>
#include <iostream>
#include <string>
// 오버로드 헬퍼: 여러 람다를 하나의 함수 객체로 결합
// 각 타입별로 다른 람다를 제공하면 컴파일러가 적절한 것 선택
template<class... Ts>
struct overloaded : Ts... {
// using Ts::operator()...: 모든 베이스 클래스의 operator() 상속
// 각 람다의 호출 연산자를 모두 사용 가능하게 함
using Ts::operator()...;
};
// Deduction guide: 생성자 인자로부터 템플릿 타입 추론
// overloaded{람다1, 람다2, ...} → overloaded<람다1타입, 람다2타입, ...>
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main() {
std::variant<int, double, std::string> v = "hello";
// overloaded 패턴: 타입별로 다른 람다 제공
// 컴파일러가 현재 variant의 타입에 맞는 람다 선택
// 가독성이 좋고 타입 안전함 (모든 타입 처리 강제)
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;
}
출력:
string: hello
int: 42
4. 실전 예제
예제 1: 상태 머신
#include <variant>
#include <iostream>
#include <string>
// 오버로드 헬퍼
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 << "작업 시작" << std::endl;
}
void update(int progress) {
if (auto* running = std::get_if<Running>(&state)) {
running->progress = progress;
std::cout << "진행: " << progress << "%" << std::endl;
if (progress >= 100) {
state = Completed{"성공"};
std::cout << "작업 완료" << std::endl;
}
}
}
void printState() const {
std::visit(overloaded{
{ std::cout << "상태: 대기 중" << std::endl; },
{ std::cout << "상태: 진행 중 (" << r.progress << "%)" << std::endl; },
{ std::cout << "상태: 완료 (" << 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;
}
출력:
상태: 대기 중
작업 시작
상태: 진행 중 (0%)
진행: 50%
상태: 진행 중 (50%)
진행: 100%
작업 완료
상태: 완료 (성공)
예제 2: 에러 처리
#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{"0으로 나눌 수 없음"};
}
return a / b;
}
Result<int, Error> squareRoot(int x) {
if (x < 0) {
return Error{"음수는 제곱근을 구할 수 없음"};
}
return static_cast<int>(std::sqrt(x));
}
int main() {
auto result1 = divide(10, 2);
std::visit(overloaded{
{ std::cout << "결과: " << value << std::endl; },
{ std::cout << "에러: " << err.message << std::endl; }
}, result1);
auto result2 = divide(10, 0);
std::visit(overloaded{
{ std::cout << "결과: " << value << std::endl; },
{ std::cout << "에러: " << err.message << std::endl; }
}, result2);
auto result3 = squareRoot(16);
std::visit(overloaded{
{ std::cout << "제곱근: " << value << std::endl; },
{ std::cout << "에러: " << err.message << std::endl; }
}, result3);
return 0;
}
출력:
결과: 5
에러: 0으로 나눌 수 없음
제곱근: 4
예제 3: 명령 패턴
#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 << "생성: " << c.name << std::endl;
},
{
std::cout << "업데이트: ID=" << c.id << ", 값=" << c.newValue << std::endl;
},
{
std::cout << "삭제: 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;
}
출력:
생성: user1
업데이트: ID=1, 값=new_value
삭제: ID=1
5. 자주 발생하는 문제
문제 1: 잘못된 타입
#include <variant>
#include <iostream>
int main() {
std::variant<int, double> v = 42;
// ❌ 잘못된 타입
try {
double d = std::get<double>(v); // std::bad_variant_access
} catch (const std::bad_variant_access& e) {
std::cout << "타입 불일치: " << e.what() << std::endl;
}
// ✅ 확인 후 접근
if (std::holds_alternative<int>(v)) {
int x = std::get<int>(v);
std::cout << "int: " << x << std::endl;
}
// ✅ get_if 사용 (안전)
if (auto* ptr = std::get_if<double>(&v)) {
std::cout << "double: " << *ptr << std::endl;
} else {
std::cout << "double이 아님" << std::endl;
}
return 0;
}
출력:
타입 불일치: std::bad_variant_access
int: 42
double이 아님
문제 2: 기본 생성
#include <variant>
#include <iostream>
int main() {
// 첫 번째 타입으로 기본 생성
std::variant<int, double> v1; // int{} = 0
std::cout << "v1: " << std::get<int>(v1) << std::endl; // 0
// 명시적 초기화
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;
}
출력:
v1: 0
v2: 3.14
v3: 2.71
문제 3: 참조
#include <variant>
#include <iostream>
#include <functional>
int main() {
int x = 42;
// ❌ 참조 저장 불가
// std::variant<int&> v{x};
// ✅ reference_wrapper 사용
std::variant<std::reference_wrapper<int>> v1{std::ref(x)};
v1.get().get() = 100;
std::cout << "x: " << x << std::endl; // 100
// ✅ 포인터 사용
std::variant<int*> v2{&x};
*std::get<int*>(v2) = 200;
std::cout << "x: " << x << std::endl; // 200
return 0;
}
출력:
x: 100
x: 200
6. 실전 예제: JSON 값 표현
#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 값들: [";
for (size_t i = 0; i < values.size(); ++i) {
if (i > 0) std::cout << ", ";
values[i].print();
}
std::cout << "]" << std::endl;
// 타입 확인
if (values[2].is<int64_t>()) {
std::cout << "values[2]는 int64_t: " << values[2].as<int64_t>() << std::endl;
}
return 0;
}
출력:
JSON 값들: [null, true, 42, 3.14, "hello"]
values[2]는 int64_t: 42
정리
핵심 요약
- variant: 타입 안전한 union
- 타입 추적: 현재 타입 자동 추적
- std::visit: 모든 타입 처리
- 오버로드 패턴: 타입별 람다
- 실무: 상태 머신, 에러 처리, 명령 패턴
variant vs union
| 특징 | union | std::variant |
|---|---|---|
| 타입 안전 | ❌ | ✅ |
| 타입 추적 | ❌ | ✅ |
| 비trivial 타입 | ❌ | ✅ |
| 소멸자 | 수동 | 자동 |
| 복사/이동 | 수동 | 자동 |
실전 팁
사용 원칙:
- 여러 타입 중 하나
- 타입 안전 필요
- 상태 머신
- 에러 처리
성능:
- 스택 할당
- 크기: 가장 큰 타입 + 인덱스
- 런타임 타입 확인
- visit 오버헤드 (작음)
주의사항:
- 타입 확인 필수
- 참조 저장 불가
- 중복 타입 불가
- 첫 번째 타입 기본값
다음 단계
- C++ optional
- C++ any
- C++ Union
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Union과 Variant | “타입 안전 공용체” 가이드
- C++ optional | “선택적 값” 가이드
- C++ any | “타입 소거” 가이드
관련 글
- C++ std::variant vs union |
- C++ Union과 Variant |
- C++ Algorithm Set |
- C++ any |
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기