C++ optional | "선택적 값" 가이드
이 글의 핵심
std::optional 은 C++17에서 도입된 타입으로, 값이 있거나 없을 수 있는 상태를 표현합니다. null 포인터나 특수 값(-1, -999 등)을 사용하지 않고도 "값 없음"을 타입 안전하게 표현할 수 있습니다.
들어가며
**std::optional**은 C++17에서 도입된 타입으로, 값이 있거나 없을 수 있는 상태를 표현합니다. null 포인터나 특수 값(-1, -999 등)을 사용하지 않고도 “값 없음”을 타입 안전하게 표현할 수 있습니다.
#include <optional>
#include <vector>
#include <algorithm>
#include <iostream>
// std::optional<int>: int 값이 있거나 없을 수 있음을 명시
// 반환 타입으로 "찾지 못할 수 있음"을 타입 시스템에 표현
std::optional<int> findValue(const std::vector<int>& v, int target) {
// std::find: 벡터에서 target 값 찾기
auto it = std::find(v.begin(), v.end(), target);
if (it != v.end()) {
// 찾았으면 값 반환 (int → optional<int> 자동 변환)
return *it;
}
// std::nullopt: "값 없음"을 표현하는 특수 값
// -1 같은 특수 값보다 명확하고 타입 안전
return std::nullopt; // 값 없음
}
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// optional 반환값 받기
auto result = findValue(v, 3);
// optional은 bool로 변환 가능: 값이 있으면 true
if (result) {
// *result: optional에서 실제 값 추출 (역참조 연산자)
std::cout << "찾음: " << *result << std::endl;
}
auto notFound = findValue(v, 10);
// !notFound: 값이 없으면 true
if (!notFound) {
std::cout << "못 찾음" << std::endl;
}
return 0;
}
왜 필요한가?:
- 타입 안전: null 포인터나 특수 값 대신 명시적으로 “값 없음” 표현
- 명확한 의도: 함수가 값을 반환하지 않을 수 있음을 명확히 표시
- 예외 회피: 값 없음을 예외 대신 반환 값으로 처리
- 안전성: 값 없음 상태를 컴파일러가 강제로 확인하게 함
1. optional vs 전통적 방식
특수 값 vs optional
#include <vector>
#include <algorithm>
#include <iostream>
#include <optional>
// ❌ 전통적 방식: 특수 값 사용 (모호함)
int findIndexOld(const std::vector<int>& v, int target) {
auto it = std::find(v.begin(), v.end(), target);
if (it != v.end()) {
return std::distance(v.begin(), it);
}
return -1; // -1이 "값 없음"인지 실제 인덱스인지 모호
}
// ✅ optional: 명확함
std::optional<size_t> findIndex(const std::vector<int>& v, int target) {
auto it = std::find(v.begin(), v.end(), target);
if (it != v.end()) {
return std::distance(v.begin(), it);
}
return std::nullopt; // 명확하게 "값 없음"
}
int main() {
std::vector<int> v = {10, 20, 30, 40, 50};
// 구식
int idx1 = findIndexOld(v, 30);
if (idx1 != -1) {
std::cout << "인덱스: " << idx1 << std::endl;
}
int idx2 = findIndexOld(v, 99);
if (idx2 == -1) {
std::cout << "못 찾음" << std::endl;
}
// 현대적
auto idx3 = findIndex(v, 30);
if (idx3) {
std::cout << "인덱스: " << *idx3 << std::endl;
}
auto idx4 = findIndex(v, 99);
if (!idx4) {
std::cout << "못 찾음" << std::endl;
}
return 0;
}
출력:
인덱스: 2
못 찾음
인덱스: 2
못 찾음
optional vs 포인터
| 특징 | std::optional | T* (포인터) |
|---|---|---|
| 메모리 | 스택 (값 포함) | 힙 (동적 할당 필요) |
| 소유권 | 소유 | 소유하지 않음 |
| null 표현 | std::nullopt | nullptr |
| 타입 안전 | ✅ 강함 | ❌ 약함 |
| 복사 | 값 복사 | 포인터 복사 |
| 크기 | sizeof(T) + 1 | sizeof(void*) |
// optional: 값 소유
std::optional<int> opt = 42;
// 스택에 저장, 자동 관리
// 포인터: 메모리 관리 필요
int* ptr = new int(42);
// 힙에 저장, 수동 관리
delete ptr;
2. 값 접근
다양한 접근 방법
#include <optional>
#include <iostream>
int main() {
std::optional<int> opt = 42;
// 방법 1: has_value() + value()
// has_value(): 값이 있는지 명시적으로 확인
if (opt.has_value()) {
// value(): 값 추출 (값 없으면 예외)
std::cout << "값: " << opt.value() << std::endl;
}
// 방법 2: bool 변환 + * 연산자 (가장 간결)
// optional은 bool로 암묵적 변환: 값 있으면 true
if (opt) {
// *opt: 역참조로 값 추출 (포인터처럼 사용)
// 값 없을 때 사용하면 UB (Undefined Behavior)
std::cout << "값: " << *opt << std::endl;
}
// 방법 3: value_or() - 기본값 제공
// 값이 있으면 그 값, 없으면 기본값(0) 반환
// 예외 없이 안전하게 값 추출
int val = opt.value_or(0);
std::cout << "값: " << val << std::endl;
// 방법 4: value() - 예외 처리
// 값 없으면 std::bad_optional_access 예외 발생
try {
int val = opt.value();
std::cout << "값: " << val << std::endl;
} catch (const std::bad_optional_access& e) {
std::cout << "예외: " << e.what() << std::endl;
}
return 0;
}
출력:
값: 42
값: 42
값: 42
값: 42
값 없을 때
#include <optional>
#include <iostream>
int main() {
std::optional<int> opt; // 값 없음
// has_value()
std::cout << "has_value: " << opt.has_value() << std::endl; // 0
// bool 변환
if (!opt) {
std::cout << "값 없음" << std::endl;
}
// value_or()
int val = opt.value_or(99);
std::cout << "value_or: " << val << std::endl; // 99
// value() - 예외
try {
int val = opt.value();
} catch (const std::bad_optional_access& e) {
std::cout << "예외: " << e.what() << std::endl;
}
return 0;
}
출력:
has_value: 0
값 없음
value_or: 99
예외: bad optional access
3. 실전 예제
예제 1: 검색 함수
#include <optional>
#include <vector>
#include <iostream>
#include <string>
struct User {
int id;
std::string name;
int age;
};
std::optional<User> findUser(const std::vector<User>& users, int id) {
for (const auto& user : users) {
if (user.id == id) {
return user;
}
}
return std::nullopt;
}
std::optional<User> findUserByName(const std::vector<User>& users, const std::string& name) {
for (const auto& user : users) {
if (user.name == name) {
return user;
}
}
return std::nullopt;
}
int main() {
std::vector<User> users = {
{1, "Alice", 25},
{2, "Bob", 30},
{3, "Charlie", 28}
};
// ID로 검색
auto user1 = findUser(users, 2);
if (user1) {
std::cout << "찾음: " << user1->name << " (" << user1->age << "세)" << std::endl;
}
// 이름으로 검색
auto user2 = findUserByName(users, "Charlie");
if (user2) {
std::cout << "찾음: ID=" << user2->id << ", " << user2->name << std::endl;
}
// 못 찾음
auto user3 = findUser(users, 99);
if (!user3) {
std::cout << "사용자 없음" << std::endl;
}
return 0;
}
출력:
찾음: Bob (30세)
찾음: ID=3, Charlie
사용자 없음
예제 2: 파싱
#include <optional>
#include <string>
#include <sstream>
#include <iostream>
std::optional<int> parseInt(const std::string& str) {
std::istringstream iss{str};
int value;
if (iss >> value) {
char remaining;
if (!(iss >> remaining)) { // 남은 문자 없음
return value;
}
}
return std::nullopt;
}
std::optional<double> parseDouble(const std::string& str) {
try {
size_t pos;
double value = std::stod(str, &pos);
if (pos == str.length()) {
return value;
}
} catch (...) {
}
return std::nullopt;
}
int main() {
auto result1 = parseInt("123");
if (result1) {
std::cout << "파싱 성공: " << *result1 << std::endl;
}
auto result2 = parseInt("abc");
if (!result2) {
std::cout << "파싱 실패" << std::endl;
}
auto result3 = parseDouble("3.14");
if (result3) {
std::cout << "파싱 성공: " << *result3 << std::endl;
}
auto result4 = parseDouble("3.14abc");
if (!result4) {
std::cout << "파싱 실패" << std::endl;
}
return 0;
}
출력:
파싱 성공: 123
파싱 실패
파싱 성공: 3.14
파싱 실패
예제 3: 설정 관리
#include <optional>
#include <iostream>
#include <string>
class Configuration {
std::optional<std::string> databaseUrl_;
std::optional<int> port_;
std::optional<int> timeout_;
std::optional<bool> debug_;
public:
void setDatabaseUrl(const std::string& url) {
databaseUrl_ = url;
}
void setPort(int port) {
port_ = port;
}
void setTimeout(int timeout) {
timeout_ = timeout;
}
void setDebug(bool debug) {
debug_ = debug;
}
std::string getDatabaseUrl() const {
return databaseUrl_.value_or("localhost");
}
int getPort() const {
return port_.value_or(5432);
}
int getTimeout() const {
return timeout_.value_or(30);
}
bool isDebug() const {
return debug_.value_or(false);
}
void print() const {
std::cout << "Database: " << getDatabaseUrl() << std::endl;
std::cout << "Port: " << getPort() << std::endl;
std::cout << "Timeout: " << getTimeout() << "s" << std::endl;
std::cout << "Debug: " << (isDebug() ? "true" : "false") << std::endl;
}
};
int main() {
Configuration config;
std::cout << "=== 기본 설정 ===" << std::endl;
config.print();
std::cout << "\n=== 커스텀 설정 ===" << std::endl;
config.setDatabaseUrl("postgres://db.example.com");
config.setPort(5433);
config.setDebug(true);
config.print();
return 0;
}
출력:
=== 기본 설정 ===
Database: localhost
Port: 5432
Timeout: 30s
Debug: false
=== 커스텀 설정 ===
Database: postgres://db.example.com
Port: 5433
Timeout: 30s
Debug: true
4. 실전 예제: 캐시 시스템
#include <optional>
#include <map>
#include <iostream>
#include <string>
template<typename Key, typename Value>
class Cache {
std::map<Key, Value> storage_;
size_t maxSize_;
public:
Cache(size_t maxSize = 100) : maxSize_(maxSize) {}
std::optional<Value> get(const Key& key) const {
auto it = storage_.find(key);
if (it != storage_.end()) {
return it->second;
}
return std::nullopt;
}
void put(const Key& key, const Value& value) {
if (storage_.size() >= maxSize_) {
// 캐시 가득 참 (간단한 예시: 첫 번째 제거)
storage_.erase(storage_.begin());
}
storage_[key] = value;
}
bool contains(const Key& key) const {
return storage_.find(key) != storage_.end();
}
size_t size() const {
return storage_.size();
}
};
int main() {
Cache<std::string, int> cache(3);
// 캐시에 추가
cache.put("user:1", 100);
cache.put("user:2", 200);
cache.put("user:3", 300);
std::cout << "캐시 크기: " << cache.size() << std::endl;
// 캐시 히트
if (auto value = cache.get("user:2")) {
std::cout << "캐시 히트: user:2 = " << *value << std::endl;
}
// 캐시 미스
if (auto value = cache.get("user:99")) {
std::cout << "캐시 히트: " << *value << std::endl;
} else {
std::cout << "캐시 미스: user:99" << std::endl;
}
// 기본값 사용
int value = cache.get("user:99").value_or(-1);
std::cout << "value_or: " << value << std::endl;
return 0;
}
출력:
캐시 크기: 3
캐시 히트: user:2 = 200
캐시 미스: user:99
value_or: -1
5. 자주 발생하는 문제
문제 1: 예외
#include <optional>
#include <iostream>
int main() {
std::optional<int> opt;
// ❌ 값 없을 때 예외
try {
int val = opt.value(); // std::bad_optional_access
} catch (const std::bad_optional_access& e) {
std::cout << "예외: " << e.what() << std::endl;
}
// ✅ 확인 후 접근
if (opt) {
int val = *opt;
std::cout << "값: " << val << std::endl;
} else {
std::cout << "값 없음" << std::endl;
}
// ✅ 기본값
int val = opt.value_or(0);
std::cout << "value_or: " << val << std::endl;
return 0;
}
출력:
예외: bad optional access
값 없음
value_or: 0
문제 2: 참조
#include <optional>
#include <iostream>
#include <functional>
int main() {
int x = 42;
// ❌ optional<int&>: 불가능
// std::optional<int&> opt{x};
// ✅ reference_wrapper 사용
std::optional<std::reference_wrapper<int>> opt1{std::ref(x)};
opt1->get() = 100;
std::cout << "x: " << x << std::endl; // 100
// ✅ 포인터 사용
std::optional<int*> opt2{&x};
*(*opt2) = 200;
std::cout << "x: " << x << std::endl; // 200
return 0;
}
출력:
x: 100
x: 200
문제 3: 비교
#include <optional>
#include <iostream>
int main() {
std::optional<int> opt1 = 42;
std::optional<int> opt2 = 42;
std::optional<int> opt3;
// 값 비교
std::cout << "opt1 == opt2: " << (opt1 == opt2) << std::endl; // 1
std::cout << "opt1 == opt3: " << (opt1 == opt3) << std::endl; // 0
// nullopt 비교
std::cout << "opt3 == nullopt: " << (opt3 == std::nullopt) << std::endl; // 1
// 값과 직접 비교
std::cout << "opt1 == 42: " << (opt1 == 42) << std::endl; // 1
return 0;
}
출력:
opt1 == opt2: 1
opt1 == opt3: 0
opt3 == nullopt: 1
opt1 == 42: 1
6. 실전 예제: 변환 체인
#include <optional>
#include <iostream>
#include <string>
#include <cmath>
// 문자열을 정수로 파싱 (실패 시 nullopt)
std::optional<int> parseInt(const std::string& str) {
try {
// std::stoi: string to int 변환
return std::stoi(str);
} catch (...) {
// 변환 실패 시 (예: "abc") nullopt 반환
return std::nullopt;
}
}
// 양수 검증 (0 이하면 nullopt)
std::optional<int> validatePositive(int value) {
if (value > 0) {
return value;
}
// 음수나 0이면 "유효하지 않음"을 nullopt로 표현
return std::nullopt;
}
// 제곱 계산 (항상 성공)
std::optional<int> square(int value) {
return value * value;
}
// 제곱근 계산 (음수면 nullopt)
std::optional<double> squareRoot(int value) {
if (value >= 0) {
return std::sqrt(value);
}
// 음수의 제곱근은 실수 범위에서 불가능
return std::nullopt;
}
int main() {
// 체이닝 1: and_then으로 연속 변환
// and_then: optional이 값을 가지면 함수 적용, 없으면 nullopt 전파
// "10" → 10 → 양수 확인 → 100
auto result1 = parseInt("10")
.and_then(validatePositive) // 10 > 0 → 10
.and_then(square); // 10 * 10 → 100
if (result1) {
std::cout << "결과: " << *result1 << std::endl; // 100
}
// 체이닝 2 (실패): 중간에 nullopt 발생
// "-5" → -5 → 양수 아님 → nullopt (이후 함수 실행 안됨)
auto result2 = parseInt("-5")
.and_then(validatePositive) // -5 ≤ 0 → nullopt
.and_then(square); // 실행 안됨 (nullopt 전파)
if (!result2) {
std::cout << "유효하지 않은 입력" << std::endl;
}
// 체이닝 3: 다른 타입으로 변환
// "16" → 16 → √16 = 4.0
auto result3 = parseInt("16")
.and_then(squareRoot); // int → optional<double>
if (result3) {
std::cout << "제곱근: " << *result3 << std::endl; // 4
}
return 0;
}
출력:
결과: 100
유효하지 않은 입력
제곱근: 4
정리
핵심 요약
- optional: 값이 있거나 없을 수 있는 상태
- nullopt: 값 없음 표현
- 타입 안전: 특수 값 대신 명시적 표현
- 접근 방법:
*opt,value(),value_or() - 실무: 검색, 파싱, 설정, 캐시
값 접근 방법
| 방법 | 값 없을 때 | 용도 |
|---|---|---|
*opt | UB | 빠름, 확인 후 사용 |
opt.value() | 예외 | 안전, 예외 처리 |
opt.value_or(default) | 기본값 | 안전, 기본값 |
opt.has_value() | false | 확인 |
실전 팁
사용 원칙:
- 검색 결과
- 파싱 결과
- 설정 값
- 체이닝
성능:
- 스택 할당
- 크기:
sizeof(T) + 1(정렬 포함) - 동적 할당 없음
- 캐시 친화적
주의사항:
- 값 없을 때 접근 주의
- 참조 저장 불가
value()예외 처리value_or()기본값
다음 단계
- C++ variant
- C++ any
- C++ expected
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Optional 완벽 가이드 | nullopt·value_or·C++23 모나딕 연산·성능·실전 패턴
- C++ variant | “타입 안전 union” 가이드
- C++ any | “타입 소거” 가이드
관련 글
- C++ Optional 완벽 가이드 | nullopt·value_or·C++23 모나딕 연산·성능·실전 패턴
- C++ std::optional vs 포인터 |
- C++ any |
- C++ Buffer Overflow |
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기