[C++23 새기능 소개] std::unreachable() 함수

C++23에서는 컴파일러에게 특정 코드 경로가 절대 도달하지 않을 것임을 알리는 새로운 함수인 std::unreachable()가 도입되었습니다. 이번 글에서는 std::unreachable()의 개념과 사용법, 그리고 이전 버전과 비교하여 어떻게 개선되었는지 알아보겠습니다.

 

std::unreachable()란 무엇인가요?

std::unreachable()는 코드 상에서 절대 실행될 수 없는 경로임을 컴파일러에 알려주는 함수입니다. 이는 정의되지 않은 동작(Undefined Behavior)을 발생시키며, 컴파일러가 이 정보를 활용하여 해당 코드 경로를 제거하거나 최적화할 수 있게 합니다. 예를 들어, switch 문의 default 분기가 절대 발생하지 않는 경우나 특정 조건이 항상 참이라 이외의 경로로는 진행되지 않을 때, std::unreachable()를 통해 해당 경로가 비현실적임을 명시할 수 있습니다.

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

C++23 이전에는 도달 불가능한 경로를 표시하기 위해 다음과 같은 우회적인 방법을 사용했습니다.

1. assert(false) 또는 throw 사용

switch (value) {
    case 1: /* ... */ break;
    case 2: /* ... */ break;
    default:
        // 여기에 도달하지 않는다고 가정하지만, 컴파일러는 확신할 수 없음
        assert(false); 
        // 또는
        // throw std::runtime_error("unreachable");
}
  • 문제점:
    • assert(false)는 런타임 검사이며, 최적화에 직접 기여하지 않습니다.
    • 예외를 던지는 것은 런타임 비용이 발생하며, 컴파일러 최적화를 위해 "절대 도달하지 않는다"는 의미를 충분히 전달하지 못합니다.
    • 컴파일러별 확장(예: GCC의 __builtin_unreachable(), MSVC의 _assume(false))를 사용할 수도 있었지만, 이는 이식성 문제를 낳습니다.

2. 컴파일러별 확장 함수 사용

#ifdef __GNUC__
#define UNREACHABLE() __builtin_unreachable()
#elif defined(_MSC_VER)
#define UNREACHABLE() __assume(0)
#else
#define UNREACHABLE() do { assert(false); } while(0)
#endif
  • 문제점:
    • 비표준적인 방법으로, 이식성과 유지보수성 저하
    • 코드 가독성 및 명확성 저하

C++23의 std::unreachable()를 사용한 개선

이제 표준 라이브러리에서 제공하는 std::unreachable()를 사용할 수 있으므로, 컴파일러별 조건부 코드나 비표준 함수에 의존할 필요가 없습니다.

예제: std::unreachable() 사용

#include <utility> // std::unreachable
#include <iostream>

int processValue(int value) {
    switch (value) {
        case 1: return 10;
        case 2: return 20;
        default:
            std::unreachable(); 
            // 이 지점은 절대 도달하지 않는다고 가정
            // 컴파일러는 여기서 UB 발생 가능성을 인지하여 최적화
    }
}

int main() {
    std::cout << processValue(1) << '\n'; // 10 출력
    // processValue(99)는 UB 발생 (실제로는 호출하지 않는 상황 가정)
    return 0;
}
  • std::unreachable()는 이 부분이 절대 실행되지 않음을 컴파일러에게 알리고, 정의되지 않은 동작을 발생시킵니다.
  • 컴파일러는 이 정보를 활용하여 불필요한 분기 제거 등 최적화를 수행할 수 있습니다.

어떻게 좋아졌나요?

  • 표준화된 방법: 이전에는 컴파일러별 확장 함수에 의존했지만, 이제 표준 함수 사용으로 이식성과 일관성 확보
  • 직관적 코드: std::unreachable()를 보면 이 코드 경로가 도달 불가능함을 명확히 알 수 있어 가독성과 유지보수성 향상
  • 최적화 기회 제공: 컴파일러가 더욱 강력한 최적화를 적용할 수 있으므로, 성능 개선에 도움이 될 수 있습니다.

상세한 예제와 비교

1. 절대 실행되지 않는 함수에 표시

int getImpossibleValue() {
    // 어떤 이유로 여기가 절대 호출되지 않는다는 확신이 있음
    std::unreachable();
    // 이후 코드는 UB, 삭제해도 됨
}
  • 이전에는 assert(false)나 예외 던지기 등을 했지만, 이제는 명확히 이 경로가 불가능함을 표시

2. 코드 정리 및 최적화

int compute(int x) {
    if (x > 0) {
        return x * 2;
    } else if (x == 0) {
        return 0;
    } else {
        std::unreachable();
    }
}
  • x가 음수일 수 없는 상황을 가정하고, 이를 std::unreachable()로 명확히 표현하면, 컴파일러는 음수에 대한 분기 제거 등 최적화 가능

주의 사항

  • 정의되지 않은 동작: std::unreachable()는 해당 위치에 실행이 도달하면 UB를 발생시킵니다. 개발자가 절대 도달하지 않는다는 보장을 실제로 해야 합니다.
  • 디버깅 난이도: 잘못된 가정으로 std::unreachable()를 사용하는 경우 디버깅이 어려울 수 있습니다. 조건이 항상 만족된다는 확신이 있는 경우에만 사용하세요.
  • 컴파일러 지원: C++23 기능이므로, 이를 지원하는 컴파일러와 표준 라이브러리가 필요합니다.

요약

C++23의 std::unreachable()는 코드 상에서 결코 도달할 수 없는 경로임을 표준화된 방식으로 명시하는 기능을 제공합니다. 이전에는 컴파일러별 확장이나 우회적인 방법에 의존했지만, 이제는 표준 라이브러리 함수 사용으로 이식성, 가독성, 유지보수성, 최적화를 모두 개선할 수 있습니다. 단, 이 함수 호출 부위가 정말 도달 불가능하다는 보장이 필요하다는 점에 유의해야 합니다.

 

 

참고 자료:

반응형