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는 타입 안전성이 떨어지므로 꼭 필요할 때만 쓰고, 대부분은 optional과 variant로 커버할 수 있습니다.
이 글을 읽으면:
- optional로 값의 존재 여부를 안전하게 표현할 수 있습니다.
- variant로 여러 타입 중 하나를 저장할 수 있습니다.
- any로 임의의 타입을 저장할 수 있습니다.
- 실전에서 타입 안전하게 코드를 작성할 수 있습니다.
목차
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
| 특징 | optional | variant | any |
|---|---|---|---|
| 용도 | 값 있음/없음 | 여러 타입 중 하나 | 임의 타입 |
| 타입 개수 | 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<T>"]
O1[bool: 값 유무] --> O2[T: 실제 값]
end
subgraph variant["variant<A,B,C>"]
V1[union: max size] --> V2[index]
end
subgraph any["any"]
A1[힙 할당] --> A2[type_info]
A2 --> A3[소멸자]
end
벤치마크 요약 (참고용)
| 연산 | optional | variant | any |
|---|---|---|---|
| 생성 | ~1 cycle | ~1 cycle | 힙 할당 (~100+ cycles) |
| 복사 | T 크기에 비례 | max(T…) 크기 | 힙 할당 + 복사 |
| 접근 | 인라인 가능 | switch/인덱스 | 가상 호출 |
| 메모리 | sizeof(T) + 1 | max(sizeof…) + 정렬 | 포인터 + 힙 |
성능 팁
- optional:
std::optional<std::string>보다std::optional<std::string_view>가 작은 문자열에 유리할 수 있음 (복사 비용 감소) - variant:
std::visit는 컴파일 타임에 모든 타입을 처리하므로 인라인·최적화가 잘 됨 - 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 | 임의 타입 | 유연함 | 느림, 타입 불안전 |
핵심 원칙:
- 값 유무는 optional
- 고정 타입 집합은 variant
- 동적 타입은 any (최후의 수단)
- 포인터 대신 optional
- 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 포인터 |