dynamic_cast 변환

dynamic_cast 변환

상속 계층 위에서 클래스에 대한 포인터와 참조를 위로(up), 아래로(down), 그리고 옆으로(side) 안전하게 변환할 때 사용하는 캐스트예요. 다형성 타입을 다룰 때, "이 객체가 정말로 이 파생 타입일까?"를 런타임에 검사해 주는 변환이라는 점이 핵심이에요.

출처: cppreference

본문

문법

dynamic_cast< target-type >( expression )
  • target-type — 완전한(complete) 클래스 타입에 대한 포인터, 완전한 클래스 타입에 대한 참조, 또는 (선택적으로 cv 한정된) void에 대한 포인터.
  • expressiontarget-type이 참조 타입이면 완전한 클래스 타입의 glvalue(C++11 이전에는 lvalue), target-type이 포인터 타입이면 완전한 클래스 타입에 대한 포인터의 prvalue.

설명

편의상 설명할 때 "expression 또는 그 결과가 T에 대한 참조다"라고 하면, "그것이 T 타입의 glvalue다"라는 뜻으로 받아들이기로 해요. 이는 decltype(C++11 이후)에서 쓰는 관례와 같아요.

상수성(또는 휘발성)을 제거하는 변환을 하지 않는 한, dynamic_cast로 할 수 있는 변환은 다음과 같아요.

  1. expression의 타입이 정확히 target-type이거나 더 덜 cv 한정된 버전이라면, 결과는 target-type 타입을 가진 expression의 값이에요. 다시 말해 dynamic_cast로 const를 추가할 수 있어요. 이 변환은 암시적 변환과 static_cast로도 할 수 있어요.
  2. target-type이 "(가능하면 cv 한정된) Base에 대한 포인터"이고 expression의 타입이 "(가능하면 cv 한정된) Derived에 대한 포인터"여서 BaseDerived의 기본 클래스라면, 결과는
    • expression이 널 포인터 값이면 널 포인터 값,
    • 그렇지 않으면 expression이 가리키는 Derived 객체의 유일한 Base 하위 객체에 대한 포인터예요. 다시 말해 파생에서 기본으로 포인터를 위로 캐스팅(업캐스트)할 수 있어요. 이것도 암시적 변환과 static_cast로 가능해요.
  3. target-type이 "(가능하면 cv 한정된) Base에 대한 참조"이고 expression의 타입이 "(가능하면 cv 한정된) Derived"여서 BaseDerived의 기본 클래스라면, 결과는 expression이 가리키는 Derived 객체의 유일한 Base 하위 객체예요. 참조를 업캐스트하는 경우죠.
  4. expression다형성 타입의 널 포인터 값이라면, 결과는 target-type의 널 포인터 값이에요.
  5. 그 외의 경우, expression은 수명 안에 있거나 생성·파괴 중인 다형성 타입 객체에 대한 포인터 또는 참조여야 해요(그렇지 않으면 동작이 정의되지 않아요). 그리고
    • a) target-type이 "(가능하면 cv 한정된) void에 대한 포인터"라면, 결과는 expression이 가리키는 가장 파생된 객체에 대한 포인터예요.
    • b) 그 외에는 expression이 가리키는/참조하는 객체를 target-type이 가리키는/참조하는 타입 Target으로 변환할 수 있는지 런타임 검사를 해요.
      • i) expression이 가리키는/참조하는 가장 파생된 객체 안에서, expressionTarget 객체의 public 기본 클래스 하위 객체를 가리키고/참조하며, 그 하위 객체에서 파생된 Target 타입 객체가 오직 하나뿐이라면, 결과는 그 Target 객체를 가리켜요/참조해요. 다시 말해 기본에서 파생으로 포인터/참조를 아래로 캐스팅(다운캐스트)할 수 있어요.
      • ii) 그렇지 않고, expression가장 파생된 객체의 public 기본 클래스 하위 객체를 가리키고/참조하며, 가장 파생된 객체의 타입이 Target 타입의 모호하지 않은 public 기본 클래스를 가진다면, 결과는 가장 파생된 객체의 Target 하위 객체를 가리켜요/참조해요. 다시 말해 같은 기본에서 파생된 두 타입 사이를 옆으로 캐스팅(크로스캐스트/side-cast)할 수 있어요.
      • iii) 그렇지 않으면 런타임 검사에 실패해요.
        • target-type이 포인터 타입이면 결과는 target-type널 포인터 값이에요.
        • target-type이 참조 타입이면, std::bad_cast 타입의 핸들러에 맞는 예외가 던져져요.

dynamic_cast가 생성자나 소멸자에서 (직접/간접적으로) 쓰일 때, expression이 현재 생성/파괴 중인 객체를 가리키면 그 객체는 가장 파생된 객체로 간주돼요. 만약 target-type이 생성자/소멸자 자신의 클래스나 그 기본 클래스 중 하나에 대한 포인터/참조가 아니라면 동작은 정의되지 않아요.

다른 캐스트 표현식과 마찬가지로 결과는:

  • target-type이 참조 타입이면 lvalue(C++11 이전)
  • target-type이 포인터 타입이면 rvalue(C++11 이전)
  • C++11 이후에는:
    • target-type이 lvalue 참조 타입이면 lvalue(expression은 lvalue여야 함)
    • target-type이 rvalue 참조 타입이면 xvalue(expression은 lvalue 또는 rvalue(C++17 이전) / glvalue여야 하며 prvalue는 실체화됨(C++17 이후) — 완전한 클래스 타입)
    • target-type이 포인터 타입이면 prvalue

Notes

다운캐스트는 static_cast로도 할 수 있어요. 그러면 런타임 검사 비용을 피할 수 있지만, 프로그램이 (다른 논리로) expression이 가리키는 객체가 확실히 Derived임을 보장할 수 있을 때만 안전해요.

dynamic_cast의 일부 형태는 **런타임 타입 식별(RTTI)**에 의존해요. 즉 컴파일된 프로그램의 각 다형성 클래스에 대한 정보에 의존하죠. 컴파일러는 보통 이 정보의 포함 여부를 끄는 옵션을 제공해요.

기능 테스트 매크로 표준 기능
__cpp_constexpr 201907L (C++20) 상수 표현식에서의 dynamic_cast

키워드

dynamic_cast

예제

여기서 확인할 것: 런타임에 검사되는 다운캐스트와 사이드캐스트가 각각 성공하는지, 그리고 실제 객체가 아닌 대상을 다운캐스트하려 하면 널 포인터가 나오는지 보는 코드예요.

#include <iostream>

struct V
{
    virtual void f() {} // must be polymorphic to use runtime-checked dynamic_cast
};

struct A : virtual V {};

struct B : virtual V
{
    B(V* v, A* a)
    {
        // casts during construction (see the call in the constructor of D below)
        dynamic_cast<B*>(v); // well-defined: v of type V*, V base of B, results in B*
        dynamic_cast<B*>(a); // undefined behavior: a has type A*, A not a base of B
    }
};

struct D : A, B
{
    D() : B(static_cast<A*>(this), this) {}
};

struct Base
{
    virtual ~Base() {}
};

struct Derived : Base
{
    virtual void name() {}
};

int main()
{
    D d; // the most derived object
    A& a = d; // upcast, dynamic_cast may be used, but unnecessary
    
    [[maybe_unused]]
    D& new_d = dynamic_cast<D&>(a); // downcast
    [[maybe_unused]]
    B& new_b = dynamic_cast<B&>(a); // sidecast
    
    Base* b1 = new Base;
    if (Derived* d = dynamic_cast<Derived*>(b1); d != nullptr)
    {
        std::cout << "downcast from b1 to d successful\n";
        d->name(); // safe to call
    }
    
    Base* b2 = new Derived;
    if (Derived* d = dynamic_cast<Derived*>(b2); d != nullptr)
    {
        std::cout << "downcast from b2 to d successful\n";
        d->name(); // safe to call
    }
    
    delete b1;
    delete b2;
}

출력:

downcast from b2 to d successful

여기서 주의할 점은 b1은 실제로 Base 객체니까 다운캐스트가 실패해서 널 포인터가 나오고, b2는 실제 Derived 객체라서 성공한다는 부분이에요.

결함 보고 (Defect reports)

다음의 동작 변경 결함 보고는 이전에 발표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 1269 C++11 target-type이 rvalue 참조 타입일 때 xvalue 표현식에 대해 런타임 검사가 수행되지 않았음 수행됨
CWG 2861 C++98 expression이 타입이 접근 불가능한 객체를 가리킬/참조할 수 있었음 이 경우 동작은 정의되지 않음

참고 문헌

  • C++23 표준 (ISO/IEC 14882:2024): 7.6.1.7 Dynamic cast [expr.dynamic.cast]
  • C++20 표준 (ISO/IEC 14882:2020): 7.6.1.6 Dynamic cast [expr.dynamic.cast]
  • C++17 표준 (ISO/IEC 14882:2017): 8.2.7 Dynamic cast [expr.dynamic.cast]
  • C++14 표준 (ISO/IEC 14882:2014): 5.2.7 Dynamic cast [expr.dynamic.cast]
  • C++11 표준 (ISO/IEC 14882:2011): 5.2.7 Dynamic cast [expr.dynamic.cast]
  • C++98 표준 (ISO/IEC 14882:1998): 5.2.7 Dynamic cast [expr.dynamic.cast]
  • C++03 표준 (ISO/IEC 14882:2003): 5.2.7 Dynamic cast [expr.dynamic.cast]

더 알아보기 (Learn more)