본문 바로가기
Language/C++

std::expected를 이용한 효과적인 에러처리

by W00gie 2026. 10. 1.

https://en.cppreference.com/cpp/utility/expected

 

std::expected - cppreference.com

template< class T, class E > class expected; (1) (since C++23) template< class T, class E > requires std::is_void_v class expected ; (2) (since C++23) The class template std::expected provides a way to represent either of two values: an expected value of t

en.cppreference.com

 

이번 포스트에서는 C++23에 포함된 std::expected를 활용한 에러 처리 코드를 소개한다.

 

1. Expected 장점

std::expected는 성공했다면 결과값, 실패했을 때의 에러 이유 중 하나를 안전하게 전달한다.

기존까지 여러 에러처리방식이 있었지만 각각 단점이 존재했다.

기존까지 존재하는 함수실행후 에러처리 방식은 아래와 같다.

에러 코드 반환 (int 등) 함수가 0이나 -1을 리턴 진짜 결과값과 에러 코드를 혼동하기 쉽고, 왜 실패했는지 구체적인 이유를 알기 어려움
예외 던지기 (try-catch) throw로 에러 전파 코드가 복잡해지고, 성능 저하(오버헤드)가 발생할 수 있음
std::optional 값이 있으면 값 반환, 없으면 빈 값(std::nullopt) 반환 값이 왜 없는지(에러의 이유)를 알 수 없음 ("그냥 실패했구나" 외에는 정보가 없음)

 

위와 같은 단점에서 나아가

expected를 사용하게되면, optional보다 한단계 나아가서 왜 실패했는지에 대한 구체적인 에러타입을 담을 수 있다.

 

2. 활용사례

필자의 경우 사이드 프로젝트의 로그인 로직에서 user 오브젝트를 할당해주는 결과값과

'로그인 실패' 라는 케이스를 구분하는데에서 해당 기능을 적용해 보았다.

 template <typename T>
 using expected = std::expected<T, error>;

auto user_manager::user_login(session_s_ptr_t session, std::wstring_view user_name) -> fw::expected<user_s_ptr_t>
{
	auto error = fw::error{};

	ASSERT_RETURN_VALUE(session != nullptr, fw::unexpected(error::code::session_invalid));

	if (!is_valid_user_name(user_name))
	{
		FLOG_WARN("user_login :: invalid user_name length({}) session({})", user_name.size(), session->get_index());
		return fw::unexpected(error::code::user_name_invalid);
	}

	if (find_user(session) != nullptr)
	{
		FLOG_WARN("user_login :: already login session({})", session->get_index());
		return fw::unexpected(error::code::user_already_login);
	}

	auto sql = fw::get_sql<user_sql>();
	ASSERT_RETURN_VALUE(sql != nullptr, fw::unexpected(error::code::sql_stmt_invalid));

	user_no_t user_no{};
	error = sql->user_login(user_name, user_no);
	ASSERT_RETURN_VALUE(!error, fw::unexpected(error));
	ASSERT_RETURN_VALUE(user_no != user_no_t{}, fw::unexpected(error::code::user_login_fail));

	auto user = user_pool_.AcquireObject();
	ASSERT_RETURN_VALUE(nullptr != user, fw::unexpected(error::code::object_acquire_fail));

	user->set_session(session);
	user->set_user_no(user_no);

	auto [it, inserted] = user_list_.insert(user);
	ASSERT_RETURN_VALUE(inserted, fw::unexpected(error::code::user_already_login));

	return *it;
}

 

3. 내부 구현 및 성능

내부적으로 std::variant<T,E>와 매우 유사한 구조다.

- 성공 데이터(T)와 에러 데이터(E)는 크기가 크거나 타입이 다를 수 있으므로, 메모리를 공유하는 union 공간에 저장된다.

- RVO및 이동 연산(Move)이 적용되어 복사 비용이 없다.