일반적으로 Class Type Casting은 Downcasting이 안전한지 확인한 뒤 타입을 변환하는 작업을 의미합니다.
아래 코드에서 GetController() 는 AController* 를 반환하며, 해당 클래스 포인터(인스턴스)가 APlayerController 를 구현하고 있는지(동일 클래스이거나 자식 클래스인지)를 확인한 후, 조건에 맞을 경우 적절한 포인터를 반환합니다.
APlayerController* PlayerController = Cast<APlayerController>(GetController())
Class Upcasting은 Compile-Time에 바로 확인되지만, Downcasting은 그렇지 않습니다. 따라서 해당 Object(Class Instance)에 대한 타입 정보를 제공받거나, 별도의 구현이 필요합니다.
C++을 제외하고 현재 널리 사용되는 대부분의 언어는 Memory Managed 언어이며, Framework 차원에서 Garbage Collection과 Reflection을 지원합니다. 또한 이러한 언어들은 기본적으로 Run-Time Type Information(이하 RTTI)을 제공합니다.
따라서 대부분의 언어에서는 RTTI 사용 여부가 선택사항이 아니며 기본적으로 제공됩니다. 별도로 고려하거나 구현할 필요가 없습니다.
반면 C++에서 RTTI는 선택 사항입니다. 컴파일 타임에 사용 여부를 지정할 수 있습니다. 기능 자체는 유용하지만, RTTI는 추가 비용이 발생하기 때문에 선택적으로 제공됩니다.
Visual Studio 2022 C++에서 RTTI 옵션은 아래와 같습니다.

옵션을 활성화하면 다음과 같은 기능을 사용할 수 있습니다.
class Base {
virtual void foo() {} // RTTI 사용을 위해서는 반드시 가상 함수가 필요합니다.
};
class Derived : public Base {
};
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
// 캐스팅 성공
} else {
// 캐스팅 실패
}
Base* basePtr = new Derived();
std::cout << typeid(*basePtr).name() << std::endl; // Derived 클래스 이름 출력
C++에서 RTTI를 사용하면 virtual-function-table 기반의 추가 정보가 필요하며, 이는 오버헤드로 작용합니다. 또한 dynamic_cast 는 static_cast 에 비해 상대적으로 느립니다. 이러한 이유로 C++에서는 RTTI 사용 여부를 신중하게 선택하게 됩니다.
언리얼 엔진에서는 기본적으로 C++ RTTI가 비활성화되어 있으며, 특정 Module에서 필요할 경우 module.build.cs 에서 bUseRTTI=true; 로 활성화할 수 있습니다.
이제 Cast<APlayerController>(GetController()) 가 어떻게 구현되어 있는지 살펴보겠습니다.
UE_5.4/Engine/Source/Runtime/CoreUObject/Public/Templates/Casts.h
template <typename Type>
struct TCastFlags
{
static const EClassCastFlags Value = CASTCLASS_None;
};
// Dynamically cast an object type-safely.
template <typename To, typename From>
FORCEINLINE To* Cast(From* Src)
{
static_assert(sizeof(From) > 0 && sizeof(To) > 0, "Attempting to cast between incomplete types");
if (Src) // 'Mark A'
{
if constexpr (TIsIInterface<From>::Value) // Mark B
{
if (UObject* Obj = Src->_getUObject())
{
if constexpr (TIsIInterface<To>::Value)
{
return (To*)Obj->GetInterfaceAddress(To::UClassType::StaticClass());
}
else
{
if constexpr (std::is_same_v<To, UObject>)
{
return Obj;
}
else
{
if (Obj->IsA<To>())
{
return (To*)Obj;
}
}
}
}
}
else if constexpr (UE_USE_CAST_FLAGS && TCastFlags<To>::Value != CASTCLASS_None) // Mark C
{
if constexpr (std::is_base_of_v<To, From>)
{
return (To*)Src;
}
else
{
#if UE_ENABLE_UNRELATED_CAST_WARNINGS
UE_STATIC_ASSERT_WARN((std::is_base_of_v<From, To>), "Attempting to use Cast<> on types that are not related");
#endif
if (((const UObject*)Src)->GetClass()->HasAnyCastFlag(TCastFlags<To>::Value))
{
return (To*)Src;
}
}
}
else
{
static_assert(std::is_base_of_v<UObject, From>, "Attempting to use Cast<> on a type that is not a UObject or an Interface");
if constexpr (TIsIInterface<To>::Value)
{
return (To*)((UObject*)Src)->GetInterfaceAddress(To::UClassType::StaticClass());
}
else if constexpr (std::is_base_of_v<To, From>)
{
return Src;
}
else
{
#if UE_ENABLE_UNRELATED_CAST_WARNINGS
UE_STATIC_ASSERT_WARN((std::is_base_of_v<From, To>), "Attempting to use Cast<> on types that are not related");
#endif
if (((const UObject*)Src)->IsA<To>()) // Mark D
{
return (To*)Src;
}
}
}
}
return nullptr;
}
위 코드는 Cast(Src)가 실제로 동작하는 방식입니다. 이를 이해하기 위해서는 C++ template, UObject-UClass 관계, 상속 구조에서의 UClass Instance 관계, 그리고 Unreal Engine의 Interface 처리 방식에 대한 이해가 필요합니다.
여기서는 Interface 관련 내용은 간략히 넘기고, 다음 사항을 중심으로 이해하시면 충분합니다.
class UMyObject : public UObject 를 선언하면 Module 로딩 시 UClass Instance가 생성됩니다.UMyObjectA 와 UMyObjectB 는 각각 별도의 UClass Instance를 가지며, 타입은 동일하게 UClass 입니다.std::is_base_of_v<To, From> 는 To가 From의 부모 클래스인지 확인하는 C++17 표준 기능입니다.constexpr 는 Compile-Time에 코드 생성을 제어할 때 사용됩니다.if (Src)
캐스팅 대상 포인터가 nullptr인지 확인합니다. nullptr인 경우 그대로 nullptr을 반환합니다.
if constexpr (TIsIInterface<From>::Value)
대상이 Interface인지 확인한 후, Interface일 경우 별도의 방식으로 처리합니다.
else if constexpr (UE_USE_CAST_FLAGS && TCastFlags<To>::Value != CASTCLASS_None)
Editor 환경에서 활성화되며, CastFlags를 활용한 빠른 판별을 수행합니다.
if (((const UObject*)Src)->IsA<To>())
핵심 로직으로, UObject의 UClass 정보를 기반으로 상속 관계를 확인합니다.
Editor 환경에서는 부모 클래스를 따라 올라가며 비교하고,
Runtime 환경에서는 미리 구성된 BaseChain을 통해 빠르게 판별합니다.
UE5에서 Cast<ClassType>(...) 은 매우 빠르게 동작하도록 설계되어 있습니다.