std::optional과 std::variant 실전 활용
C++17은 선택적 값을 표현하는 std::optional과 타입 안전 유니온 std::variant를 표준에 추가했습니다. 이 두 타입은 nullptr 역참조, C 스타일 유니온의 타입 혼용 같은 오류를 컴파일 타임에 방지합니다.
1. std::optional
섹션 제목: “1. std::optional”1.1 개념과 생성
섹션 제목: “1.1 개념과 생성”std::optional<T>는 T 타입의 값 또는 “값 없음”을 표현합니다.
#include <optional>#include <string>
std::optional<int> findValue(bool found) { if (found) return 42; return std::nullopt; // 값 없음}
auto result = findValue(true);auto empty = findValue(false);
std::cout << result.has_value() << "\n"; // 1std::cout << empty.has_value() << "\n"; // 01.2 값 접근
섹션 제목: “1.2 값 접근”std::optional<std::string> name = "Alice";
// 방법 1: value() — 비어있으면 std::bad_optional_access 예외std::cout << name.value() << "\n";
// 방법 2: *연산자 — UB if emptystd::cout << *name << "\n";
// 방법 3: value_or() — 기본값 지정std::cout << name.value_or("Unknown") << "\n";
// 방법 4: 조건 확인 후 접근if (name) { std::cout << *name << "\n";}1.3 실전 패턴 — 검색 결과 반환
섹션 제목: “1.3 실전 패턴 — 검색 결과 반환”struct Player { int id; std::string name; float health;};
std::optional<Player> findPlayer(const std::vector<Player>& players, int id) { for (const auto& p : players) { if (p.id == id) return p; } return std::nullopt;}
auto player = findPlayer(players, 42);if (player) { std::cout << "찾은 플레이어: " << player->name << "\n";} else { std::cout << "플레이어 없음\n";}1.4 std::optional 체이닝 (C++23)
섹션 제목: “1.4 std::optional 체이닝 (C++23)”// C++23: and_then, transform, or_elsestd::optional<int> parseAge(const std::string& s) { /* ... */ }std::optional<std::string> getInput() { /* ... */ }
auto age = getInput() .and_then(parseAge) // optional<int> .transform([](int a) { // optional<int> return a * 2; }) .value_or(0);2. std::variant
섹션 제목: “2. std::variant”2.1 개념과 생성
섹션 제목: “2.1 개념과 생성”std::variant<T1, T2, ...>는 나열된 타입 중 하나를 보유하는 타입 안전 유니온입니다.
#include <variant>
std::variant<int, double, std::string> v;
v = 42; // int 보유v = 3.14; // double로 변경v = "hello"; // string으로 변경
// 현재 보유 타입 인덱스std::cout << v.index() << "\n"; // 2 (string이 2번째)2.2 값 접근
섹션 제목: “2.2 값 접근”std::variant<int, std::string> v = "world";
// get<T>: 타입 불일치 시 std::bad_variant_accessstd::string& s = std::get<std::string>(v);
// get_if<T>: 타입 불일치 시 nullptr 반환if (auto* p = std::get_if<std::string>(&v)) { std::cout << *p << "\n";}
// holds_alternative<T>: 타입 확인if (std::holds_alternative<std::string>(v)) { std::cout << "string 보유\n";}2.3 std::visit
섹션 제목: “2.3 std::visit”std::visit는 variant에 들어있는 모든 타입에 대해 함수를 적용합니다.
std::variant<int, double, std::string> v = 3.14;
std::visit([](const auto& val) { std::cout << val << "\n";}, v);2.4 오버로드 패턴
섹션 제목: “2.4 오버로드 패턴”여러 람다를 하나의 방문자(visitor)로 묶는 패턴입니다.
template<typename... Ts>struct Overloaded : Ts... { using Ts::operator()...;};
// C++20: 추론 가이드 없이도 동작// C++17: 명시적 가이드 필요template<typename... Ts>Overloaded(Ts...) -> Overloaded<Ts...>;
std::variant<int, double, std::string> v = "hello";
std::visit(Overloaded{ [](int i) { std::cout << "int: " << i << "\n"; }, [](double d) { std::cout << "double: " << d << "\n"; }, [](const std::string& s){ std::cout << "string: " << s << "\n"; }}, v);// string: hello3. std::monostate — 빈 상태 표현
섹션 제목: “3. std::monostate — 빈 상태 표현”variant의 첫 번째 타입으로 std::monostate를 사용하면 “아무것도 아닌” 상태를 표현할 수 있습니다.
#include <variant>#include <monostate>
// 초기화되지 않은 상태를 표현std::variant<std::monostate, int, std::string> result;
result = 42;result = std::monostate{}; // 초기화 해제
if (std::holds_alternative<std::monostate>(result)) { std::cout << "결과 없음\n";}4. 에러 처리 패턴
섹션 제목: “4. 에러 처리 패턴”4.1 optional을 이용한 오류 반환
섹션 제목: “4.1 optional을 이용한 오류 반환”std::optional<int> divide(int a, int b) { if (b == 0) return std::nullopt; return a / b;}
if (auto result = divide(10, 0)) { std::cout << "결과: " << *result << "\n";} else { std::cout << "0으로 나눌 수 없음\n";}4.2 variant를 이용한 Result 타입
섹션 제목: “4.2 variant를 이용한 Result 타입”struct ParseError { std::string message; };struct NetworkError { int code; };
using Error = std::variant<ParseError, NetworkError>;using Result = std::variant<int, Error>;
Result loadValue() { // 성공 return 42; // 또는 실패 // return Error{ParseError{"잘못된 형식"}};}
std::visit(Overloaded{ [](int value) { std::cout << "성공: " << value << "\n"; }, [](const Error& e) { std::visit(Overloaded{ [](const ParseError& pe) { std::cout << "파싱 오류: " << pe.message << "\n"; }, [](const NetworkError& ne) { std::cout << "네트워크 오류: " << ne.code << "\n"; } }, e); }}, loadValue());5. 게임 개발 활용 사례
섹션 제목: “5. 게임 개발 활용 사례”5.1 아이템 속성 variant
섹션 제목: “5.1 아이템 속성 variant”using StatValue = std::variant<int, float, bool, std::string>;
struct ItemStat { std::string name; StatValue value;};
std::vector<ItemStat> stats = { {"damage", 100}, {"speed", 1.5f}, {"magical", true}, {"element", std::string("fire")}};
for (const auto& stat : stats) { std::cout << stat.name << ": "; std::visit([](const auto& v) { std::cout << v; }, stat.value); std::cout << "\n";}5.2 컴포넌트 조회 결과
섹션 제목: “5.2 컴포넌트 조회 결과”struct Transform { float x, y, z; };struct Rigidbody { float mass; };struct Renderer { int meshId; };
using Component = std::variant<Transform, Rigidbody, Renderer>;
std::optional<Component> getComponent(int entityId, int compType) { // 컴포넌트 조회 // ... return std::nullopt;}
if (auto comp = getComponent(1, 0)) { if (auto* t = std::get_if<Transform>(&*comp)) { std::cout << "위치: " << t->x << ", " << t->y << "\n"; }}| 타입 | 용도 | 대체 전 패턴 |
|---|---|---|
std::optional<T> | 값이 있을 수도 없을 수도 있을 때 | T*, -1 센티넬 값 |
std::variant<T...> | 여러 타입 중 하나를 안전하게 보유 | C 스타일 union |
std::monostate | variant의 “빈” 상태 | 별도 bool 플래그 |
두 타입 모두 타입 안전성을 보장하며 널 포인터 역참조나 유니온 타입 오류를 컴파일 타임에 방지합니다.