← 문제로

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바이트.

단계별 내부 동작

  1. render(c)render<Circle> 인스턴스화 → sh.draw()Shape<Circle>::drawstatic_cast<const Circle*>(this)->drawImpl()Circle::drawImpl 직접 호출(인라인 가능).
  2. render(q)render<Square> 인스턴스화 → 같은 방식으로 Square::drawImpl.
  3. sizeof: Shape<Circle> 빈 기반 + double(8) → EBO 로 8. Square 도 동일.
  4. 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, 플러그인/스크립트 객체처럼 런타임 다형이 필요하면 가상 함수.