← 문제로

18. 템플릿 인스턴스화와 컴파일타임 디스패치(SFINAE·태그 디스패치·if constexpr)

난이도 최상
내 리뷰 · C++
해설 · C++

해설 — 템플릿 인스턴스화와 컴파일타임 디스패치(SFINAE·태그 디스패치·if constexpr)

난이도: 최상

요약

enable_if/void_t치환 실패를 오류가 아니라 후보 제거(SFINAE) 로 바꿔 오버로드를 타입 특성으로 가른다. 태그 디스패치는 특성을 태그 타입으로 바꿔 오버로드 해석에 태워 컴파일타임에 분기한다. if constexpr 는 조건이 거짓인 분기를 인스턴스화에서 제외(discarded statement) 해, 런타임 if 와 달리 "그 분기가 이 T 에선 컴파일조차 안 되어도" 된다.

예측 출력

integral
floating
1 0
obj pod
41 82 -1

핵심 개념 · 단계별

1) kind() 의 SFINAE 오버로드 분리

  • kind(42): T=int. 후보 A(is_integral) 는 enable_if_t<true,int>=int → 유효, 후보 B(is_floating_point) 는 enable_if_t<false,int>멤버 type 없음 → 치환 실패. 실패는 하드 에러가 아니라 후보 집합에서 조용히 제거(SFINAE). 남은 A 선택 → "integral".
  • kind(3.14): T=double. A 제거, B 선택 → "floating".
  • 두 함수 시그니처가 반환형·인자만으론 구별 안 되므로, enable_if비타입 템플릿 인자 기본값(int = 0) 자리에 넣어 SFINAE 를 건다(반환형에 걸어도 됨).

2) has_idvoid_t 검출 관용구

  • 1차 템플릿 has_id<T, class=void>false_type. 부분특수화 has_id<T, void_t<decltype(declval<T>().id())>> 는, T.id()유효하면 decltype(...) 이 성립 → void_t<...>=void부분특수화가 더 특수해서 선택 → true_type.
  • Player.id() 있음 → 특수화 매칭 → true(1). int.id() 없음 → decltype 치환 실패 → 특수화 탈락 → 1차 템플릿 → false(0).
  • 핵심은 declval<T>()(객체 없이 T 표현식을 만드는 도구)와 표현식 유효성 검사를 SFINAE 로 하는 것.

3) describe() 태그 디스패치

  • std::conditional_t<is_class<T>, obj_tag, pod_tag> 로 컴파일타임에 태그 타입을 고르고, dispatch_impl(x, tag{}) 호출이 오버로드 해석으로 pod_tag/obj_tag 버전을 고른다.
  • describe(Player{}): Player 는 클래스 → obj_tag"obj". describe(5): int 는 비클래스 → pod_tag"pod". → obj pod.
  • if 문 없이 타입으로 분기하므로, 각 분기 본문이 서로 다른 타입에서만 유효해도 안전(선택된 것만 인스턴스화).

4) sizeInfo()if constexpr

  • sizeInfo(int{}): is_integral_v<int> 참 → 첫 분기, sizeof(int)=4*10+1 = 41. 나머지 분기는 버려져(discarded) 인스턴스화되지 않음.
  • sizeInfo(double{}): 둘째 분기, sizeof(double)=8*10+2 = 82.
  • sizeInfo(Player{}): 두 특성 모두 거짓 → else-1.
  • 런타임 if 였다면 모든 분기가 컴파일돼야 하므로, 예컨대 정수 전용 연산을 클래스 타입에도 컴파일하려다 에러가 난다. if constexpr 는 거짓 분기를 인스턴스화 제외해 이를 피한다(단, 버려진 분기도 문법·비의존 검사는 받는다).

흔한 오해·함정

  • SFINAE 는 "표현식/타입 치환 단계"의 실패만 무시한다. 함수 본문 안에서 나는 오류는 하드 에러(SFINAE 대상 아님) — 그래서 검출은 시그니처/decltype 로 한다.
  • 두 SFINAE 오버로드의 조건이 겹치면 모호성(ambiguous) — 조건은 상호배타적이어야 한다(정수/부동은 배타적이라 OK).
  • if constexpr 의 버려진 분기도 "템플릿 매개변수에 의존하지 않는" 부분은 검사된다. 완전히 무효한 코드를 숨기려면 분기가 T 에 의존하게 해야 한다.
  • C++20 이면 이 대부분을 concepts/requires 로 훨씬 읽기 쉽게 대체 가능(kindrequires std::integral<T> 로).

면접 포인트

  • SFINAE 의 의미("치환 실패는 오류가 아니다")와 enable_if/void_t/declval 의 역할.
  • 태그 디스패치 vs if constexpr vs 오버로드 — 언제 무엇을(런타임 분기 없는 정적 다형성; cpp_internals/problem16 CRTP 와 비교).
  • 런타임 ifif constexpr 의 인스턴스화 차이, 그리고 "버려진 분기도 문법 검사는 받는다"는 함정.