콘텐츠로 이동

std::optional과 std::variant 실전 활용

C++17은 선택적 값을 표현하는 std::optional과 타입 안전 유니온 std::variant를 표준에 추가했습니다. 이 두 타입은 nullptr 역참조, C 스타일 유니온의 타입 혼용 같은 오류를 컴파일 타임에 방지합니다.


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"; // 1
std::cout << empty.has_value() << "\n"; // 0
std::optional<std::string> name = "Alice";
// 방법 1: value() — 비어있으면 std::bad_optional_access 예외
std::cout << name.value() << "\n";
// 방법 2: *연산자 — UB if empty
std::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";
}
// C++23: and_then, transform, or_else
std::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);

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번째)
std::variant<int, std::string> v = "world";
// get<T>: 타입 불일치 시 std::bad_variant_access
std::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";
}

std::visit는 variant에 들어있는 모든 타입에 대해 함수를 적용합니다.

std::variant<int, double, std::string> v = 3.14;
std::visit([](const auto& val) {
std::cout << val << "\n";
}, v);

여러 람다를 하나의 방문자(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: hello

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";
}

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";
}
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());

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";
}
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::monostatevariant의 “빈” 상태별도 bool 플래그

두 타입 모두 타입 안전성을 보장하며 널 포인터 역참조나 유니온 타입 오류를 컴파일 타임에 방지합니다.