16. CRTP와 정적 다형성(static polymorphism)
난이도 중내 리뷰 · C++
해설 · C++
해설 — CRTP와 정적 다형성(static polymorphism)
난이도: 중상
요약
CRTP 는 기반 클래스 템플릿이 자기 파생 타입을 템플릿 인자로 받아(struct Circle : Shape<Circle>) static_cast<Derived*>(this) 로 파생의 구현을 호출하는 패턴이다. 디스패치가 컴파일 타임에 정적으로 결정돼 vtable·간접 호출 없이 다형적 인터페이스를 얻는다.
예측 출력
Circle r=2.0
Square s=3.0
sizeof(Circle)=8 sizeof(Square)=8
areas: 12.57 9.00
핵심 개념
- CRTP:
Base<Derived>는 인스턴스화 시점에Derived의 완전한 정의를 알고 있으므로static_cast<const Derived*>(this)->drawImpl()이 정확한 파생 메서드로 정적 바인딩된다. - 정적 다형성:
render(const Shape<D>&)는 가상 디스패치가 아니라D마다 별도 함수로 인스턴스화된다. "어떤 draw 를 부를지"가 컴파일 타임에 고정. - 빈 기반 클래스 최적화(EBO):
Shape<Circle>은 멤버가 없는 빈 클래스. 표준은 빈 기반 서브오브젝트가 공간을 차지하지 않도록 허용해,Circle크기는double r만큼인 8바이트.
단계별 내부 동작
render(c)→render<Circle>인스턴스화 →sh.draw()는Shape<Circle>::draw→static_cast<const Circle*>(this)->drawImpl()→Circle::drawImpl직접 호출(인라인 가능).render(q)→render<Square>인스턴스화 → 같은 방식으로Square::drawImpl.sizeof:Shape<Circle>빈 기반 +double(8) → EBO 로 8.Square도 동일.area:3.14159*4 = 12.56636 → 12.57(반올림),3*3 = 9.00.
흔한 오해·함정
- CRTP 객체들을 한 컨테이너에 담을 수 없다:
Shape<Circle>와Shape<Square>는 서로 다른 타입이라std::vector<Shape*>같은 단일 기반 포인터로 묶을 수 없다. 런타임에 타입이 섞여 결정되는 이종 컬렉션이 필요하면 가상 함수가 맞다. - 기반에서 파생 멤버 접근 시점:
Base<Derived>의 멤버 함수 본문은 인스턴스화 지연 덕에Derived가 그 시점엔 완전해 동작하지만, 기반의 데이터 멤버 초기화에서 파생을 건드리면 미완성 타입 문제가 생긴다. static_cast전제:this가 실제로Derived의 서브오브젝트라는 가정. 다른 타입이Shape<Circle>을 상속하면 UB.
면접 포인트
- 정적(컴파일 타임) vs 동적(vtable) 다형성의 비용/제약: CRTP 는 간접호출·vtable 없음(인라인·최적화 유리)이지만 이종 런타임 컬렉션 불가, 코드 팽창(타입마다 인스턴스화), 컴파일 시간 증가.
- EBO 가 왜
sizeof를 8로 만드는가, 그리고[[no_unique_address]]와의 관계. - 실무 적용: 핫패스 인터페이스(수학 타입, 정책 클래스, 믹스인)에 CRTP, 플러그인/스크립트 객체처럼 런타임 다형이 필요하면 가상 함수.