[C++23 새기능 소개] std::expected

C++23에서는 코드의 안정성과 오류 처리를 향상시키기 위한 새로운 기능으로 std::expected가 도입되었습니다. 이번 글에서는 std::expected의 개념과 사용법, 그리고 이전 버전과 비교하여 어떻게 개선되었는지 알아보겠습니다.

std::expected란 무엇인가요?

std::expected는 함수의 반환값으로 정상적인 결과오류 정보를 함께 전달할 수 있는 객체입니다. 이를 통해 예외를 사용하지 않고도 함수의 실패를 표현할 수 있으며, 코드의 가독성과 안정성을 높일 수 있습니다. std::expected는 템플릿 클래스로, 성공 시의 값 타입과 오류 타입을 지정할 수 있습니다.

이전 버전에서는 어떻게 했나요?

C++23 이전에는 함수의 오류 처리를 위해 주로 다음과 같은 방법을 사용했습니다.

1. 예외(Exception) 사용

int divide(int numerator, int denominator) {
    if (denominator == 0) {
        throw std::runtime_error("0으로 나눌 수 없습니다.");
    }
    return numerator / denominator;
}

int main() {
    try {
        int result = divide(10, 0);
    } catch (const std::exception& e) {
        std::cerr << "오류 발생: " << e.what() << '\n';
    }
    return 0;
}
  • 문제점:
    • 예외 처리는 런타임 오버헤드가 발생할 수 있습니다.
    • 예외 안전성(Exception Safety)을 보장하기 위해 코드가 복잡해질 수 있습니다.
    • 일부 시스템에서는 예외 사용이 제한적이거나 비권장됩니다.

2. std::optional 사용

std::optional<int> divide(int numerator, int denominator) {
    if (denominator == 0) {
        return std::nullopt;
    }
    return numerator / denominator;
}

int main() {
    auto result = divide(10, 0);
    if (result.has_value()) {
        std::cout << "결과: " << result.value() << '\n';
    } else {
        std::cout << "오류: 0으로 나눌 수 없습니다.\n";
    }
    return 0;
}
  • 문제점:
    • 오류에 대한 추가적인 정보 제공이 어렵습니다.
    • 오류 원인을 상세히 전달하기 위해서는 별도의 메커니즘이 필요합니다.

C++23의 std::expected를 사용한 개선

std::expected를 사용하면 함수의 반환값에 성공 시의 값실패 시의 오류 정보를 함께 담을 수 있습니다.

예제: std::expected 사용

#include <expected>
#include <string>

std::expected<int, std::string> divide(int numerator, int denominator) {
    if (denominator == 0) {
        return std::unexpected("0으로 나눌 수 없습니다.");
    }
    return numerator / denominator;
}

int main() {
    auto result = divide(10, 0);
    if (result) {
        std::cout << "결과: " << result.value() << '\n';
    } else {
        std::cout << "오류: " << result.error() << '\n';
    }
    return 0;
}
  • std::expected<int, std::string>은 성공 시 int 타입의 값, 실패 시 std::string 타입의 오류 메시지를 가집니다.
  • result가 참(truthy)이면 성공, 거짓(falsy)이면 실패로 간주합니다.
  • 오류 정보를 상세히 전달할 수 있습니다.

어떻게 좋아졌나요?

  • 오류 정보 전달 강화: 실패 시 상세한 오류 정보를 함께 반환할 수 있습니다.
  • 예외 사용 없이 오류 처리: 예외를 사용하지 않고도 함수의 실패를 표현할 수 있어 런타임 오버헤드를 줄일 수 있습니다.
  • 가독성 향상: 코드 흐름이 명확해지고, 오류 처리 로직이 간결해집니다.
  • 함수 합성의 용이성: 여러 함수를 조합할 때 std::expected를 활용하여 에러 전파를 쉽게 할 수 있습니다.

상세한 예제와 비교

1. 여러 단계의 함수 호출에서의 오류 전파

이전 방식

std::optional<int> readValue() {
    // 파일에서 값을 읽어옴
    return std::nullopt; // 오류 발생 시
}

std::optional<int> processValue(int value) {
    // 값 처리
    return std::nullopt; // 오류 발생 시
}

int main() {
    auto value = readValue();
    if (!value.has_value()) {
        std::cerr << "값을 읽어오는 데 실패했습니다.\n";
        return 1;
    }

    auto result = processValue(value.value());
    if (!result.has_value()) {
        std::cerr << "값을 처리하는 데 실패했습니다.\n";
        return 1;
    }

    std::cout << "처리 결과: " << result.value() << '\n';
    return 0;
}
  • 문제점:
    • 각 단계마다 오류 체크 코드가 필요합니다.
    • 오류 원인에 대한 상세 정보 전달이 어렵습니다.

C++23 방식

#include <expected>
#include <string>

std::expected<int, std::string> readValue() {
    // 파일에서 값을 읽어옴
    return std::unexpected("파일을 열 수 없습니다."); // 오류 발생 시
}

std::expected<int, std::string> processValue(int value) {
    // 값 처리
    return std::unexpected("값이 유효하지 않습니다."); // 오류 발생 시
}

int main() {
    auto value = readValue();
    if (!value) {
        std::cerr << "오류: " << value.error() << '\n';
        return 1;
    }

    auto result = processValue(value.value());
    if (!result) {
        std::cerr << "오류: " << result.error() << '\n';
        return 1;
    }

    std::cout << "처리 결과: " << result.value() << '\n';
    return 0;
}
  • 각 함수에서 오류 메시지를 명확하게 전달할 수 있습니다.
  • 오류 처리가 일관되고 간결합니다.

2. std::expected와 함수 합성

#include <expected>
#include <string>

std::expected<int, std::string> step1() {
    // ...
}

std::expected<int, std::string> step2(int value) {
    // ...
}

std::expected<int, std::string> step3(int value) {
    // ...
}

int main() {
    auto result = step1()
        .and_then(step2)
        .and_then(step3);

    if (result) {
        std::cout << "최종 결과: " << result.value() << '\n';
    } else {
        std::cerr << "오류: " << result.error() << '\n';
    }

    return 0;
}
  • and_then을 사용하여 함수를 체인처럼 연결할 수 있습니다.
  • 중간에 오류가 발생하면 자동으로 전파됩니다.

주의 사항

  • 오류 타입의 선택: 오류 정보를 표현하기 위해 적절한 타입을 선택해야 합니다. std::string, std::error_code 등을 사용할 수 있습니다.
  • std::expected와 예외의 조합: 예외를 사용하는 코드와 혼용할 때는 주의가 필요합니다. 일관된 오류 처리 방식을 선택하는 것이 좋습니다.
  • 헤더 파일 포함: std::expected를 사용하려면 <expected> 헤더를 포함해야 합니다.

요약

C++23의 std::expected는 함수의 반환값에 성공과 실패를 모두 표현할 수 있는 강력한 도구입니다. 이전에는 예외나 std::optional을 사용하여 오류 처리를 했지만, std::expected를 통해 타입 안전하고 상세한 오류 정보 전달이 가능해졌습니다. 이를 통해 코드의 안정성과 가독성이 향상되며, 함수 합성을 통한 에러 전파가 용이해집니다.

 

참고 자료:

 

반응형