[2026] C++ Optional 완벽 가이드 | nullopt·value_or·C++23 모나딕 연산·성능·실전 패턴

[2026] C++ Optional 완벽 가이드 | nullopt·value_or·C++23 모나딕 연산·성능·실전 패턴

이 글의 핵심

널 포인터 대신 뭘 쓰죠, 값이 없을 수도 있는데 어떻게 표현하죠 같은 문제 해결. std::optional 기초부터 C++23 모나딕 연산(and_then, or_else, transform), 성능 고려사항, 실전 에러 핸들링 패턴까지.

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

실무에서 겪는 문제들

C++ 개발 중 이런 상황을 자주 겪습니다:

  • 검색 실패 — 사용자 ID로 검색했는데 없음. nullptr 반환? 예외? 특수값 -1?
  • 설정 값 누락 — 설정 파일에 특정 키가 없음. 기본값을 어떻게 처리?
  • 파싱 실패 — 문자열을 숫자로 변환 실패. 예외는 과하고 에러 코드는 불편함
  • 캐시 미스 — 캐시에 데이터가 없음. 매번 nullptr 체크는 번거로움
  • 부분 초기화 — 객체의 일부 필드가 선택적. 포인터는 메모리 관리 부담 기존 방법의 문제점: | 방법 | 문제점 | |------|--------| | nullptr | 메모리 관리 부담, 역참조 시 크래시 위험 | | 특수값 (-1, INT_MIN) | 유효한 값과 구분 어려움, 타입 안전하지 않음 | | 예외 | 예상된 실패에는 과함, 성능 오버헤드 | | std::pair<bool, T> | 장황함, 실수하기 쉬움 | std::optional로 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 기존 방법
int* findUser(int id) {
    // ...
    return nullptr;  // 메모리 관리 필요
}
// ✅ optional 사용
std::optional<int> findUser(int id) {
    // ...
    return std::nullopt;  // 안전하고 명확
}

목표:

  • std::optional 기초 (생성, 접근, 체크)
  • C++23 모나딕 연산 (and_then, or_else, transform)
  • 실전 패턴 (에러 핸들링, API 설계, 체이닝)
  • 성능 고려사항 (언제 사용하지 말아야 하는가)
  • 다른 타입과 비교 (variant, expected, 예외)
  • 자주 하는 실수와 해결법
  • 프로덕션 패턴 요구 환경: C++17 이상 (C++23 기능은 별도 표시)

목차

  1. 문제 시나리오: 값이 없는 상황 처리
  2. std::optional 기초
  3. C++23 모나딕 연산
  4. 실전 에러 핸들링 패턴
  5. 성능 고려사항
  6. 다른 타입과 비교
  7. 완전한 예제 모음
  8. 자주 발생하는 실수와 해결법
  9. 모범 사례·베스트 프랙티스
  10. 프로덕션 패턴
  11. 정리 및 체크리스트

1. 문제 시나리오: 값이 없는 상황 처리

시나리오 1: 데이터베이스 조회 실패

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 포인터 사용 (메모리 관리 부담)
User* findUserById(int id) {
    // DB 조회
    if (/* 찾음 */) {
        return new User{id, "Alice"};  // 💥 누가 delete?
    }
    return nullptr;
}
// 사용
User* user = findUserById(123);
if (user != nullptr) {
    std::cout << user->name << std::endl;
    delete user;  // 💥 깜빡하면 메모리 누수
}

주의사항: DB 조회 결과를 new로 넘기는 패턴은 예외·조기 반환 시 누수가 나기 쉽습니다. 소유권이 필요하면 unique_ptr, “없을 수 있음”이면 optional을 우선 검토하세요. 다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ optional 사용 (안전하고 명확)
std::optional<User> findUserById(int id) {
    // DB 조회 (실제로는 DB 쿼리 실행)
    if (/* 사용자를 찾았다면 */) {
        // User 객체를 optional로 감싸서 반환
        return User{id, "Alice"};
    }
    // 찾지 못했으면 nullopt 반환 (값이 없음을 명시)
    return std::nullopt;
}
// 사용 예시
if (auto user = findUserById(123)) {
    // user가 값을 가지고 있으면 (찾았으면) 이 블록 실행
    std::cout << user->name << std::endl;  // ✅ 자동 정리, 메모리 관리 불필요
}
// if 블록을 벗어나면 user는 자동으로 소멸됨

시나리오 2: 설정 파일 파싱

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 특수값 사용 (유효한 값과 구분 어려움)
int getTimeout(const Config& config) {
    if (config.has("timeout")) {
        return config.getInt("timeout");
    }
    return -1;  // 💥 -1이 유효한 값일 수도 있음
}
// 사용
int timeout = getTimeout(config);
if (timeout == -1) {  // 💥 -1이 실제 값인지 에러인지 모호
    timeout = 30;  // 기본값
}

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ optional 사용 (명확한 의미)
std::optional<int> getTimeout(const Config& config) {
    if (config.has("timeout")) {
        return config.getInt("timeout");
    }
    return std::nullopt;
}
// 사용
int timeout = getTimeout(config).value_or(30);  // ✅ 간결하고 명확

시나리오 3: 문자열 파싱

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 예외 사용 (예상된 실패에는 과함)
int parseInt(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        throw std::runtime_error("Parse failed");  // 💥 예외는 비용이 큼
    }
}

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ optional 사용 (예상된 실패)
std::optional<int> parseInt(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::nullopt;  // ✅ 예상된 실패는 optional로
    }
}
// 사용
if (auto num = parseInt("123")) {
    std::cout << "Parsed: " << *num << std::endl;
} else {
    std::cout << "Parse failed" << std::endl;
}

주의사항: 빈 문자열·오버플로 등은 여전히 stoi의 예외 정책에 따릅니다. 초고속 경로에서는 from_chars 등으로 바꾸는 것도 검토하세요.

시나리오 4: 캐시 조회

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ pair<bool, T> 사용 (장황함)
std::pair<bool, std::string> getCached(const std::string& key) {
    if (cache.contains(key)) {
        return {true, cache[key]};
    }
    return {false, ""};  // 💥 빈 문자열이 유효한 값일 수도
}
// 사용
auto [found, value] = getCached("user:123");
if (found) {  // 💥 실수하기 쉬움
    std::cout << value << std::endl;
}

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ optional 사용 (간결하고 안전)
// 실행 예제
std::optional<std::string> getCached(const std::string& key) {
    if (cache.contains(key)) {
        return cache[key];
    }
    return std::nullopt;
}
// 사용
if (auto value = getCached("user:123")) {
    std::cout << *value << std::endl;  // ✅ 명확
}

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph Problems[값이 없는 상황]
        P1[검색 실패]
        P2[설정 누락]
        P3[파싱 실패]
        P4[캐시 미스]
    end
    subgraph OldSolutions["기존 해결법 (문제 있음)"]
        O1[nullptr - 메모리 관리]
        O2[특수값 - 모호함]
        O3[예외 - 과함]
        O4[pair - 장황함]
    end
    subgraph NewSolution[std optional]
        N1[안전]
        N2[명확]
        N3[간결]
    end
    P1 --> O1
    P2 --> O2
    P3 --> O3
    P4 --> O4
    O1 --> N1
    O2 --> N2
    O3 --> N3
    O4 --> N1

2. std::optional 기초

생성과 초기화

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
#include <string>
// 1. 기본 생성 (값 없음)
std::optional<int> opt1;
std::optional<int> opt2 = std::nullopt;
// 2. 값으로 초기화
std::optional<int> opt3 = 42;
std::optional<int> opt4{42};
// 3. make_optional
auto opt5 = std::make_optional<int>(42);
auto opt6 = std::make_optional<std::string>("Hello");
// 4. in_place 생성 (복잡한 타입)
struct Point {
    int x, y;
    Point(int x, int y) : x(x), y(y) {}
};
std::optional<Point> opt7{std::in_place, 10, 20};  // Point(10, 20) 직접 생성
// 5. emplace (나중에 값 할당)
std::optional<Point> opt8;
opt8.emplace(30, 40);  // Point(30, 40) 생성

값 확인과 접근

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TD
    Start[optional 값 확인] --> HasValue{has_value?}
    HasValue -->|true| Access[값 접근]
    HasValue -->|false| Handle[nullopt 처리]
    
    Access --> Method1[value - 예외 가능]
    Access --> Method2[operator* - UB 가능]
    Access --> Method3[value_or - 안전]
    
    Method3 --> Safe[기본값 반환]
    
    style Method3 fill:#90EE90
    style Safe fill:#90EE90

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
int main() {
    std::optional<int> opt = 42;
    
    // 1. has_value() - 명시적 체크
    if (opt.has_value()) {
        std::cout << "값 있음: " << opt.value() << std::endl;
    }
    
    // 2. operator bool - 암시적 변환
    if (opt) {
        std::cout << "값 있음: " << *opt << std::endl;
    }
    
    // 3. value() - 예외 발생 가능
    try {
        std::optional<int> empty;
        int x = empty.value();  // 💥 std::bad_optional_access 예외
    } catch (const std::bad_optional_access& e) {
        std::cerr << "에러: " << e.what() << std::endl;
    }
    
    // 4. operator* - 역참조 (값이 없으면 UB)
    std::optional<int> opt2 = 100;
    std::cout << *opt2 << std::endl;  // 100
    
    // 5. operator-> - 멤버 접근
    std::optional<std::string> opt3 = "Hello";
    std::cout << opt3->length() << std::endl;  // 5
    
    // 6. value_or() - 기본값 제공 (가장 안전)
    std::optional<int> empty;
    int x = empty.value_or(0);  // 0 (기본값)
    std::cout << x << std::endl;
    
    return 0;
}

주의사항: 값이 없을 때 operator*는 미정의 동작이고 value()bad_optional_access를 던집니다. 방어 코드에서는 value_or나 선행 if를 기본으로 두세요.

값 수정과 제거

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
int main() {
    std::optional<int> opt = 42;
    
    // 1. 대입으로 값 변경
    opt = 100;
    std::cout << *opt << std::endl;  // 100
    
    // 2. reset() - 값 제거
    opt.reset();
    std::cout << opt.has_value() << std::endl;  // false
    
    // 3. nullopt 대입
    opt = 42;
    opt = std::nullopt;
    std::cout << opt.has_value() << std::endl;  // false
    
    // 4. emplace() - 새 값 생성
    opt.emplace(200);
    std::cout << *opt << std::endl;  // 200
    
    // 5. swap()
    std::optional<int> opt1 = 10;
    std::optional<int> opt2 = 20;
    opt1.swap(opt2);
    std::cout << *opt1 << " " << *opt2 << std::endl;  // 20 10
    
    return 0;
}

비교 연산

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
int main() {
    std::optional<int> a = 10;
    std::optional<int> b = 20;
    std::optional<int> empty;
    
    // optional끼리 비교
    std::cout << (a == b) << std::endl;     // false
    std::cout << (a < b) << std::endl;      // true
    std::cout << (a == empty) << std::endl; // false
    
    // 값과 직접 비교
    std::cout << (a == 10) << std::endl;    // true
    std::cout << (a != 20) << std::endl;    // true
    
    // nullopt와 비교
    std::cout << (empty == std::nullopt) << std::endl;  // true
    std::cout << (a != std::nullopt) << std::endl;      // true
    
    // 비교 규칙
    // - nullopt < 모든 값
    // - 값이 있으면 값끼리 비교
    std::optional<int> opt1 = 5;
    std::optional<int> opt2;
    std::cout << (opt2 < opt1) << std::endl;  // true (nullopt < 5)
    
    return 0;
}

3. C++23 모나딕 연산

and_then: 체이닝 (flatMap)

C++23부터 사용 가능 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
#include <string>
// 사용자 조회
std::optional<int> findUserId(const std::string& username) {
    if (username == "alice") return 1;
    if (username == "bob") return 2;
    return std::nullopt;
}
// 사용자 이메일 조회
std::optional<std::string> findEmail(int userId) {
    if (userId == 1) return "alice@example.com";
    if (userId == 2) return "bob@example.com";
    return std::nullopt;
}
int main() {
    // ❌ C++17 방식 (중첩된 if)
    auto userId = findUserId("alice");
    if (userId) {
        auto email = findEmail(*userId);
        if (email) {
            std::cout << "Email: " << *email << std::endl;
        }
    }
    
    // ✅ C++23 and_then (체이닝)
    auto email = findUserId("alice")
        .and_then(findEmail);
    
    if (email) {
        std::cout << "Email: " << *email << std::endl;
    }
    
    // 실패 케이스도 자연스럽게 처리
    auto noEmail = findUserId("charlie")  // nullopt 반환
        .and_then(findEmail);  // 실행되지 않음
    
    std::cout << noEmail.has_value() << std::endl;  // false
    
    return 0;
}

transform: 값 변환 (map)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
#include <string>
int main() {
    std::optional<int> opt = 42;
    
    // ❌ C++17 방식
    std::optional<std::string> str1;
    if (opt) {
        str1 = std::to_string(*opt);
    }
    
    // ✅ C++23 transform
    auto str2 = opt.transform( {
        return std::to_string(x);
    });
    
    std::cout << *str2 << std::endl;  // "42"
    
    // 체이닝
    auto result = std::optional<int>{10}
        .transform( { return x * 2; })      // 20
        .transform( { return x + 5; })      // 25
        .transform( { return std::to_string(x); });  // "25"
    
    std::cout << *result << std::endl;  // "25"
    
    // 빈 optional은 변환 안 됨
    std::optional<int> empty;
    auto result2 = empty.transform( {
        std::cout << "실행 안 됨" << std::endl;
        return x * 2;
    });
    std::cout << result2.has_value() << std::endl;  // false
    
    return 0;
}

or_else: 대체값 제공

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
std::optional<int> getFromCache(const std::string& key) {
    // 캐시 조회
    return std::nullopt;  // 캐시 미스
}
std::optional<int> getFromDatabase(const std::string& key) {
    // DB 조회
    return 42;
}
int main() {
    // ❌ C++17 방식
    auto value1 = getFromCache("user:123");
    if (!value1) {
        value1 = getFromDatabase("user:123");
    }
    
    // ✅ C++23 or_else
    auto value2 = getFromCache("user:123")
        .or_else( { return getFromDatabase("user:123"); });
    
    std::cout << *value2 << std::endl;  // 42
    
    // 여러 대체 소스 체이닝
    auto value3 = getFromCache("user:123")
        .or_else( { return getFromDatabase("user:123"); })
        .or_else( { return std::optional<int>{0}; });  // 최종 기본값
    
    return 0;
}

실전 조합: 복잡한 파이프라인

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
#include <string>
#include <map>
struct User {
    int id;
    std::string name;
    std::optional<std::string> email;
};
std::map<int, User> users = {
    {1, {1, "Alice", "alice@example.com"}},
    {2, {2, "Bob", std::nullopt}},
};
std::optional<User> findUser(int id) {
    auto it = users.find(id);
    if (it != users.end()) {
        return it->second;
    }
    return std::nullopt;
}
int main() {
    // 사용자 조회 → 이메일 추출 → 도메인 추출
    auto domain = findUser(1)
        .and_then( { return u.email; })
        .transform( {
            size_t pos = email.find('@');
            return email.substr(pos + 1);
        });
    
    if (domain) {
        std::cout << "Domain: " << *domain << std::endl;  // "example.com"
    }
    
    // 이메일 없는 사용자
    auto noDomain = findUser(2)
        .and_then( { return u.email; })  // nullopt 반환
        .transform( {
            // 실행 안 됨
            return email.substr(email.find('@') + 1);
        });
    
    std::cout << noDomain.has_value() << std::endl;  // false
    
    return 0;
}

4. 실전 에러 핸들링 패턴

패턴 1: 설정 파일 파싱

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <iostream>
#include <map>
#include <string>
class Config {
private:
    std::map<std::string, std::string> data_;
    
public:
    void set(const std::string& key, const std::string& value) {
        data_[key] = value;
    }
    
    std::optional<std::string> getString(const std::string& key) const {
        auto it = data_.find(key);
        if (it != data_.end()) {
            return it->second;
        }
        return std::nullopt;
    }
    
    std::optional<int> getInt(const std::string& key) const {
        return getString(key).and_then( -> std::optional<int> {
            try {
                return std::stoi(s);
            } catch (...) {
                return std::nullopt;
            }
        });
    }
    
    std::optional<bool> getBool(const std::string& key) const {
        return getString(key).transform( {
            return s == "true" || s == "1";
        });
    }
};
int main() {
    Config config;
    config.set("port", "8080");
    config.set("host", "localhost");
    config.set("debug", "true");
    config.set("invalid", "abc");
    
    // 값 있으면 사용, 없으면 기본값
    int port = config.getInt("port").value_or(3000);
    std::string host = config.getString("host").value_or("0.0.0.0");
    bool debug = config.getBool("debug").value_or(false);
    
    std::cout << "Port: " << port << std::endl;
    std::cout << "Host: " << host << std::endl;
    std::cout << "Debug: " << debug << std::endl;
    
    // 잘못된 값 처리
    auto invalid = config.getInt("invalid");
    if (!invalid) {
        std::cerr << "Invalid integer value" << std::endl;
    }
    
    return 0;
}

패턴 2: 캐시 시스템

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <map>
#include <string>
#include <chrono>
#include <iostream>
template<typename K, typename V>
class TimedCache {
private:
    struct Entry {
        V value;
        std::chrono::steady_clock::time_point expires_at;
    };
    
    std::map<K, Entry> data_;
    std::chrono::seconds default_ttl_;
    
public:
    explicit TimedCache(std::chrono::seconds ttl = std::chrono::seconds{60})
        : default_ttl_(ttl) {}
    
    void put(const K& key, const V& value) {
        auto expires = std::chrono::steady_clock::now() + default_ttl_;
        data_[key] = {value, expires};
    }
    
    std::optional<V> get(const K& key) {
        auto it = data_.find(key);
        if (it == data_.end()) {
            return std::nullopt;  // 캐시 미스
        }
        
        // 만료 확인
        if (std::chrono::steady_clock::now() > it->second.expires_at) {
            data_.erase(it);
            return std::nullopt;  // 만료됨
        }
        
        return it->second.value;
    }
    
    // C++23 or_else 활용
    template<typename F>
    V getOrCompute(const K& key, F&& compute) {
        return get(key).or_else([&]() -> std::optional<V> {
            V value = compute();
            put(key, value);
            return value;
        }).value();
    }
};
int main() {
    TimedCache<std::string, int> cache{std::chrono::seconds{5}};
    
    // 캐시 저장
    cache.put("user:123", 42);
    
    // 캐시 조회
    if (auto value = cache.get("user:123")) {
        std::cout << "Cached: " << *value << std::endl;
    }
    
    // 없으면 계산 후 저장
    int score = cache.getOrCompute("user:456",  {
        std::cout << "Computing..." << std::endl;
        return 100;  // DB에서 조회했다고 가정
    });
    std::cout << "Score: " << score << std::endl;
    
    return 0;
}

패턴 3: API 응답 처리

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <string>
#include <iostream>
struct ApiResponse {
    int status_code;
    std::optional<std::string> body;
    std::optional<std::string> error;
};
ApiResponse fetchData(const std::string& url) {
    // HTTP 요청 시뮬레이션
    if (url == "https://api.example.com/users") {
        return {200, R"({"users": []})", std::nullopt};
    } else {
        return {404, std::nullopt, "Not Found"};
    }
}
int main() {
    auto response = fetchData("https://api.example.com/users");
    
    if (response.status_code == 200 && response.body) {
        std::cout << "Success: " << *response.body << std::endl;
    } else if (response.error) {
        std::cerr << "Error: " << *response.error << std::endl;
    }
    
    // C++23 transform 활용
    auto bodyLength = response.body.transform( {
        return s.length();
    });
    
    std::cout << "Body length: " << bodyLength.value_or(0) << std::endl;
    
    return 0;
}

5. 성능 고려사항

메모리 오버헤드

#include <optional>
#include <iostream>
struct SmallType {
    int x;
};
struct LargeType {
    int data[1000];
};
int main() {
    std::cout << "int: " << sizeof(int) << " bytes" << std::endl;
    std::cout << "optional<int>: " << sizeof(std::optional<int>) << " bytes" << std::endl;
    // 출력: int: 4 bytes, optional<int>: 8 bytes (bool 플래그 + 패딩)
    
    std::cout << "SmallType: " << sizeof(SmallType) << " bytes" << std::endl;
    std::cout << "optional<SmallType>: " << sizeof(std::optional<SmallType>) << " bytes" << std::endl;
    
    std::cout << "LargeType: " << sizeof(LargeType) << " bytes" << std::endl;
    std::cout << "optional<LargeType>: " << sizeof(std::optional<LargeType>) << " bytes" << std::endl;
    // 큰 타입도 bool 플래그만 추가됨 (약 1바이트 + 패딩)
    
    return 0;
}

결론: optional의 오버헤드는 대부분 1바이트 + 정렬 패딩입니다.

언제 optional을 피해야 하는가

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 사용: 항상 값이 있는 경우
std::optional<int> getUserAge(int userId) {
    // 항상 나이를 반환한다면 optional 불필요
    return 25;
}
// ✅ 좋은 사용: 값이 없을 수 있는 경우
std::optional<int> getUserAge(int userId) {
    if (userId < 0) {
        return std::nullopt;  // 유효하지 않은 사용자
    }
    return 25;
}
// ❌ 나쁜 사용: 성능 크리티컬한 루프
for (int i = 0; i < 1000000; ++i) {
    std::optional<int> result = compute(i);  // 매번 optional 생성
    if (result) {
        process(*result);
    }
}
// ✅ 좋은 사용: 특수값으로 처리
for (int i = 0; i < 1000000; ++i) {
    int result = compute(i);  // -1 = 실패
    if (result != -1) {
        process(result);
    }
}
// ❌ 나쁜 사용: 참조를 optional로
// std::optional<T&>는 불가능
// std::optional<std::reference_wrapper<T>>는 복잡함
// ✅ 좋은 사용: 포인터 사용
T* ptr = findObject();  // nullptr 가능

성능 벤치마크

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <chrono>
#include <iostream>
// 1. optional vs 포인터
void benchmarkOptional() {
    auto start = std::chrono::high_resolution_clock::now();
    
    for (int i = 0; i < 10000000; ++i) {
        std::optional<int> opt = (i % 2 == 0) ? std::optional<int>{i} : std::nullopt;
        if (opt) {
            volatile int x = *opt;  // 최적화 방지
        }
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "optional: " << duration.count() << "ms" << std::endl;
}
void benchmarkPointer() {
    auto start = std::chrono::high_resolution_clock::now();
    
    for (int i = 0; i < 10000000; ++i) {
        int* ptr = (i % 2 == 0) ? new int(i) : nullptr;
        if (ptr) {
            volatile int x = *ptr;
            delete ptr;
        }
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "pointer: " << duration.count() << "ms" << std::endl;
}
int main() {
    benchmarkOptional();  // 약 50ms
    benchmarkPointer();   // 약 500ms (new/delete 오버헤드)
    
    // optional이 훨씬 빠름!
    return 0;
}

6. 다른 타입과 비교

optional vs variant

#include <optional>
#include <variant>
#include <string>
#include <iostream>
// optional: 값이 있거나 없음
std::optional<int> divide(int a, int b) {
    if (b == 0) return std::nullopt;
    return a / b;
}
// variant: 여러 타입 중 하나
std::variant<int, std::string> parseValue(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return "Parse error: " + str;
    }
}
int main() {
    // optional 사용
    if (auto result = divide(10, 2)) {
        std::cout << "Result: " << *result << std::endl;
    }
    
    // variant 사용
    auto value = parseValue("123");
    if (std::holds_alternative<int>(value)) {
        std::cout << "Integer: " << std::get<int>(value) << std::endl;
    } else {
        std::cout << "Error: " << std::get<std::string>(value) << std::endl;
    }
    
    return 0;
}
특징optionalvariant<T, E>
용도값이 있거나 없음여러 타입 중 하나
에러 정보없음 (nullopt만)에러 타입 저장 가능
크기sizeof(T) + 1max(sizeof(T), sizeof(E)) + 태그
사용간단한 실패상세한 에러 정보 필요

optional vs 예외

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <stdexcept>
#include <iostream>
// 예외 사용
int parseIntException(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (const std::exception& e) {
        throw std::runtime_error("Parse failed: " + str);
    }
}
// optional 사용
std::optional<int> parseIntOptional(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::nullopt;
    }
}
int main() {
    // 예외: 예외적인 상황
    try {
        int x = parseIntException("abc");
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    
    // optional: 예상된 실패
    if (auto x = parseIntOptional("abc")) {
        std::cout << "Parsed: " << *x << std::endl;
    } else {
        std::cout << "Parse failed (expected)" << std::endl;
    }
    
    return 0;
}
특징optional예외
사용 시기예상된 실패예외적 상황
성능빠름 (분기)느림 (스택 언와인딩)
에러 정보없음상세한 메시지
강제 처리아니오예 (catch 필요)
코드 흐름명시적암시적

optional vs expected (C++23 제안)

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// expected<T, E>: optional + 에러 정보
// C++23에서 표준화 예정
template<typename T, typename E>
class expected {
    // T 또는 E를 저장
    // optional<T>와 유사하지만 에러 정보 포함
};
// 사용 예시
expected<int, std::string> divide(int a, int b) {
    if (b == 0) {
        return unexpected{"Division by zero"};
    }
    return a / b;
}
// optional과 비교
std::optional<int> divideOptional(int a, int b) {
    if (b == 0) return std::nullopt;  // 에러 정보 없음
    return a / b;
}

관련 글: Optional과 Variant 활용에서 두 타입을 함께 사용하는 실전 패턴을 학습하세요.

7. 완전한 예제 모음

예제 1: JSON 파서

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <optional>
#include <string>
#include <map>
#include <iostream>
class JsonValue {
public:
    std::map<std::string, std::string> data;
    
    std::optional<std::string> getString(const std::string& key) const {
        auto it = data.find(key);
        return (it != data.end()) ? std::optional{it->second} : std::nullopt;
    }
    
    std::optional<int> getInt(const std::string& key) const {
        return getString(key).and_then( -> std::optional<int> {
            try {
                return std::stoi(s);
            } catch (...) {
                return std::nullopt;
            }
        });
    }
    
    std::optional<bool> getBool(const std::string& key) const {
        return getString(key).transform( {
            return s == "true";
        });
    }
};
int main() {
    JsonValue json;
    json.data[name] = "Alice";
    json.data[age] = "30";
    json.data[active] = "true";
    
    // 안전한 접근
    auto name = json.getString("name").value_or("Unknown");
    auto age = json.getInt("age").value_or(0);
    auto active = json.getBool("active").value_or(false);
    
    std::cout << "Name: " << name << std::endl;
    std::cout << "Age: " << age << std::endl;
    std::cout << "Active: " << active << std::endl;
    
    return 0;
}

8. 자주 발생하는 실수와 해결법

실수 1: value() 호출 시 예외

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 방법
std::optional<int> empty;
int x = empty.value();  // 💥 std::bad_optional_access 예외
// ✅ 올바른 방법 1: has_value() 체크
if (empty.has_value()) {
    int x = empty.value();
}
// ✅ 올바른 방법 2: operator bool
if (empty) {
    int x = *empty;
}
// ✅ 올바른 방법 3: value_or() (가장 안전)
int x = empty.value_or(0);

실수 2: 포인터처럼 사용

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 방법
std::optional<int> opt = 42;
if (opt != nullptr) {  // 💥 컴파일 에러
}
// ✅ 올바른 방법
if (opt.has_value()) {
    // ...
}
// 또는
if (opt) {
    // ...
}
// nullopt와 비교
if (opt != std::nullopt) {
    // ...
}

실수 3: optional<T&> 시도

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 방법 (컴파일 에러)
int x = 10;
// std::optional<int&> opt = x;  // 💥 불가능
// ✅ 올바른 방법 1: reference_wrapper
std::optional<std::reference_wrapper<int>> opt = std::ref(x);
if (opt) {
    opt->get() = 20;  // x가 20으로 변경됨
}
// ✅ 올바른 방법 2: 포인터 사용 (더 간단)
std::optional<int*> opt2 = &x;
if (opt2) {
    **opt2 = 30;
}

실수 4: 불필요한 복사

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 비효율적
std::optional<std::string> getLargeString() {
    std::string large(10000, 'x');
    return large;  // 복사 발생 (RVO가 작동하지 않을 수 있음)
}
// ✅ 효율적 (RVO 활용)
std::optional<std::string> getLargeString() {
    if (/* 조건 */) {
        return std::string(10000, 'x');  // RVO
    }
    return std::nullopt;
}
// ✅ 이동 명시
std::optional<std::string> getLargeString() {
    std::string large(10000, 'x');
    return std::move(large);  // 이동
}

실수 5: optional<optional> 중첩

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 복잡하고 혼란스러움
std::optional<std::optional<int>> nested() {
    return std::optional<int>{42};  // 💥 중첩된 optional
}
// ✅ 단일 optional 사용
std::optional<int> simple() {
    return 42;
}
// 정말 중첩이 필요하면 variant 고려
std::variant<int, std::string, std::monostate> alternative() {
    return 42;
}

실수 6: 성능 크리티컬한 코드에서 남용

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 핫 루프에서 optional 생성
for (int i = 0; i < 1000000; ++i) {
    std::optional<int> result = compute(i);
    if (result) {
        process(*result);
    }
}
// ✅ 특수값 사용 (더 빠름)
for (int i = 0; i < 1000000; ++i) {
    int result = compute(i);  // -1 = 실패
    if (result != -1) {
        process(result);
    }
}

9. 모범 사례·베스트 프랙티스

1. value_or()를 기본으로 사용

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ 간결하고 안전
int port = config.getInt("port").value_or(8080);
// ❌ 장황함
int port;
if (auto p = config.getInt("port")) {
    port = *p;
} else {
    port = 8080;
}

2. 함수 반환 타입으로 사용

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 실패 가능성을 타입으로 표현
std::optional<User> findUser(int id);
// ❌ 포인터 (메모리 관리 부담)
User* findUser(int id);
// ❌ 예외 (예상된 실패에는 과함)
User findUser(int id);  // 없으면 예외

3. C++23 모나딕 연산 활용

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ 체이닝으로 간결하게
auto email = findUser(123)
    .and_then( { return u.getEmail(); })
    .value_or("no-reply@example.com");
// ❌ 중첩된 if
std::string email = "no-reply@example.com";
if (auto user = findUser(123)) {
    if (auto e = user->getEmail()) {
        email = *e;
    }
}

4. 구조화된 바인딩 활용 (C++17)

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct Result {
    std::optional<int> value;
    std::optional<std::string> error;
};
Result compute() {
    // ...
    return {42, std::nullopt};
}
// ✅ 구조화된 바인딩
auto [value, error] = compute();
if (value) {
    std::cout << "Success: " << *value << std::endl;
} else if (error) {
    std::cerr << "Error: " << *error << std::endl;
}

5. 명확한 네이밍

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 명확한 함수 이름
std::optional<User> tryFindUser(int id);
std::optional<int> maybeParseInt(const std::string& str);
// ❌ 모호한 이름
User getUser(int id);  // 없으면 어떻게 되나?
int parseInt(const std::string& str);  // 실패하면?

10. 프로덕션 패턴

패턴 1: 옵셔널 체이닝 헬퍼

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template<typename T, typename F>
auto map_optional(const std::optional<T>& opt, F&& func) 
    -> std::optional<std::invoke_result_t<F, T>> {
    if (opt) {
        return func(*opt);
    }
    return std::nullopt;
}
// 사용
std::optional<int> opt = 42;
auto result = map_optional(opt,  { return x * 2; });

패턴 2: 여러 optional 결합

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template<typename T>
std::optional<std::vector<T>> collect_optionals(
    const std::vector<std::optional<T>>& opts) {
    std::vector<T> result;
    for (const auto& opt : opts) {
        if (!opt) {
            return std::nullopt;  // 하나라도 없으면 실패
        }
        result.push_back(*opt);
    }
    return result;
}
// 사용
std::vector<std::optional<int>> opts = {1, 2, 3};
if (auto values = collect_optionals(opts)) {
    // 모든 값이 있음
}

패턴 3: 지연 평가

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template<typename F>
class LazyOptional {
private:
    F compute_;
    mutable std::optional<std::invoke_result_t<F>> cache_;
    
public:
    explicit LazyOptional(F func) : compute_(std::move(func)) {}
    
    auto get() const -> std::optional<std::invoke_result_t<F>> {
        if (!cache_) {
            cache_ = compute_();
        }
        return cache_;
    }
};
// 사용
LazyOptional expensive{ -> std::optional<int> {
    // 비싼 계산
    return 42;
}};
// 필요할 때만 계산
if (auto value = expensive.get()) {
    std::cout << *value << std::endl;
}

11. 정리 및 체크리스트

optional 사용 가이드

상황사용 여부대안
검색 실패 가능✅ 사용-
설정 값 누락✅ 사용-
파싱 실패✅ 사용-
항상 값 있음❌ 불필요일반 타입
상세한 에러 정보 필요❌ 부적합variant, expected
성능 크리티컬❌ 신중히특수값, 포인터
참조 저장❌ 불가능포인터, reference_wrapper

체크리스트

다음은 bash를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# ✅ optional 사용 시
- [ ] 값이 없을 있는 상황인가?
- [ ] value_or() 기본값 제공했는가?
- [ ] value() 호출 has_value() 체크했는가?
- [ ] 불필요한 복사를 피했는가?
- [ ] 성능 크리티컬한 코드가 아닌가?
- [ ] 타입은 const 참조로 전달했는가?
# ✅ API 설계 시
- [ ] 함수 이름이 optional 반환을 암시하는가? (try~, maybe~, find~)
- [ ] 문서에 nullopt 반환 조건을 명시했는가?
- [ ] 대안 (예외, variant, expected)을 고려했는가?
- [ ] 체이닝이 너무 깊지 않은가? (3단계 이하 권장)
# ✅ 코드 리뷰 시
- [ ] optional<bool> 사용이 적절한가? (3-state 필요한가?)
- [ ] optional<optional<T>> 중첩은 없는가?
- [ ] 에러 정보가 필요하면 variant 고려했는가?

실전 팁: optional 효율적으로 사용하기

  1. 함수 이름으로 의도 표현

아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

   // ✅ 좋은 이름 (optional 반환 암시)
   std::optional<User> tryFindUser(int id);
   std::optional<int> maybeParseInt(const std::string& s);
   std::optional<Config> loadConfig(const std::string& path);
   
   // ❌ 모호한 이름
   User getUser(int id);  // 없으면 어떻게?
   int parseInt(const std::string& s);  // 실패하면?
  1. value_or()를 기본으로

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

   // ✅ 간결하고 안전
   int port = config.getInt("port").value_or(8080);
   
   // ❌ 장황함
   int port;
   auto opt = config.getInt("port");
   if (opt.has_value()) {
       port = opt.value();
   } else {
       port = 8080;
   }
  1. C++23 모나딕 연산 활용

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

   // ✅ 체이닝으로 간결하게
// 변수 선언 및 초기화
   auto result = findUser(id)
       .and_then( { return u.getEmail(); })
       .transform( { return e.toLowerCase(); })
       .value_or("unknown@example.com");
  1. 에러 로깅과 함께 사용

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

   auto user = findUser(id);
   if (!user) {
       LOG_WARNING("User not found: " + std::to_string(id));
       return std::nullopt;
   }
   return user->process();

빠른 참조: optional 사용 결정 트리

아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

값이 없을 수 있는가?
├─ Yes → optional 사용 고려
│   ├─ 에러 정보 필요? → variant<T, Error> 또는 expected<T, E>
│   ├─ 성능 크리티컬? → 특수값 또는 포인터 고려
│   └─ 일반적인 경우 → std::optional ✅
└─ No → 일반 타입 사용

트러블슈팅: 빠른 문제 해결

증상원인해결법
bad_optional_accessvalue() 호출 시 값 없음value_or() 사용 또는 has_value() 체크
컴파일 에러: optional<T&>참조 타입 불가optional<reference_wrapper> 또는 포인터 사용
성능 저하큰 타입을 값으로 복사const 참조로 전달
중첩 optionaloptional<optional>설계 재검토, variant 고려
체이닝 복잡너무 깊은 and_then중간 변수로 분리

optional vs 다른 방법 성능 비교

방법메모리 오버헤드실행 속도안전성사용 난이도
optionalsizeof(T) + 1~8바이트빠름높음쉬움
T* (포인터)8바이트매우 빠름낮음중간
unique_ptr8바이트 + 힙 할당느림높음중간
예외0 (실패 시만)매우 느림높음어려움
특수값0매우 빠름낮음쉬움

다음 단계


FAQ

Q1: optional은 언제 사용하나요?

A:

  • 값이 없을 수 있는 경우
  • 에러 표시 (간단한 경우)
  • 널 포인터 대체

Q2: optional vs 포인터?

A:

  • optional: 값 의미론, 안전
  • 포인터: 참조 의미론, 위험

Q3: optional vs 예외?

A:

  • optional: 예상된 실패
  • 예외: 예외적 상황

Q4: 성능 오버헤드는?

A: bool 플래그 하나 추가. 거의 무시할 수 있습니다.

Q5: optional<T&>는?

A: 불가능. optional<reference_wrapper> 사용.

Q6: Optional 학습 리소스는?

A:

  • cppreference.com
  • “C++17: The Complete Guide”
  • “Effective Modern C++“

Q7: optional을 함수 인자로 받을 때 const 참조를 써야 하나요?

A: 작은 타입은 값으로, 큰 타입은 const 참조로 전달하세요. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 작은 타입 (int, double 등): 값으로
void process(std::optional<int> opt) {
    if (opt) { /* ....*/ }
}
// 큰 타입 (string, vector 등): const 참조로
void process(const std::optional<std::string>& opt) {
    if (opt) { /* ....*/ }
}

Q8: optional을 멤버 변수로 사용할 때 주의사항은?

A: 생성자에서 명시적으로 초기화하고, 안전한 접근 패턴을 사용하세요. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Config {
private:
    std::optional<std::string> api_key_;
    
public:
    Config() : api_key_(std::nullopt) {}
    
    std::string getApiKey() const {
        return api_key_.value_or("default_key");
    }
};

Q9: optional은 언제 사용하나요?

A: “true/false/모름” 세 가지 상태가 필요할 때 사용합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 사용자 동의 상태
std::optional<bool> user_consent;  // nullopt = 아직 묻지 않음
if (!user_consent) {
    askForConsent();
} else if (*user_consent) {
    proceed();
} else {
    showError();
}

Q10: optional 체이닝이 너무 깊어지면?

A: 중간 변수로 분리하여 가독성을 높이세요. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 중간 변수로 분리
auto user = getUser(id);
if (!user) return std::nullopt;
auto profile = user->getProfile();
if (!profile) return std::nullopt;
return profile->getEmail();

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

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


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3