C++23 std::expected 오류 처리
std::expected<T, E>는 C++23에서 도입된 값 또는 오류를 담는 타입입니다. 함수가 성공하면 T 값을, 실패하면 E 오류를 반환합니다. 예외를 던지지 않으면서도 오류 정보를 명시적으로 전달할 수 있습니다.
1. 기본 사용
섹션 제목: “1. 기본 사용”#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";}2. 모나딕 체이닝 연산자
섹션 제목: “2. 모나딕 체이닝 연산자”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';}3. transform — 성공 값 변환
섹션 제목: “3. transform — 성공 값 변환”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>4. or_else — 오류 복구
섹션 제목: “4. or_else — 오류 복구”auto recovered = parse_int("bad") .or_else([](ParseError) { return std::expected<int, ParseError>{0}; // 기본값 반환 });// *recovered == 05. std::optional vs std::expected
섹션 제목: “5. std::optional vs std::expected”| 항목 | 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); });}7. value_or — 기본값 제공
섹션 제목: “7. value_or — 기본값 제공”int val = parse_int("bad").value_or(0); // 오류 시 0 반환8. 오류 타입 설계 전략
섹션 제목: “8. 오류 타입 설계 전략”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";11. expected 멤버 함수 요약
섹션 제목: “11. expected 멤버 함수 요약”std::expected<int, std::string> ex{42};std::expected<int, std::string> err = std::unexpected("실패");
// 값 확인bool has_val = ex.has_value(); // truebool 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)