/

C++ std::optional, 값이 없을 때 예외 없이 다루는 법

C++ std::optional은 값이 있을 수도 없을 수도 있는 C++17 래퍼이며, has_value와 value_or로 빈 상태를 예외 없이 확인합니다. 값이 필요한 경우에만 `value()`를 선택하고, 빈 상태에서는 기본값 반환이나 조건 분기를 사용합니다.

이 글은 cppreference의 std::optional 문서를 2026-08-27 기준으로 정리한 일반 설명이며, 표준 버전과 구현에 따라 세부 동작은 달라질 수 있습니다.

값이 없을 때를 예외 없이 다루는 이유는?

**한 줄 답:** 이 타입은 값이 있을 수도 없을 수도 있는 상태를 객체로 나타내므로, 값 부재를 정상적인 분기로 표현할 수 있습니다.

`` 헤더에 정의된 이 클래스 템플릿은 C++17부터 제공되며, `template< class T > class optional;` 형태로 선언됩니다. 이 타입은 포함된 값이 있을 수도 없을 수도 있는 상태를 관리합니다. 따라서 함수가 값을 반환하지 못할 수 있을 때도 반환 형식에 값의 부재를 함께 표현할 수 있습니다.

cppreference는 실패할 수 있는 함수의 반환값을 흔한 사용처로 설명하며, `std::pair<T, bool>`보다 의도를 명시하기 쉽다고 설명합니다. `T`와 성공 여부를 별도로 해석하는 대신, 반환 객체 자체가 값 포함 여부를 가집니다. 비용이 큰 객체도 이런 반환 형태로 다룰 수 있다고 설명하지만, 구현별 비용이나 성능 수치는 이 글에서 다루지 않습니다.

빈 상태 자체는 예외가 아닙니다. `has_value()`, 조건부 `bool` 변환, `value_or()`는 빈 상태를 확인하거나 대체 값을 고르는 동작이며, 빈 상태 때문에 `std::bad_optional_access`를 던지지 않습니다.

has_value와 value_or는 어떻게 쓰나?

**한 줄 답:** `has_value()` 또는 `if (opt)`로 값 존재를 확인하고, 대체 값이 필요하면 `value_or(default_value)`로 읽습니다.

`has_value()`는 포함 값이 있으면 `true`, 없으면 `false`를 반환합니다. `explicit operator bool()`도 조건부 문맥에서 같은 값을 제공하므로 `if (opt)` 형태로 확인할 수 있습니다. 두 함수는 C++17부터 제공되며 `noexcept`입니다.

```cpp
std::optional opt;
opt.has_value(); // false

opt = 43;
if (opt)
/* opt.value()로 값에 접근할 수 있습니다. */

opt.reset();
opt.has_value(); // false
```

`value_or(default_value)`는 값이 있으면 포함 값을 반환하고, 없으면 전달한 기본값을 반환합니다. 그래서 반환값이 없을 때 사용할 정책이 하나의 표현식에 드러나며, 빈 상태 때문에 `bad_optional_access`를 던지지 않습니다. `value_or`를 사용할 때에는 기본값이 포함 값의 타입으로 변환될 수 있고, 필요한 복사 또는 이동 생성 조건을 만족해야 합니다.

```cpp
maybe_getenv("SHELL").value_or("(none)");
maybe_getenv("MYPWD").value_or("(none)");
```

실제 반환 내용은 실행 환경에 따라 달라질 수 있으므로 특정 환경 변수 경로를 전제하지 않습니다.

포인터와 무엇이 다른가?

**한 줄 답:** 포함된 값은 이 래퍼 객체 안에 중첩되므로, 접근 연산자가 있어도 포인터 모델이 아니라 객체 모델입니다.

어떤 인스턴스도 각 시점에 값을 포함하거나 포함하지 않는 둘 중 하나의 상태입니다. 값이 포함되면 그 값은 이 래퍼 객체 안에 중첩됩니다. 그러므로 빈 상태는 널 주소를 가리키는 상태가 아니라, 객체가 값을 포함하지 않는 상태입니다.

`operator*`와 `operator->`가 제공되지만, 이 연산자들은 포함 값에 접근하는 문법일 뿐 이 타입을 포인터 타입으로 바꾸지 않습니다. 포인터와 같은 주소·널 여부의 모델로 해석하지 않고, 값이 있는지 먼저 확인하는 객체 모델로 이해해야 합니다. 이 차이는 값의 존재를 표현하는 방식과 접근 실패를 처리하는 방식을 구분하게 합니다.

[C++ 기술면접 질문 10가지](https://donggrri.tistory.com/48)는는) 이 글의 값 부재 표현과 별도로 C++ 개념을 점검할 때 참고할 수 있습니다.

value()는 왜 예외를 던지나?

**한 줄 답:** `value()`는 값이 있으면 포함 값의 참조를 반환하지만, 없으면 `std::bad_optional_access`를 던져 접근 실패를 명시합니다.

`value()`는 값이 있으면 포함 값에 대한 참조를 반환합니다. 값이 없으면 `std::bad_optional_access` 예외를 던지므로, 호출자가 예외를 처리할 수 있는 접근 실패 신호가 됩니다. 다음 예시는 빈 상태에서 호출한 뒤 값을 대입하면 정상적으로 읽고 쓸 수 있음을 보여줍니다.

```cpp
std::optional opt = {};
try {
int n = opt.value(); // 예외가 발생합니다.
} catch (const std::bad_optional_access& e) {
// e.what() → "bad optional access"
}

opt = 43;
*opt; // 43
opt.value() = 44;
```

예외 없이 처리하려는 목적이면 `value()`를 바로 호출하지 말고 먼저 `has_value()` 또는 `if (opt)`로 분기하거나 `value_or()`를 선택합니다. 반대로 값이 반드시 있어야 하며 접근 실패를 예외로 알리는 계약이라면 `value()`의 동작을 사용합니다. `operator*`와 `operator->`는 빈 상태를 검사하지 않으므로 빈 상태에서 호출하면 정의되지 않은 동작이 발생하며, 정의된 예외가 발생하는 것이 아닙니다.

cppreference는 검사하지 않는 `operator*`가 `value()`보다 더 효율적일 수 있다고 설명합니다. 다만 이 설명은 검사 여부의 차이를 말하는 것이므로, 특정 구현의 속도나 예외 비용 수치로 확대하지 않습니다.

disengaged 상태는 무엇인가?

**한 줄 답:** disengaged는 값이 없는 상태이며, 기본 생성·빈 래퍼 또는 `std::nullopt` 대입·`reset()` 호출로 만들 수 있습니다.

`disengaged`는 값이 없는 상태를 가리키는 용어입니다. `std::nullopt`는 이 상태를 나타내는 `std::nullopt_t` 타입의 상수이며, 값이 포함되지 않았음을 명시할 때 사용합니다. 이 상태에서 `has_value()`는 `false`를 반환하고 조건부 `bool` 변환도 거짓이 됩니다.

기본 초기화한 객체, 빈 래퍼 또는 `std::nullopt_t`로 초기화·대입한 객체는 값을 포함하지 않습니다. `reset()`을 호출해도 포함된 값이 있으면 그 값이 파괴되고, 객체는 값 미포함 상태로 남습니다. `op = {};`와 `op = nullopt;`(일반적인 표기는 `op = std::nullopt;`)는 모두 객체를 disengaging하는 문법으로 사용할 수 있습니다.

공식 예시의 `create`와 `create2`는 값이 있을 때 `"Godzilla"`를 반환하고, 없을 때 각각 `{}`와 `std::nullopt`를 반환합니다. `if (auto str = create2(true))`는 값 존재를 조건부 `bool`로 확인한 뒤 `*str`로 접근하는 형태입니다.

```cpp
std::optionalstd::string create(bool b)
{
if (b)
return "Godzilla";
return {};
}

auto create2(bool b)
{
return b ? std::optionalstd::string{"Godzilla"} : std::nullopt;
}

create(false).value_or("empty"); // "empty"
if (auto str = create2(true))
*str; // "Godzilla"
```

FAQ

**한 줄 답:** 빈 상태 확인, 기본값 반환, 검사 없는 접근의 차이를 구분하면 핵심 사용 판단을 할 수 있습니다.

빈 상태를 다루는 API는 목적에 따라 나누어 선택합니다. 다음 질문과 답변은 값 유무 확인, 기본값 처리, 접근 실패를 구분하는 기준을 정리합니다.

질문 답변
값이 있는지 확인하는 방법은 무엇입니까? `has_value()`와 `if (opt)`의 조건부 `bool` 변환은 모두 값이 있으면 `true`, 없으면 `false`를 반환합니다.
빈 상태에서 기본값을 사용하려면 무엇을 선택합니까? `value_or(default_value)`를 사용하면 값이 있을 때 포함 값을, 없을 때 기본값을 반환하며 빈 상태 때문에 `bad_optional_access`를 던지지 않습니다.
빈 상태에서 `value()`를 호출하면 어떻게 됩니까? `std::bad_optional_access` 예외를 던집니다.
빈 상태에서 `*opt` 또는 `opt->member`를 호출하면 어떻게 됩니까? 빈 상태 검사가 없으므로 정의되지 않은 동작입니다. 먼저 값 유무를 확인하거나 `value()` 또는 `value_or()`를 선택해야 합니다.
모든 타입을 `T`로 사용할 수 있습니까? 참조·함수·배열·cv 한정 `void`, 그리고 `nullopt_t`·`in_place_t`는 인스턴스화할 수 없습니다. `T`는 Destructible 요구사항을 만족해야 합니다.

출처

**한 줄 답:** 포함 값의 상태와 객체 모델, 관찰자, 빈 상태 표기는 모두 2026-08-27 기준 cppreference 문서로 확인합니다.

본문의 표준 동작과 코드 예시는 아래의 공식 cppreference 문서를 바탕으로 정리합니다. 문서의 세부 동작은 표준 버전과 구현에 따라 달라질 수 있으므로 실제 사용 환경에서 다시 확인해야 합니다.

- [optional 개요](https://en.cppreference.com/w/cpp/utility/optional))
- [has_value와 operator bool](https://en.cppreference.com/w/cpp/utility/optional/operator_bool))
- [value_or](https://en.cppreference.com/w/cpp/utility/optional/value_or))
- [value](https://en.cppreference.com/w/cpp/utility/optional/value))
- [std::nullopt](https://en.cppreference.com/w/cpp/utility/optional/nullopt))
- [operator*와 operator->](https://en.cppreference.com/w/cpp/utility/optional/operator*))
- [ 헤더](https://en.cppreference.com/w/cpp/header/optional))

+ Recent posts