C++ std::expected, 실패를 예외 없이 다루는 법
C++의 std::expected는 성공 값 또는 오류 값을 한 객체에 담는 C++23 유틸리티이며, 예외를 던지지 않고 실패를 반환할 때 씁니다. 호출자는 성공 여부를 확인한 뒤 값 또는 오류를 명시적으로 처리할 수 있습니다.
이 글은 cppreference의 std::expected 문서를 2026-08-30 기준으로 정리한 일반 설명이며, 표준 버전과 구현에 따라 세부 동작은 달라질 수 있습니다.
std::expected는 무엇인가?
한 줄 답: 이 타입은 기대한 값 T 또는 예상하지 못한 값 E 중 하나를 나타내며, 값도 오류도 없는 제3의 상태를 갖지 않습니다.
<expected> 헤더에 정의된 template< class T, class E > class expected;는 C++23부터 제공됩니다. 주 템플릿은 성공 값 또는 오류 값을 객체 내부의 저장 공간에 보관하며, 항상 둘 중 하나를 유지합니다.
value_type은 성공 값의 형식 T, error_type은 오류 값의 형식 E를 나타냅니다. unexpected_type은 std::unexpected<E>이며 오류 상태를 담는 형식이고, rebind<U>는 오류 형식을 유지하면서 성공 값 형식만 U로 바꾼 별칭입니다.
std::unexpected<E>를 단일 인자로 받는 생성자는 오류 값을 담은 상태를 만듭니다. 따라서 실패를 반환하는 코드는 다음처럼 작성할 수 있습니다.
return std::unexpected(parse_error::invalid_input);
성공 값이 없는 작업은 void 부분 특수화를 사용하여 성공한 void 상태 또는 오류 값을 나타낼 수 있습니다. 참조형·함수형·std::unexpected 특수화로는 인스턴스화할 수 없으며, T와 E도 저장과 소멸에 관한 형식 제약을 만족해야 합니다.
optional과 무엇이 다른가?
한 줄 답: optional은 값의 부재를 표현하고, 이 타입은 실패 사유가 담긴 오류 값까지 함께 표현합니다.
optional이 객체를 포함하지 않을 수 있는 래퍼라면, 이 반환은 성공 값 또는 오류 값 중 하나를 보관합니다. 따라서 값이 없는지만 알면 되는 경우와 실패 이유를 호출자에게 전달해야 하는 경우를 구분합니다.
성공과 실패는 어떻게 꺼내나?
한 줄 답: has_value() 또는 if (result)로 성공 여부를 먼저 확인하고, 성공이면 값을, 실패면 오류 값을 꺼냅니다.
has_value()와 명시적 operator bool()은 객체가 성공 값을 나타내는지 확인합니다. 반환값이 true이면 성공 상태이고, false이면 이 타입이 never valueless이므로 오류 값 상태입니다.
value()는 성공 값에 대한 참조를 반환합니다. 실패 상태에서 호출하면 오류 값의 사본을 담은 std::bad_expected_access<std::decay_t<E>>를 던지며, void 부분 특수화에서는 성공 시 반환할 값이 없습니다.
error()는 실패 상태의 오류 값에 접근합니다. 성공 상태에서 호출하면 C++26 전까지 정의되지 않은 동작이므로, 성공 여부를 확인하지 않고 호출하면 안 됩니다.
value_or(default_value)는 성공 값이 있으면 그 값을, 없으면 전달한 기본값을 반환합니다. error_or는 오류 값이 있으면 그것을, 성공 상태면 대체 오류 값을 반환하며, void 부분 특수화에는 이 멤버들이 없습니다.
공식 parse_number 예시의 핵심 흐름은 다음과 같습니다. 실패할 때 std::unexpected를 반환하고, 호출부에서 has_value()로 분기한 뒤 성공 값은 operator*로, 실패 원인은 error()로 확인합니다.
auto parse_number(std::string_view& str)
-> std::expected<double, parse_error>
{
const char* begin = str.data();
char* end;
double retval = std::strtod(begin, &end);
if (begin == end)
return std::unexpected(parse_error::invalid_input);
if (std::isinf(retval))
return std::unexpected(parse_error::overflow);
str.remove_prefix(end - begin);
return retval;
}
if (const auto num = parse_number(input); num.has_value())
consume(*num);
else
handle(num.error());
실패 상태에서 검사 없이 operator*나 operator->를 사용하면 정의되지 않은 동작이 될 수 있습니다. 같은 상황에서 value()는 bad_expected_access 예외를 던지고, value_or는 지정한 기본값을 사용합니다.
and_then, transform, or_else, transform_error도 C++23에서 제공되며 성공·실패 흐름을 연결할 때 사용할 수 있습니다. 각 멤버의 반환 규약은 사용하려는 함수와 함께 확인해야 합니다.
예외 대신 이 타입을 쓰는 이유는 무엇인가?
한 줄 답: 실패를 E형 반환값으로 전달하므로 호출자가 같은 반환 경로에서 성공과 오류를 분기할 수 있습니다.
has_value() 또는 operator bool()로 분기하면 실패가 스택을 풀어 올리는 예외가 아니라 호출자가 확인하는 값으로 전달됩니다. 실패 상태에서는 error()로 원인을 읽거나 error_or 같은 대체 경로를 선택할 수 있습니다.
다만 “예외 없이”라는 표현은 이 반환을 값으로 확인하는 흐름을 뜻합니다. 실패 상태에서 value()를 호출하면 std::bad_expected_access가 발생하므로, value()까지 예외를 사용하지 않는다는 뜻은 아닙니다.
이 타입은 항상 성공 값 또는 오류 값 중 하나를 가지므로 상태를 명시적으로 모델링할 수 있습니다. 그렇다고 모든 예외를 대체하거나 언제나 더 빠르다고 단정할 수는 없으며, 오류를 값으로 전달하는 API가 적합한지는 호출 규약에 따라 판단해야 합니다.
C++ 개념을 별도로 점검하려면 C++ 기술면접 질문 10가지를 참고할 수 있으며, 해당 글의 내용은 여기서 다시 다루지 않습니다.
FAQ
한 줄 답: 성공 여부 확인, 오류 접근의 전제, value()의 예외 조건을 구분하면 이 반환을 안전하게 사용할 수 있습니다.
Q. 실패를 예외 없이 반환하려면 무엇을 사용합니까? A. 성공 값과 오류 값을 함께 표현하는 이 반환을 사용하고, 호출부에서 성공 여부를 확인한 뒤 오류 값을 처리합니다.
Q. has_value()가 true일 때 error()를 호출하면 어떻게 됩니까? A. 성공 상태에서 error()를 호출하면 C++26 전까지 정의되지 않은 동작이므로, 호출 전에 실패 상태인지 확인해야 합니다.
Q. value()는 예외를 던집니까? A. 성공 상태에서는 값의 참조를 반환하지만, 실패 상태에서는 std::bad_expected_access를 던집니다.
Q. optional과 이 타입은 언제 구분해 사용합니까? A. 값의 부재만 표현하면 전자를 사용하고, 실패 사유를 오류 값으로 함께 전달해야 하면 후자를 사용합니다.
출처
한 줄 답: 상태 모델과 멤버의 사전조건은 cppreference의 C++23 expected 및 관련 문서에서 확인할 수 있습니다.
- 개요·never valueless·공식 예시: expected 개요
- 오류 값을 나타내는 형식:
std::unexpected - 생성자:
expected생성자 - 성공 여부 확인:
has_value()와operator bool - 값 접근과 예외:
value()와bad_expected_access - 오류 접근:
error() - 대체 값 접근:
value_or - 흐름 연결:
and_then와transform - 값 부재를 표현하는 래퍼:
optional