콘텐츠로 이동

C++23 std::expected 오류 처리

std::expected<T, E>는 C++23에서 도입된 값 또는 오류를 담는 타입입니다. 함수가 성공하면 T 값을, 실패하면 E 오류를 반환합니다. 예외를 던지지 않으면서도 오류 정보를 명시적으로 전달할 수 있습니다.


#include <expected>
#include <string>
#include <charconv>
enum class ParseError { InvalidInput, Overflow };
std::expected<int, ParseError> parse_int(std::string_view s)
{
int result{};
auto [ptr, ec] = std::from_chars(s.begin(), s.end(), result);
if (ec == std::errc::invalid_argument)
return std::unexpected(ParseError::InvalidInput);
if (ec == std::errc::result_out_of_range)
return std::unexpected(ParseError::Overflow);
return result; // 성공
}
int main()
{
if (auto val = parse_int("42"))
std::cout << *val << '\n'; // 42
else
std::cout << "오류\n";
auto bad = parse_int("abc");
if (!bad)
// bad.error() == ParseError::InvalidInput
std::cout << "파싱 실패\n";
}

C++23은 and_then, or_else, transform, transform_error 네 가지 체이닝 연산자를 제공합니다.

#include <expected>
#include <string>
std::expected<std::string, std::string> read_file(const std::string& path);
std::expected<int, std::string> parse_config(const std::string& content);
std::expected<void, std::string> apply_config(int value);
void load_settings(const std::string& path)
{
auto result = read_file(path)
.and_then(parse_config) // 성공 시 다음 단계
.and_then(apply_config) // 성공 시 다음 단계
.transform_error([](auto& err) { // 오류 타입 변환
return "설정 로드 실패: " + err;
});
if (!result)
std::cerr << result.error() << '\n';
}

auto doubled = parse_int("21")
.transform([](int v) { return v * 2; }); // expected<int, ParseError>{42}
auto as_string = parse_int("42")
.transform([](int v) { return std::to_string(v); }); // expected<string, ParseError>

auto recovered = parse_int("bad")
.or_else([](ParseError) {
return std::expected<int, ParseError>{0}; // 기본값 반환
});
// *recovered == 0

항목std::optional<T>std::expected<T, E>
오류 정보없음 (nullopt)있음 (E 타입)
오류 종류 구분불가가능
체이닝transform, and_then동일 + or_else
용도”값이 없을 수 있는” 상황”실패 이유가 있는” 상황

6. 실전 패턴 — 파일 파싱 파이프라인

섹션 제목: “6. 실전 패턴 — 파일 파싱 파이프라인”
#include <expected>
#include <fstream>
#include <string>
enum class FileError { NotFound, PermissionDenied, ParseFailed };
std::expected<std::string, FileError> read_file(const std::string& path)
{
std::ifstream f(path);
if (!f) return std::unexpected(FileError::NotFound);
return std::string(std::istreambuf_iterator<char>(f), {});
}
std::expected<Config, FileError> load_config(const std::string& path)
{
return read_file(path)
.and_then([](const std::string& content)
-> std::expected<Config, FileError>
{
if (content.empty())
return std::unexpected(FileError::ParseFailed);
return Config::parse(content);
});
}

int val = parse_int("bad").value_or(0); // 오류 시 0 반환

std::expected<T, E>의 오류 타입 E는 용도에 따라 다양하게 설계할 수 있습니다.

// 전략 1: 열거형 오류 코드 (가장 단순, 오버헤드 없음)
enum class DbError { ConnectionFailed, QueryFailed, NotFound, Timeout };
std::expected<User, DbError> find_user(int id);
// 전략 2: 문자열 오류 메시지 (디버깅에 유리, 성능 비용 있음)
std::expected<int, std::string> parse_port(std::string_view s);
// 전략 3: 구조체 오류 (오류 코드 + 메시지 + 컨텍스트)
struct Error {
int code;
std::string message;
std::string context; // 어디서 발생했는지
static Error make(int code, std::string msg, std::string ctx = {})
{
return {code, std::move(msg), std::move(ctx)};
}
};
std::expected<Config, Error> load_config(const std::string& path);
// 전략 4: std::error_code (표준 시스템 오류와 통합)
#include <system_error>
std::expected<std::string, std::error_code> read_file(const std::string& path)
{
std::ifstream f(path);
if (!f)
return std::unexpected(
std::make_error_code(std::errc::no_such_file_or_directory)
);
return std::string(std::istreambuf_iterator<char>(f), {});
}

9. 예외 vs std::expected vs std::optional 비교

섹션 제목: “9. 예외 vs std::expected vs std::optional 비교”
항목예외std::expected<T,E>std::optional<T>
오류 정보 전달예외 객체E 타입없음
성능 비용경로에 따라 크거나 작음거의 없음거의 없음
무시 가능 여부무시하면 terminate가능 (경고 없음)가능
오류 전파자동 스택 언와인딩수동 (and_then)수동
코드 흐름 명확성암묵적명시적명시적
적합한 상황예외적 오류, 복잡한 복구예측 가능한 실패값 없음 표현
// 예외 방식
std::string read_file_throw(const std::string& path)
{
std::ifstream f(path);
if (!f) throw std::runtime_error("파일 없음: " + path);
return std::string(std::istreambuf_iterator<char>(f), {});
}
// std::expected 방식
std::expected<std::string, std::string> read_file_expected(const std::string& path)
{
std::ifstream f(path);
if (!f) return std::unexpected("파일 없음: " + path);
return std::string(std::istreambuf_iterator<char>(f), {});
}
// 호출 측 비교
// 예외: try-catch 필요, 오류 경로가 숨겨짐
try {
auto content = read_file_throw("config.json");
} catch (const std::exception& e) {
std::cerr << e.what();
}
// expected: 오류 경로가 타입으로 명시됨
if (auto content = read_file_expected("config.json"))
process(*content);
else
std::cerr << content.error();

10. 실전 패턴 — HTTP 요청 파이프라인

섹션 제목: “10. 실전 패턴 — HTTP 요청 파이프라인”
#include <expected>
#include <string>
enum class HttpError {
InvalidUrl,
ConnectionRefused,
Timeout,
ParseError,
NotFound
};
std::string to_string(HttpError e)
{
switch (e) {
case HttpError::InvalidUrl: return "잘못된 URL";
case HttpError::ConnectionRefused: return "연결 거부";
case HttpError::Timeout: return "타임아웃";
case HttpError::ParseError: return "파싱 오류";
case HttpError::NotFound: return "리소스 없음";
default: return "알 수 없는 오류";
}
}
// 각 단계는 std::expected 반환
std::expected<std::string, HttpError> validate_url(const std::string& url);
std::expected<std::string, HttpError> fetch_content(const std::string& url);
std::expected<nlohmann::json, HttpError> parse_json(const std::string& body);
std::expected<User, HttpError> extract_user(const nlohmann::json& json);
// 파이프라인 — 어느 단계든 실패하면 오류 전파
std::expected<User, HttpError> get_user(const std::string& url)
{
return validate_url(url)
.and_then(fetch_content)
.and_then(parse_json)
.and_then(extract_user);
}
// 사용
auto result = get_user("https://api.example.com/users/42");
if (result)
std::cout << result->name << "\n";
else
std::cerr << "오류: " << to_string(result.error()) << "\n";

std::expected<int, std::string> ex{42};
std::expected<int, std::string> err = std::unexpected("실패");
// 값 확인
bool has_val = ex.has_value(); // true
bool check = static_cast<bool>(ex); // true
// 값 접근
int v1 = *ex; // 역참조 (has_value() 확인 필요)
int v2 = ex.value(); // 값 없으면 std::bad_expected_access 예외
int v3 = ex.value_or(0); // 오류 시 기본값
// 오류 접근
auto& e = err.error(); // 오류 값 참조
// 체이닝
auto r1 = ex.and_then([](int v) -> std::expected<double, std::string> {
return v * 1.5;
});
auto r2 = err.or_else([](const std::string&) -> std::expected<int, std::string> {
return 0; // 복구
});
auto r3 = ex.transform([](int v) { return v * 2; }); // expected<int, string>{84}
auto r4 = err.transform_error([](const std::string& e) {
return "Error: " + e;
});

std::expected는 예외 비용 없이 함수 실패 이유를 타입 시스템으로 표현합니다. and_then 체이닝으로 여러 단계를 깔끔하게 연결하고, 오류가 발생한 지점부터 자동으로 이후 단계를 건너뜁니다. 오류 이유가 중요한 시스템 경계 함수에서 std::optional 대신 사용하세요.

선택 기준:

  • 오류 이유가 필요없고 단순히 “없음”을 표현 → std::optional<T>
  • 오류 이유가 있고, 예측 가능한 실패 → std::expected<T, E>
  • 예외적이고 복구가 복잡한 오류 → 예외(exception)