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_id — void_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로 훨씬 읽기 쉽게 대체 가능(kind를requires std::integral<T>로).
면접 포인트
- SFINAE 의 의미("치환 실패는 오류가 아니다")와
enable_if/void_t/declval의 역할. - 태그 디스패치 vs
if constexprvs 오버로드 — 언제 무엇을(런타임 분기 없는 정적 다형성;cpp_internals/problem16CRTP 와 비교). - 런타임
if와if constexpr의 인스턴스화 차이, 그리고 "버려진 분기도 문법 검사는 받는다"는 함정.