보통 Class Type Casting 은 Downcasting 이 안전한지 확인하고 타입을 변환하는 작업이다.
아래 코드에서 GetController() 는 AController* 를 Return 하며 해당 class pointer(instance) 가 APlayerController 를 구현 하고 있는지(동일하거나 자식이거나) 확인하고 맞다면 적절한 pointer 를 반환한다.
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++ 을 사용한다는건 이 마저도 아깝기 때문일 것이다.
언리얼엔진 에서는 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) 가 실제로 작동되는 Code 이다. 위 코드를 이해하기 위해서는 약간의 C++ template 지식과 UObject-UClass 의 관계, 상속 구조 상에서의 UClass Instance 끼리의 관계를 알고 있어야 한다. 또, UnrealEngine 에서 Interface 를 다루는 방법도 어느 정도 알고 있어야 한다.
UObject-UClass 의 관계는 다른 포스팅에서 추가로 살펴보기로 하고, 여기서는 Interface 와의 형변환은 살짝 미뤄둔채 다음 내용을 숙지 하자.
class UMyObject : public UObject 를 선언하면 Module이 로딩될떄 UClass-Instance 가 생성된다.UMyObjectA 와 UMyObjectB 모두 UClass Type은 UClass 로 동일하고 Instance 가 각각 만들어 진다.std::is_base_of_v<To, From> 는 To 가 From 의 부모 Class 인지 확인하는 C++17 이후의 표준 기능이다.constexpr 은 상수 취급할 수 있는 곳에서 compile-time 에서 확인후 Code 자체를 생성/제거 할때 주로 사용된다. C++ 일반 const 와는 다르다. 일반 const 는 읽기 전용이라는 의미에 가깝다.그럼 주요 부분을 살펴보자
if (Src)
Casting 대상이 되는 object 의 pointer 가 nullptr 인지 확인한다. nullptr 이면 추가 동작 없이 nullptr 을 반환하면 된다.
if constexpr (TIsIInterface<From>::Value)
Casting 대상이 되는 object 가 TInterface 인지 확인한다음, Interface 이면 내부 동작을 수행한다. Interface 라도 내부적으로는 해당 Interface가 가르키는 Object 의 Class 를 타고 올라가면서 대상 Interface 를 구현하고 있는지 확인하는 것 외에는 크게 다르지 않다. 다음 기회에 자세히 다뤄 보기로 한다.
else if constexpr (UE_USE_CAST_FLAGS && TCastFlags<To>::Value != CASTCLASS_None)
아주 재미있는 부분이다. 먼저 UE_USE_CAST_FLAGS 를 살펴보자
#define UE_USE_CAST_FLAGS (USTRUCT_FAST_ISCHILDOF_IMPL != USTRUCT_ISCHILDOF_STRUCTARRAY)
#if UE_EDITOR
// On editor, we use the outerwalk implementation because BP reinstancing and hot reload
// mess up the struct array
#define USTRUCT_FAST_ISCHILDOF_IMPL USTRUCT_ISCHILDOF_OUTERWALK
#else
#define USTRUCT_FAST_ISCHILDOF_IMPL USTRUCT_ISCHILDOF_STRUCTARRAY
#endif
즉, Editor 에서 UE_USE_CAST_FLAGS 가 True 이고 TCastFlags::Value 를 확인한다.
TCastFlags::Value 는
UE_5.4/Engine/Source/Runtime/CoreUObject/Public/UObject/ObjectMacros.h
enum EClassCastFlags : uint64
{
CASTCLASS_None = 0x0000000000000000,
CASTCLASS_UField = 0x0000000000000001,
CASTCLASS_FInt8Property = 0x0000000000000002,
CASTCLASS_UEnum = 0x0000000000000004,
CASTCLASS_UStruct = 0x0000000000000008,
CASTCLASS_UScriptStruct = 0x0000000000000010,
CASTCLASS_UClass = 0x0000000000000020,
...
저렇게 생겼고, 해당 Class Type 에 대해 TCastFlags::Value 를 지정 하고 있다. 예를들어 TCastFlags<UClass>::Value 는 CASTCLASS_None 이 아니며 내부 코드가 작동한다.
우선 Casting 대상이 명시적으로 부모 ClassType 으로 형변환 하는지 확인하고, 아니라면 UClass 에서 ((const UObject*)Src)->GetClass()->HasAnyCastFlag(TCastFlags<To>::Value)) 을 확인한다.
UE_5.4/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp
FORCEINLINE bool HasAnyCastFlag(EClassCastFlags FlagToCheck) const
{
return (ClassCastFlags&FlagToCheck) != 0;
}
ClassCastFlags 는 형변환 가능 주요 타입의 비트 조합이며 uint64 이므로 최대 64개까지 지원됨을 알 수 있다.
/** Cast flags used to accelerate dynamic_cast<T*> on objects of this type for common T */
EClassCastFlags ClassCastFlags;
if (((const UObject*)Src)->IsA<To>())
이제 핵심적인 부분을 살펴 보자.
UE_5.4/Engine/Source/Runtime/CoreUObject/Public/UObject/UObjectBaseUtility.h
template<class T>
bool IsA() const
{
return IsA(T::StaticClass());
}
여기서 IsA 는 Editor 일떄와 아닐때 다르게 동작하는데, 우선 Editor 상에서는 UClass 기반으로 SuperClass(부모 Class)를 타고 올라가면서 일치 하는지 확인한다.
In Editor
bool UStruct::IsChildOf( const UStruct* SomeBase ) const
{
// If you're looking at this check it is due to calling IsChildOf with a this nullptr. *MAKE* sure you do not call this function
// with a this nullptr. It is undefined behavior, and some compilers, clang13 have started to optimize out this == nullptr checks.
check(this);
if (SomeBase == nullptr)
{
return false;
}
bool bOldResult = false;
for ( const UStruct* TempStruct=this; TempStruct; TempStruct=TempStruct->GetSuperStruct() )
{
if ( TempStruct == SomeBase )
{
bOldResult = true;
break;
}
}
#if USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_STRUCTARRAY
const bool bNewResult = IsChildOfUsingStructArray(*SomeBase);
#endif
#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK
ensureMsgf(bOldResult == bNewResult, TEXT("New cast code failed"));
#endif
return bOldResult;
}
Editor 가 아닐떄는 미리 BaseChain 을 구해두고, 부모Class 가 해당 Chain에 있는지 확인한다. 다중상속을 허용하지 않으므로 ParentClass 가 해당 ChildClass 의 부모라면 ChildClass 의 Chain 상에 정해진 Depth 에 놓이게 된다.
Not In Editor
class FStructBaseChain
{
protected:
COREUOBJECT_API FStructBaseChain();
COREUOBJECT_API ~FStructBaseChain();
// Non-copyable
FStructBaseChain(const FStructBaseChain&) = delete;
FStructBaseChain& operator=(const FStructBaseChain&) = delete;
COREUOBJECT_API void ReinitializeBaseChainArray();
private:
FStructBaseChain** StructBaseChainArray;
int32 NumStructBasesInChainMinusOne;
friend class UStruct;
};
void FStructBaseChain::ReinitializeBaseChainArray()
{
delete [] StructBaseChainArray;
int32 Depth = 0;
for (UStruct* Ptr = static_cast<UStruct*>(this); Ptr; Ptr = Ptr->GetSuperStruct())
{
++Depth;
}
FStructBaseChain** Bases = new FStructBaseChain*[Depth];
{
FStructBaseChain** Base = Bases + Depth;
for (UStruct* Ptr = static_cast<UStruct*>(this); Ptr; Ptr = Ptr->GetSuperStruct())
{
*--Base = Ptr;
}
}
StructBaseChainArray = Bases;
NumStructBasesInChainMinusOne = Depth - 1;
}
따라서 In Editor 가 아닐때 결국 다음 두줄로 확인이 끝난다
FORCEINLINE bool IsChildOfUsingStructArray(const FStructBaseChain& Parent) const
{
int32 NumParentStructBasesInChainMinusOne = Parent.NumStructBasesInChainMinusOne;
return NumParentStructBasesInChainMinusOne <= NumStructBasesInChainMinusOne && StructBaseChainArray[NumParentStructBasesInChainMinusOne] == &Parent;
}
UE5 에서 Cast(...) 은 매우 빠르다.