[C++23 새기능 소개] std::optional의 모나딕 연산(transform, transform_or, and_then, or_else)

C++23에서는 std::optional 타입에 모나딕(Monadic) 연산들을 추가하여 함수형 스타일의 체이닝(chaining)과 에러 처리를 더욱 편리하게 만들어주는 새로운 기능들이 도입되었습니다. 이번 글에서는 std::optional의 모나딕 연산(transform, transform_or, and_then, or_else)과 같은 기능의 개념과 사용법, 그리고 이전 버전과 비교하여 어떻게 개선되었는지 알아보겠습니다.

std::optional의 모나딕 연산이란 무엇인가요?

C++17에서 도입된 std::optional은 값이 존재할 수도 있고 존재하지 않을 수도 있는 상황을 안전하고 명확하게 처리하기 위한 타입입니다. 하지만 C++17에서는 std::optional에 대한 체이닝이나 함수 적용이 불편했으며, 값이 없는 경우를 처리하기 위해 조건문이나 별도의 분기 로직을 자주 사용해야 했습니다.

 

C++23에서는 std::optional에 다양한 모나딕 연산(transform, transform_or, and_then, or_else)을 추가하여, 함수형 프로그래밍 스타일로 값을 처리할 수 있게 되었습니다. 이를 통해 조건부 값에 대한 처리 로직을 간결하고 직관적으로 표현할 수 있습니다.

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

C++17/20에서는 std::optional의 값을 변환하거나 다른 로직과 결합하기 위해 다음과 같은 패턴을 사용했습니다.

예제: 기존 방식 (C++17/20)

#include <optional>
#include <iostream>
#include <string>

std::optional<int> parseNumber(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    std::string input = "42";
    std::optional<int> number = parseNumber(input);

    // 값이 있는지 확인하고, 있으면 변환, 없으면 기본값 사용
    int result;
    if (number.has_value()) {
        result = number.value() * 2;
    } else {
        result = -1; // 기본값
    }

    std::cout << result << '\n'; // 84 출력
    return 0;
}
  • 문제점: 값 변환 시마다 if (has_value()) 체크, 값이 없을 때의 처리(기본값 등)를 위한 별도 로직 필요
  • 모나딕 스타일로 간결하게 표현하기 어렵고, 로직 중복 및 가독성 저하

C++23의 모나딕 연산을 사용한 개선

주요 모나딕 연산

  • transform: 값이 존재하면 함수를 적용하여 변환, 값이 없으면 그대로 std::nullopt
  • transform_or: 값이 존재하면 함수를 적용, 값이 없으면 지정한 대체값 반환
  • and_then: 값이 존재하면 함수를 호출하고 그 결과(std::optional<T>) 반환, 없으면 std::nullopt
  • or_else: 값이 없으면 함수를 호출해 대체 std::optional<T>를 반환, 있으면 원래 값 유지

예제: transform와 transform_or 사용

#include <optional>
#include <iostream>
#include <string>

std::optional<int> parseNumber(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    std::string input = "42";
    std::optional<int> number = parseNumber(input);

    // transform: 값이 있으면 2배로 변환, 없으면 nullopt 유지
    auto doubled = number.transform([](int x) { return x * 2; });

    // transform_or: 값이 있으면 2배, 없으면 -1 사용
    int result = number.transform_or([](int x) { return x * 2; }, -1);

    std::cout << (doubled.has_value() ? std::to_string(*doubled) : "nullopt") << '\n'; // 84
    std::cout << result << '\n'; // 84 (값 있었으므로 2배)
    return 0;
}

예제: and_then와 or_else 사용

#include <optional>
#include <iostream>
#include <string>

std::optional<int> parseNumber(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
}

std::optional<int> half(int x) {
    return (x % 2 == 0) ? std::optional<int>{x / 2} : std::nullopt;
}

int main() {
    std::string input = "42";
    std::optional<int> number = parseNumber(input);

    // and_then: 값이 있으면 half 함수로 이어서 처리, 없으면 nullopt
    auto halved = number.and_then(half); 
    // halved: 21 (42의 절반)

    // or_else: 값이 없으면 다른 함수로 대체, 있으면 그대로
    std::optional<int> fallback = halved.or_else([]() { return std::optional<int>(0); });
    // halved에 값이 있으므로 fallback은 halved와 동일: 21

    std::cout << *fallback << '\n'; // 21
    return 0;
}

어떻게 좋아졌나요?

  • 코드 가독성 향상: if/else 로직 없이도 값 존재 여부에 따른 변환 및 대체 로직을 간결하게 표현
  • 유연성 증가: 체인 형태로 연속된 변환 및 대체 로직을 함수형 스타일로 구성 가능
  • 중복 로직 감소: 값이 존재할 때/없을 때 로직을 명확하고 일관되게 처리할 수 있어 유지보수성 향상
  • 표준화된 모나딕 스타일: 기존에 수동으로 구현하던 패턴을 표준 라이브러리에서 제공

상세한 예제와 비교

기존 방식 vs 모나딕 방식

기존 방식(C++20 이전)

auto parsed = parseNumber(input);
int result;

if (parsed.has_value()) {
    auto halfVal = half(parsed.value());
    if (halfVal.has_value()) {
        result = halfVal.value();
    } else {
        result = 0;
    }
} else {
    result = -1;
}

C++23 방식(모나딕 연산)

int result = parseNumber(input)
    .and_then(half)
    .or_else([]() { return std::optional<int>(0); })
    .transform_or([](int x){ return x; }, -1);
  • 한 줄에 모든 로직을 담아 직관적으로 표현 가능

주의 사항

  • 함수 인자의 형태: transform, and_then, or_else에 전달하는 람다는 적절한 타입 시그니처를 가져야 합니다(예: and_then은 std::optional<T>를 반환하는 함수).
  • 성능 고려: 함수형 스타일로 체이닝하는 것이 가독성은 높이지만, 성능상 오버헤드가 있을 수 있으므로, 성능 민감한 부분에서는 신중한 판단 필요
  • 컴파일러 지원: C++23 기능이므로, 해당 기능을 지원하는 컴파일러 및 표준 라이브러리가 필요

요약

C++23의 std::optional 모나딕 연산(transform, transform_or, and_then, or_else)을 통해 조건부 값 처리 로직을 함수형 스타일로 간결하게 표현할 수 있게 되었습니다. 이전에는 if/else와 value 체크가 난무하던 코드를 단순한 체이닝 형태로 재구성하여 가독성과 유지보수성을 높일 수 있으며, 표준화된 방식으로 모나딕 패턴을 지원하는 점에서 개발 생산성이 크게 향상됩니다.

 

참고 자료:

반응형