C++ optional·variant·any | "nullptr 체크 지겹다" C++17 타입 안전 처리

C++ optional·variant·any | "nullptr 체크 지겹다" C++17 타입 안전 처리

이 글의 핵심

C++ optional·variant·any에 대해 정리한 개발 블로그 글입니다. 사용자 정보를 조회하는 함수를 만들었습니다. 하지만 사용자가 없을 때를 표현하기 어려웠습니다. std::optional은 "값이 있거나 없거나"를 타입으로 표현해서 nullptr(널 포인터—아무 객체도 가리키지 않음을… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 …

들어가며: nullptr 체크에 지쳤다

“값이 없을 수도 있는데 어떻게 표현하죠?”

사용자 정보를 조회하는 함수를 만들었습니다. 하지만 사용자가 없을 때를 표현하기 어려웠습니다.
std::optional은 “값이 있거나 없거나”를 타입으로 표현해서 nullptr(널 포인터—아무 객체도 가리키지 않음을 나타내는 값)·예외·bool+참조 패턴보다 명확하고, 호출하는 쪽에서 반드시 유무를 확인하게 만들 수 있습니다. std::variant(정해진 타입 목록 중 하나를 담는 타입)·std::any(어떤 타입이든 담을 수 있는 타입)는 “여러 타입 중 하나”나 “타입을 나중에 정하는 값”을 타입 안전하게 다룰 때 쓰면, void*나 공용체보다 안전합니다.

문제의 코드:

// ❌ 방법 1: 포인터 (메모리 관리 복잡)
User* findUser(int id) {
    if (존재하지않음) return nullptr;
    return new User{...};  // 누가 delete?
}

// ❌ 방법 2: 예외 (성능 문제)
User findUser(int id) {
    if (존재하지않음) throw std::runtime_error("Not found");
    return User{...};
}

// ❌ 방법 3: bool + 참조 (복잡)
bool findUser(int id, User& out) {
    if (존재하지않음) return false;
    out = User{...};
    return true;
}

optional로 해결:

std::optional<User> findUser(int id) {
    if (존재하지않음) {
        return std::nullopt;  // 값 없음
    }
    return User{...};  // 값 있음
}

int main() {
    auto user = findUser(123);
    
    if (user) {
        std::cout << "Found: " << user->name << "\n";
    } else {
        std::cout << "Not found\n";
    }
}

선택 기준: “값이 없을 수 있다”만 표현하면 optional, “여러 타입 중 하나”면 variant, “타입을 아예 나중에 정한다”면 any입니다. any는 타입 안전성이 떨어지므로 꼭 필요할 때만 쓰고, 대부분은 optionalvariant로 커버할 수 있습니다.

이 글을 읽으면:

  • optional로 값의 존재 여부를 안전하게 표현할 수 있습니다.
  • variant로 여러 타입 중 하나를 저장할 수 있습니다.
  • any로 임의의 타입을 저장할 수 있습니다.
  • 실전에서 타입 안전하게 코드를 작성할 수 있습니다.

목차

  1. 실무에서 겪는 문제 시나리오
  2. std::optional
  3. std::variant
  4. std::any
  5. 선택 가이드
  6. 자주 발생하는 문제
  7. 성능 비교
  8. 실전 패턴
  9. 프로덕션 패턴

1. 실무에서 겪는 문제 시나리오

시나리오 1: nullptr 체크 누락으로 크래시

// ❌ 문제: nullptr 체크를 깜빡하면 크래시
User* user = findUser(123);
std::cout << user->name << "\n";  // user가 nullptr면 Segmentation fault!

// ✅ 해결: optional은 타입으로 "없을 수 있음"을 강제
std::optional<User> user = findUser(123);
// std::cout << user->name;  // 컴파일 에러! optional은 -> 연산자로 바로 접근 불가
if (user) {
    std::cout << user->name << "\n";  // 안전
}

시나리오 2: JSON/설정 파싱 시 타입 혼동

// ❌ 문제: "port"가 정수인지 문자열인지 런타임에만 알 수 있음
void* getConfig(const std::string& key);  // 반환 타입이 뭔지 모름

// ✅ 해결: variant로 허용 타입을 명시
using ConfigValue = std::variant<int, double, std::string, bool>;
std::optional<ConfigValue> getConfig(const std::string& key);

시나리오 3: 플러그인/스크립트에서 동적 타입 전달

// ❌ 문제: C++ API에 Python/JS에서 온 값을 넘겨야 함
void setProperty(const std::string& name, ??? value);  // 타입을 미리 알 수 없음

// ✅ 해결: any (최후의 수단, 타입 체크는 런타임에)
void setProperty(const std::string& name, std::any value);

타입 선택 흐름도

flowchart TD
    A[값이 없을 수 있나?] -->|예| B[optional]
    A -->|아니오| C[여러 타입 중 하나?]
    C -->|예, 타입 고정| D[variant]
    C -->|예, 타입 미정| E[any]
    C -->|아니오| F[일반 타입]

2. std::optional

기본 사용법

std::optional<T>는 “T 타입 값이 있거나, 없거나”를 하나의 타입으로 표현합니다. divide처럼 0으로 나누는 경우처럼 유효한 값을 반환할 수 없는 상황에서 std::nullopt를 반환하고, 정상일 때만 return a / b로 값을 담아 반환합니다. 호출부에서는 if (result)로 값 존재 여부를 확인한 뒤 *result 또는 result.value()로 접근하면, 포인터나 예외 없이 “값 없음”을 다룰 수 있습니다.

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o optional_divide optional_divide.cpp && ./optional_divide
#include <optional>
#include <iostream>

std::optional<int> divide(int a, int b) {
    if (b == 0) {
        return std::nullopt;  // 값 없음
    }
    return a / b;  // 값 있음
}

int main() {
    auto result = divide(10, 2);
    if (result) {
        std::cout << "Result: " << *result << "\n";  // 5
    }
    auto fail = divide(10, 0);
    if (!fail) {
        std::cout << "Division by zero\n";
    }
    return 0;
}

실행 결과: Result: 5 한 줄, 이어서 Division by zero 한 줄이 출력됩니다.

값 확인과 접근

값이 있는지 확인하는 방법은 has_value(), if (opt)(bool 변환), value_or(기본값) 이 있습니다. value()는 값이 없으면 std::bad_optional_access 예외를 던지므로, 먼저 has_value()if (opt)로 확인한 뒤 사용하는 것이 안전합니다. 기본값으로 대체하고 싶을 때는 value_or("Unknown")처럼 쓰면, 값이 없으면 “Unknown”이 반환됩니다.

#include <optional>
#include <iostream>
#include <string>

std::optional<std::string> getName() {
    return "Alice";
}

int main() {
    auto name = getName();
    
    // 방법 1: has_value()
    if (name.has_value()) {
        std::cout << name.value() << "\n";
    }
    
    // 방법 2: operator bool
    if (name) {
        std::cout << *name << "\n";
    }
    
    // 방법 3: value_or (기본값)
    std::string result = name.value_or("Unknown");
}

언제 무엇을 쓸까: 값이 있을 때만 쓰려면 if (opt) 또는 has_value()로 확인한 뒤 value() 또는 *opt로 접근합니다. 값이 없을 수 있는데 기본값으로 대체하고 싶으면 value_or(기본값)이 가장 안전합니다. value()는 값이 없으면 std::bad_optional_access 예외를 던지고, *opt는 값이 없을 때 정의되지 않은 동작(UB)이므로 확실히 값이 있을 때만 사용하세요.

예외 처리

#include <optional>
#include <iostream>

int main() {
    std::optional<int> opt;

    // ❌ 값이 없으면 예외
    try {
        int value = opt.value();  // std::bad_optional_access
    } catch (const std::bad_optional_access& e) {
        std::cerr << "No value\n";
    }

    // ✅ 권장: value_or
    int value = opt.value_or(0);
    return 0;
}

optional 생성

#include <optional>
#include <string>

// 빈 optional
std::optional<int> opt1;
std::optional<int> opt2 = std::nullopt;

// 값 있는 optional
std::optional<int> opt3 = 42;
std::optional<int> opt4{42};
std::optional<int> opt5 = std::make_optional(42);

// in-place 생성
std::optional<std::string> opt6(std::in_place, 10, 'x');  // "xxxxxxxxxx"

optional 수정

#include <optional>

int main() {
    std::optional<int> opt;

    // 값 할당
    opt = 42;

    // 값 제거
    opt.reset();
    opt = std::nullopt;

    // emplace
    opt.emplace(100);
    return 0;
}

완전한 optional 예제: 사용자 조회 시스템

// g++ -std=c++17 -o optional_user optional_user.cpp && ./optional_user
#include <optional>
#include <iostream>
#include <string>
#include <map>

struct User {
    int id;
    std::string name;
    int age;
};

std::map<int, User> db = {
    {1, {1, "Alice", 25}},
    {2, {2, "Bob", 30}},
};

std::optional<User> findUser(int id) {
    auto it = db.find(id);
    if (it == db.end()) {
        return std::nullopt;
    }
    return it->second;
}

int main() {
    // 케이스 1: 사용자 있음
    if (auto user = findUser(1)) {
        std::cout << "Found: " << user->name << ", " << user->age << "\n";
    }

    // 케이스 2: 사용자 없음
    auto missing = findUser(999);
    std::cout << "User 999: " << (missing ? "Found" : "Not found") << "\n";

    // 케이스 3: 기본값 사용
    auto user = findUser(2).value_or(User{0, "Guest", 0});
    std::cout << "User 2 or Guest: " << user.name << "\n";
    return 0;
}

3. std::variant

기본 사용법

#include <variant>
#include <string>

std::variant<int, double, std::string> value;

value = 42;                    // int
value = 3.14;                  // double
value = std::string("hello");  // string

값 접근

#include <variant>
#include <iostream>
#include <string>

std::variant<int, double, std::string> v = 42;

// 방법 1: std::get<T>
try {
    int i = std::get<int>(v);  // OK
    // double d = std::get<double>(v);  // 예외!
} catch (const std::bad_variant_access& e) {
    std::cerr << "Wrong type\n";
}

// 방법 2: std::get<index>
int i = std::get<0>(v);  // 첫 번째 타입

// 방법 3: std::get_if
if (auto ptr = std::get_if<int>(&v)) {
    std::cout << "int: " << *ptr << "\n";
}

타입 확인

#include <variant>
#include <iostream>
#include <string>

std::variant<int, double, std::string> v = 3.14;

// index()
std::cout << v.index() << "\n";  // 1 (double은 두 번째)

// holds_alternative
if (std::holds_alternative<double>(v)) {
    std::cout << "It's a double\n";
}

std::visit

#include <variant>
#include <iostream>
#include <string>
#include <type_traits>

std::variant<int, double, std::string> v = 42;

// 방문자 패턴
std::visit( {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "int: " << arg << "\n";
    } else if constexpr (std::is_same_v<T, double>) {
        std::cout << "double: " << arg << "\n";
    } else if constexpr (std::is_same_v<T, std::string>) {
        std::cout << "string: " << arg << "\n";
    }
}, v);

visit의 의미: “지금 variant가 들고 있는 타입이 뭐든, 그에 맞는 동작을 한 번에 적용”하고 싶을 때 std::visit를 씁니다. get<T>get_if<T>는 타입을 미리 알고 있을 때 쓰고, visit는 모든 타입을 동일한 방식으로 처리할 때(로깅, 직렬화, 포맷 출력 등) 유리합니다.

오버로드 패턴

#include <variant>
#include <iostream>
#include <string>

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

int main() {
    std::variant<int, double, std::string> v = "hello";
    
    std::visit(overloaded{
         { std::cout << "int: " << i << "\n"; },
         { std::cout << "double: " << d << "\n"; },
         { std::cout << "string: " << s << "\n"; }
    }, v);
}

이 패턴이 하는 일: overloaded는 여러 람다(함수 객체)를 하나로 묶어서, visit가 variant에 들어 있는 값의 타입에 따라 해당하는 람다만 호출하게 합니다. 타입별로 get_if로 분기하는 대신, “int면 이렇게, double이면 이렇게, string이면 이렇게”를 한 번에 나열해 두는 방식이라 가독성이 좋습니다.

완전한 variant 예제: Result 타입 (에러 처리)

// g++ -std=c++17 -o variant_result variant_result.cpp && ./variant_result
#include <variant>
#include <iostream>
#include <string>

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

struct Error {
    std::string message;
    int code;
};

template <typename T>
using Result = std::variant<T, Error>;

Result<int> divide(int a, int b) {
    if (b == 0) {
        return Error{"Division by zero", -1};
    }
    return a / b;
}

int main() {
    auto r1 = divide(10, 2);
    std::visit(overloaded{
         { std::cout << "Result: " << v << "\n"; },
         { std::cerr << "Error: " << e.message << "\n"; }
    }, r1);

    auto r2 = divide(10, 0);
    std::visit(overloaded{
         { std::cout << "Result: " << v << "\n"; },
         { std::cerr << "Error: " << e.message << " (code=" << e.code << ")\n"; }
    }, r2);
    return 0;
}

4. std::any

기본 사용법

#include <any>
#include <string>
#include <vector>

std::any value;

value = 42;                    // int
value = 3.14;                  // double
value = std::string("hello");  // string
value = std::vector<int>{1, 2, 3};  // vector

값 접근

#include <any>
#include <iostream>

std::any a = 42;

// any_cast
try {
    int i = std::any_cast<int>(a);  // OK
    // double d = std::any_cast<double>(a);  // 예외!
} catch (const std::bad_any_cast& e) {
    std::cerr << "Wrong type\n";
}

// 포인터 버전 (예외 안 던짐)
if (auto ptr = std::any_cast<int>(&a)) {
    std::cout << "int: " << *ptr << "\n";
} else {
    std::cout << "Not an int\n";
}

타입 확인

#include <any>
#include <iostream>
#include <typeinfo>

std::any a = 42;

// has_value()
if (a.has_value()) {
    std::cout << "Has value\n";
}

// type()
if (a.type() == typeid(int)) {
    std::cout << "It's an int\n";
}

any 수정

#include <any>
#include <string>

int main() {
    std::any a;

    // 값 할당
    a = 42;

    // 값 제거
    a.reset();

    // emplace
    a.emplace<std::string>("hello");
    return 0;
}

완전한 any 예제: 속성 저장소

// g++ -std=c++17 -o any_properties any_properties.cpp && ./any_properties
#include <any>
#include <optional>
#include <iostream>
#include <string>
#include <map>

class PropertyStore {
    std::map<std::string, std::any> data;

public:
    template <typename T>
    void set(const std::string& key, const T& value) {
        data[key] = value;
    }

    template <typename T>
    std::optional<T> get(const std::string& key) const {
        auto it = data.find(key);
        if (it == data.end()) {
            return std::nullopt;
        }
        try {
            return std::any_cast<T>(it->second);
        } catch (const std::bad_any_cast&) {
            return std::nullopt;
        }
    }
};

int main() {
    PropertyStore store;
    store.set("port", 8080);
    store.set("host", std::string("localhost"));
    store.set("ratio", 0.95);

    if (auto port = store.get<int>("port")) {
        std::cout << "Port: " << *port << "\n";
    }
    if (auto host = store.get<std::string>("host")) {
        std::cout << "Host: " << *host << "\n";
    }
    if (auto ratio = store.get<double>("ratio")) {
        std::cout << "Ratio: " << *ratio << "\n";
    }
    if (!store.get<int>("missing")) {
        std::cout << "Missing key returns nullopt\n";
    }
    return 0;
}

5. 선택 가이드

optional vs variant vs any

특징optionalvariantany
용도값 있음/없음여러 타입 중 하나임의 타입
타입 개수1개고정 (컴파일 타임)무제한 (런타임)
타입 안전성✅ 높음✅ 높음⚠️ 낮음
성능✅ 빠름✅ 빠름⚠️ 느림 (힙 할당)
메모리스택스택

사용 시나리오

optional 사용:

// 값이 없을 수 있음
std::optional<User> findUser(int id);
std::optional<int> parseInt(const std::string& str);
std::optional<std::string> getEnv(const std::string& key);

variant 사용:

// 여러 타입 중 하나 (고정)
std::variant<int, double, std::string> ConfigValue;
std::variant<Success, Error> Result;
std::variant<Circle, Rectangle, Triangle> Shape;

any 사용:

// 타입을 미리 알 수 없음
std::map<std::string, std::any> properties;
std::vector<std::any> heterogeneousContainer;

6. 자주 발생하는 문제

문제 1: optional에 값 없는데 value()나 *opt 호출

증상: std::bad_optional_access 예외 또는 정의되지 않은 동작(UB)

// ❌ 잘못된 사용
std::optional<int> opt;
int x = opt.value();  // 예외!
int y = *opt;         // UB!

// ✅ 해결
if (opt) {
    int x = opt.value();
}
int y = opt.value_or(0);

문제 2: variant에 get으로 잘못된 타입 요청

증상: std::bad_variant_access 예외

// ❌ 잘못된 사용
std::variant<int, double> v = 42;
double d = std::get<double>(v);  // 예외!
// ✅ 해결: get_if 또는 holds_alternative 먼저 확인
if (auto ptr = std::get_if<double>(&v)) {
    double d = *ptr;
}

문제 3: any_cast로 잘못된 타입 요청

증상: std::bad_any_cast 예외

// ❌ 잘못된 사용
std::any a = 42;
std::string s = std::any_cast<std::string>(a);  // 예외!
// ✅ 해결: 포인터 버전 사용 또는 예외 처리
if (auto ptr = std::any_cast<int>(&a)) {
    int i = *ptr;
} else {
    // 다른 타입 처리
}

문제 4: optional 중첩

증상: std::optional<std::optional<T>>로 불필요한 중첩

// ❌ 혼란스러운 반환
std::optional<std::optional<int>> weird() {
    return std::optional<int>(42);  // optional을 optional로 감쌈
}

// ✅ 해결: 단일 optional
std::optional<int> clean() {
    return 42;
}

문제 5: variant에 void 타입 포함

증상: 컴파일 에러 — variant는 void를 담을 수 없음

// ❌ 잘못된 사용
std::variant<int, void> v;  // 컴파일 에러!

// ✅ 해결: std::monostate로 "빈" 상태 표현
std::variant<std::monostate, int> v;
v = std::monostate{};  // "값 없음" 의미

문제 6: any에 참조 저장

증상: std::any는 참조를 decay해서 값으로 저장함

// ❌ 의도와 다름
int x = 42;
std::any a = x;  // int 복사본 저장, x와 연결 없음
std::any_cast<int>(a) = 100;  // a 내부만 변경, x는 여전히 42

// ✅ 참조가 필요하면 std::reference_wrapper
#include <functional>
std::any a = std::ref(x);
std::any_cast<std::reference_wrapper<int>>(a).get() = 100;  // x도 100

7. 성능 비교

메모리 레이아웃

flowchart TB
    subgraph optional["optional&lt;T&gt;"]
        O1[bool: 값 유무] --> O2[T: 실제 값]
    end
    subgraph variant["variant&lt;A,B,C&gt;"]
        V1[union: max size] --> V2[index]
    end
    subgraph any["any"]
        A1[힙 할당] --> A2[type_info]
        A2 --> A3[소멸자]
    end

벤치마크 요약 (참고용)

연산optionalvariantany
생성~1 cycle~1 cycle힙 할당 (~100+ cycles)
복사T 크기에 비례max(T…) 크기힙 할당 + 복사
접근인라인 가능switch/인덱스가상 호출
메모리sizeof(T) + 1max(sizeof…) + 정렬포인터 + 힙

성능 팁

  1. optional: std::optional<std::string>보다 std::optional<std::string_view>가 작은 문자열에 유리할 수 있음 (복사 비용 감소)
  2. variant: std::visit는 컴파일 타임에 모든 타입을 처리하므로 인라인·최적화가 잘 됨
  3. any: 핫 루프에서는 피하고, 설정 로드·플러그인 인터페이스 등에서만 사용
// ❌ any를 반복문에서 매번 사용
for (int i = 0; i < 1000000; ++i) {
    std::any val = getValue(i);  // 매번 힙 할당 가능
    process(val);
}

// ✅ variant로 타입 고정
using Value = std::variant<int, double, std::string>;
for (int i = 0; i < 1000000; ++i) {
    Value val = getValueVariant(i);  // 스택 할당
    process(val);
}

8. 실전 패턴

패턴 1: 안전한 파싱

#include <optional>
#include <iostream>
#include <string>
#include <stdexcept>

std::optional<int> parseInt(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    auto num = parseInt("123");
    if (num) {
        std::cout << "Parsed: " << *num << "\n";
    }
    
    auto fail = parseInt("abc");
    int value = fail.value_or(0);  // 기본값
}

패턴 2: 설정 값

#include <variant>
#include <optional>
#include <map>
#include <iostream>
#include <string>

class Config {
    std::map<std::string, std::variant<int, double, std::string>> data;
    
public:
    template <typename T>
    void set(const std::string& key, const T& value) {
        data[key] = value;
    }
    
    template <typename T>
    std::optional<T> get(const std::string& key) const {
        auto it = data.find(key);
        if (it == data.end()) {
            return std::nullopt;
        }
        
        if (auto ptr = std::get_if<T>(&it->second)) {
            return *ptr;
        }
        return std::nullopt;
    }
};

int main() {
    Config config;
    config.set("port", 8080);
    config.set("host", std::string("localhost"));
    
    if (auto port = config.get<int>("port")) {
        std::cout << "Port: " << *port << "\n";
    }
}

패턴 3: 에러 처리

#include <variant>
#include <iostream>
#include <string>

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{"Division by zero"};
    }
    return a / b;
}

int main() {
    auto result = divide(10, 2);

    std::visit(overloaded{
         { std::cout << "Result: " << value << "\n"; },
         { std::cerr << "Error: " << err.message << "\n"; }
    }, result);
}

패턴 4: 체이닝

#include <optional>
#include <iostream>
#include <string>

std::optional<std::string> getName(int id) {
    if (id == 1) return "Alice";
    return std::nullopt;
}

std::optional<int> getAge(const std::string& name) {
    if (name == "Alice") return 25;
    return std::nullopt;
}

int main() {
    auto name = getName(1);
    if (name) {
        auto age = getAge(*name);
        if (age) {
            std::cout << *name << " is " << *age << "\n";
        }
    }
    
    // 또는 and_then (C++23)
    // auto age = getName(1).and_then(getAge);
}

패턴 5: 캐시

#include <any>
#include <optional>
#include <map>
#include <string>

class Cache {
    std::map<std::string, std::any> data;
    
public:
    template <typename T>
    void put(const std::string& key, const T& value) {
        data[key] = value;
    }
    
    template <typename T>
    std::optional<T> get(const std::string& key) const {
        auto it = data.find(key);
        if (it == data.end()) {
            return std::nullopt;
        }
        
        try {
            return std::any_cast<T>(it->second);
        } catch (...) {
            return std::nullopt;
        }
    }
};

9. 프로덕션 패턴

패턴 1: API 응답 래퍼

#include <optional>
#include <string>

// REST API 응답: 성공 시 데이터, 실패 시 에러 메시지
template <typename T>
struct ApiResponse {
    std::optional<T> data;
    std::optional<std::string> error;

    static ApiResponse success(T value) {
        return {std::move(value), std::nullopt};
    }
    static ApiResponse failure(std::string msg) {
        return {std::nullopt, std::move(msg)};
    }
    bool ok() const { return data.has_value(); }
};

패턴 2: 설정 검증 (variant + visit)

// 설정값 검증: 타입별로 범위 체크
#include <variant>
#include <string>

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

using ConfigValue = std::variant<int, double, std::string>;

bool validate(const ConfigValue& v) {
    return std::visit(overloaded{
         { return i >= 0 && i <= 65535; },
         { return d >= 0.0 && d <= 1.0; },
         { return !s.empty() && s.size() <= 256; }
    }, v);
}

패턴 3: 플러그인 콜백 (any 최소화)

#include <variant>
#include <string>
#include <vector>

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

// 플러그인: variant로 타입 제한해 any보다 안전하게
using PluginParam = std::variant<int, double, std::string, std::vector<int>>;

void invokePlugin(const std::string& name, const PluginParam& param) {
    std::visit(overloaded{
        [&](int v) { /* ... */ },
        [&](double v) { /* ... */ },
        [&](const std::string& v) { /* ... */ },
        [&](const std::vector<int>& v) { /* ... */ }
    }, param);
}

패턴 4: 데이터베이스 NULL 처리

#include <optional>
#include <string>

// DB 컬럼: NULL 가능
struct UserRow {
    int id;
    std::optional<std::string> name;   // NULL 허용
    std::optional<int> age;            // NULL 허용
};

프로덕션 체크리스트

  • optional: value() 호출 전 has_value() 또는 if (opt) 확인
  • variant: get<T> 대신 get_if<T> 또는 std::visit 사용
  • any: 핫 루프에서는 피하고, 타입이 정해지면 variant로 전환 검토
  • 예외: bad_optional_access, bad_variant_access, bad_any_cast 처리
  • 로깅: 값 없음/타입 불일치 시 명확한 에러 메시지

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
  • C++ 범위 기반 for문과 구조화된 바인딩 | 모던 C++ 반복문
  • C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression

이 글에서 다루는 키워드 (관련 검색어)

C++ optional variant any, std::optional, std::variant, std::visit, has_value value_or, 타입 안전 등으로 검색하시면 이 글이 도움이 됩니다.

정리

타입용도장점단점
optional값 유무타입 안전, 빠름1개 타입만
variant다중 타입타입 안전, 빠름타입 고정
any임의 타입유연함느림, 타입 불안전

핵심 원칙:

  1. 값 유무는 optional
  2. 고정 타입 집합은 variant
  3. 동적 타입은 any (최후의 수단)
  4. 포인터 대신 optional
  5. union 대신 variant

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++17 optional·variant·any 완벽 가이드. std::optional로 null 안전하게 처리, std::variant로 타입 안전한 union, std::any로 동적 타입 저장, has_valu… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

한 줄 요약: optional·variant·any로 “값 없음”·“여러 타입 중 하나”를 타입 안전하게 다룰 수 있습니다. 다음으로 람다 표현식(#13-1)를 읽어보면 좋습니다.

이전 글: C++ 실전 가이드 #12-2: 범위 기반 for와 구조화된 바인딩

다음 글: C++ 실전 가이드 #13-1: 람다 표현식


관련 글

  • C++ 범위 기반 for문과 구조화된 바인딩 | 모던 C++ 반복문
  • C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
  • C++ 참조(Reference) 완벽 가이드 | lvalue·rvalue
  • C++ std::optional·std::variant 완벽 가이드 | nullptr 대신 타입 안전하게
  • C++ std::optional vs 포인터 |