※ 게임 개발 포에버 http://www.gamedevforever.com 에서 연재 중인 글입니다.
RTTI란?
Run-Time Type Infomation 또는 Run-Time Type Identification 이라고도 합니다. 한글로 풀이하자면 "실시간 형식 정보" 라고 할수 있겠죠. 그럼 이게 무엇에 쓰는 물건인가? 라고 하면, 일단 다형성에 대해 알아야 할 것 입니다. C++에서는 대표적인 특징 중 하나로 다형성이라는 것이 있습니다. 부모 객체의 포인터로 자식 객체를 가리킬수 있는 것이죠. 간단하게 코드로 봐보자면 밑에와 같습니다.
그런데 여기서 Child의 PrintMemVar 함수를 호출할 일이 생긴다면 어떨까요? pParent는 실제 Child 객체이니까 PrintMemVar 함수를 호출할 수 있어야 합니다. 그런데 pParent는 현재 Parent 형이니 PrintMemVar 함수에 대한 정보가 없습니다. Child 형으로 형 변환을 해주어야 PrintMemVar 함수를 호출 할 수 있죠.
pParent가 Child가 아닌 Parent로 객체를 생성한 후, Child*형으로 형변환 하여 PrintfMemvar를 호출 해보면 전혀 예상치 못한 값들이 출력됩니다. 실제 프로젝트에서 이런 상황이 발생하게 되면 최악의 경우 크래시가 발생할 확률이 커집니다. 이런 문제를 방지 하기 위해 형변환을 하기 전 객체의 형식 정보를 확인할 필요가 있습니다.
RTTI는 바로 이 확인 작업을 위한 장치로 현재 객체의 형식 정보를 검사할 수 있는 명령어를 제공하고 있습니다. 대표적으로 dynamic_cast가 그것입니다. ( 이 외에, typeid, type_info 등이 있습니다. 자세한 것은 http://msdn.microsoft.com/en-us/library/b2ay8610.aspx 참고 )
dynamic_cast의 문제
dynamic_cast 로 안전하게 다운캐스트( down cast, 부모형에서 자식형으로 형변환 )를 할수 있다면 모든 문제가 다 해결된거 같습니다. 그런데 문제가 있습니다. dynamic_cast가 느리다는 겁니다. dynamic_cast 를 사용하면 내부적으로 RTTI 정보를 체크하고, 변환할 객체에 대한 형식 정보를 비교하는데 이 수행 비용이 큰게 문제입니다.
게임에서 퍼포먼스는 생명과도 같습니다. 아무리 멋진 게임이라도 프레임이 뚝뚝 끊기거나 느리면 하기 싫어지죠. 그래서 많은 프로그래머들이 1프레임이라도 더 빠르게 하기 위해 많은 노력을 합니다. 느린 dynamic_cast 대신에 빠르면서도 안전하게 다운캐스트 할수 있는 방법은 없을까 생각해볼 수 있죠.
만들어 봅시다
dynamic_cast 보다 빠르면서 같은 기능을 수행할 수 있는 방법은 의외로 간단하게 구현 할 수 있습니다.
그럼 실제 사용 예제를 보겠습니다.
해결해야 할 점
기존 dynamic_cast에 비해서 빨라졌다 하나 아직은 개선의 여지가 있습니다. 상위 클래스와 비교시 상속 깊이만큼 루프를 돈다는 점이나 메모리 비친화적이라는 점, 매크로를 많이 쓴다는 등이 있겠네요. 이런 점등은 좀더 고민해봐서 개선을 해봐야 할 것입니다.
RTTI란?
Run-Time Type Infomation 또는 Run-Time Type Identification 이라고도 합니다. 한글로 풀이하자면 "실시간 형식 정보" 라고 할수 있겠죠. 그럼 이게 무엇에 쓰는 물건인가? 라고 하면, 일단 다형성에 대해 알아야 할 것 입니다. C++에서는 대표적인 특징 중 하나로 다형성이라는 것이 있습니다. 부모 객체의 포인터로 자식 객체를 가리킬수 있는 것이죠. 간단하게 코드로 봐보자면 밑에와 같습니다.
class Parent { public: virtual void Print() { printf( "i'm your father!!\n" ); } }; class Child : public Parent { public: Child() : m_iMagicNum(777), m_pMagicPointer(NULL) {} virtual void Print() { printf( "i'm succeeding you father!!\n" ); } void PrintMemVar() { printf( "Magic~ Magic~ : %d, %d\n" ); } private: int m_iMagicNum; int* m_pMagicPointer; }; void main() { Parent *pParent = new Child; pParent->Print(); }위의 main 함수를 보시면 부모 클래스인 Parent 포인터 변수를 통해 Child 객체를 생성하여 Print 함수를 호출하고 있습니다. 여기서는 "i'm succeeding you father!!" 가 출력되겠죠. 이와 같은 코드는 실무에서 빈번하게 일어납니다. 실제 객체가 어떤 모양이든 간에 상관없이 인터페이스 클래스를 통해 객체들을 통합적으로 관리할수 있는 이점을 제공하기 때문이죠.
그런데 여기서 Child의 PrintMemVar 함수를 호출할 일이 생긴다면 어떨까요? pParent는 실제 Child 객체이니까 PrintMemVar 함수를 호출할 수 있어야 합니다. 그런데 pParent는 현재 Parent 형이니 PrintMemVar 함수에 대한 정보가 없습니다. Child 형으로 형 변환을 해주어야 PrintMemVar 함수를 호출 할 수 있죠.
void main() { Parent *pParent = new Child; pParent->Print(); // Child* 형으로 형변환 Child *pChild = (Child*)pParent; pChild->PrintMemVar(); }잘 작동합니다. pParent가 애초에 Child 객체이니 당연한 결과죠. 하지만 이 코드는 굉장히 위험한 코드입니다. 위의 코드야 앞서 pParent가 Child 객체인 것을 알고 있으니 이런 식으로 코드를 작성할 수 있는 것이지 실무에서는 현재 가리키고 있는 객체가 Child 형인지 Parent인지 또는 전혀 다른 상속 객체인지 알 수가 없습니다.
pParent가 Child가 아닌 Parent로 객체를 생성한 후, Child*형으로 형변환 하여 PrintfMemvar를 호출 해보면 전혀 예상치 못한 값들이 출력됩니다. 실제 프로젝트에서 이런 상황이 발생하게 되면 최악의 경우 크래시가 발생할 확률이 커집니다. 이런 문제를 방지 하기 위해 형변환을 하기 전 객체의 형식 정보를 확인할 필요가 있습니다.
RTTI는 바로 이 확인 작업을 위한 장치로 현재 객체의 형식 정보를 검사할 수 있는 명령어를 제공하고 있습니다. 대표적으로 dynamic_cast가 그것입니다. ( 이 외에, typeid, type_info 등이 있습니다. 자세한 것은 http://msdn.microsoft.com/en-us/library/b2ay8610.aspx 참고 )
void main() { Parent *pParent = new Child; pParent->Print(); // pParent가 Child* 형으로 변환 가능한지 검사 // pParent가 Child* 형이 아니면 NULL 이 반환된다. Child *pChild = dynamic_cast<Child*>(pParent); if( pChild ) pChild->PrintMemVar(); }위의 메인 함수에 추가된 코드 입니다. dynamic_cast를 통해 형 변환을 수행하면 pParent가 가리키고 있는 객체가 Child 이라면 포인터를 반환하고 그렇지 않으면 NULL 포인터를 반환하게 됩니다. 이를 통해 안전하게 Child 형으로 변환하여 Child에서 정의한 함수를 호출할 수 있습니다.
dynamic_cast의 문제
dynamic_cast 로 안전하게 다운캐스트( down cast, 부모형에서 자식형으로 형변환 )를 할수 있다면 모든 문제가 다 해결된거 같습니다. 그런데 문제가 있습니다. dynamic_cast가 느리다는 겁니다. dynamic_cast 를 사용하면 내부적으로 RTTI 정보를 체크하고, 변환할 객체에 대한 형식 정보를 비교하는데 이 수행 비용이 큰게 문제입니다.
게임에서 퍼포먼스는 생명과도 같습니다. 아무리 멋진 게임이라도 프레임이 뚝뚝 끊기거나 느리면 하기 싫어지죠. 그래서 많은 프로그래머들이 1프레임이라도 더 빠르게 하기 위해 많은 노력을 합니다. 느린 dynamic_cast 대신에 빠르면서도 안전하게 다운캐스트 할수 있는 방법은 없을까 생각해볼 수 있죠.
만들어 봅시다
dynamic_cast 보다 빠르면서 같은 기능을 수행할 수 있는 방법은 의외로 간단하게 구현 할 수 있습니다.
class CRTTI { public: CRTTI( const wstring strClassName, const CRTTI* pBaseRTTI ); inline const wstring& GetClassName() const; inline const CRTTI* GetBaseRTTI() const; private: const wstring m_strClassName; const CRTTI* m_pBaseRTTI; };위 클래스가 새로운 RTTI 검사를 위한 클래스입니다. 정말 간단하죠? 단순히 클래스 이름과 부모 클래스를 위한 멤버변수 하나가 전부입니다. 그러면 이것을 어떻게 이용해서 객체의 형식을 검사할지 보겠습니다.
// 최상위 클래스에 선언 #define DeclRootRTTI(classname) \ public: \ static const CRTTI ms_RTTI; \ virtual const CRTTI* GetRTTI() const { return &ms_RTTI; } \ static bool IsKindOf( const CRTTI* pRTTI, const classname *pObject ) \ { \ if( NULL == pObject ) \ { \ return false; \ } \ return pObject->IsKindOf( pRTTI ); \ } \ bool IsKindOf( const CRTTI* pRTTI ) const \ { \ const CRTTI* pTmp = GetRTTI(); \ while( NULL != pTmp ) \ { \ if (pTmp == pRTTI) \ { \ return true; \ } \ pTmp = pTmp->GetBaseRTTI(); \ } \ return false; \ } // 자식 클래스들에 선언 #define DeclRTTI \ public: \ static const CRTTI ms_RTTI; \ virtual const CRTTI* GetRTTI() const {return &ms_RTTI;} #define ImplRootRTTI(classname) \ const CRTTI classname::ms_RTTI(L#classname, NULL) #define ImplRTTI( classname, baseclassname) \ const CRTTI classname::ms_RTTI(L#classname, &baseclassname::ms_RTTI) // Parent 클래스는 최상위 클래스다 class Parent { public: DeclRootRTTI(Parent); // RootRTTI 선언 public: virtual void Print() { printf( "i'm your father!!\n" ); } }; ImplRootRTTI(Parent); // Child 클래스는 Parent로부터 상속 되었다 class Child : public Parent { public: DeclRTTI; public: Child() : m_iMagicNum(777), m_pMagicPointer(NULL) {} virtual void Print() { printf( "i'm succeeding you father!!\n" ); } void PrintMemVar() { printf( "Magic~ Magic~ : %d, %d\n" ); } private: int m_iMagicNum; int* m_pMagicPointer; }; ImplRTTI(Child, Parent); // Parent에서 상속됨을 알린다매크로 때문에 코드가 복잡해 보일 수 있지만 잘 보시면 별거 없습니다. CRTTI를 각 클래스에 정적 변수로 선언을 해주고, 상위 클래스가 있는 경우 CRTTI의 m_pBaseRTTI 멤버 변수에 상위 클래스의 CRTTI 정적 멤버 변수를 저장하는 것이 전부 지요. 그리고 이 각 클래스의 CRTTI 정적 멤버 변수를 비교함으로서 클래스의 형식 정보를 알아 낼 수 있습니다. 단순 포인터 비교이기 때문에 속도도 빠르지요.
그럼 실제 사용 예제를 보겠습니다.
#define IsKindOf(classname, pObject) \ classname::IsKindOf(&classname::ms_RTTI, pObject) void main() { Parent *pParent = new Child; pParent->Print(); // pParent가 Child* 형으로 변환 가능한지 검사 if( IsKindOf( Child, pParent ) ) { Child *pChild = (Child*)pParent; pChild->PrintMemVar(); } } // 좀더 dynamic_cast 와 비슷한 방식으로 사용 하기 위한 방법 // DeclRootRTTI 매크로에 밑의 코드를 추가 static classname* DynamicCast( const CRTTI* pRTTI, \ const classname* pObject ) \ { \ if( !pObject ) \ { \ return NULL; \ } \ return pObject->DynamicCast( pRTTI ); \ } \ classname* DynamicCast( const CRTTI* pRTTI ) const \ { \ return ( IsKindOf( pRTTI ) ? ( classname* ) this : NULL ); \ } #define DynamicCast(classname, pObject) \ ((classname*) classname::DynamicCast(&classname::ms_RTTI, pObject)) void main() { Parent *pParent = new Child; pParent->Print(); // pParent가 Child* 형으로 변환 가능한지 검사 Child *pChild = DynamicCast(Child, pParent); if( pChild ) pChild->PrintMemVar(); }밑의 DynamicCast 부분을 보시면 dynamic_cast와 같은 방식으로 사용하면서도 수행 속도에서는 훨씬 빠른 형식 정보 검사를 할수 있습니다.
해결해야 할 점
기존 dynamic_cast에 비해서 빨라졌다 하나 아직은 개선의 여지가 있습니다. 상위 클래스와 비교시 상속 깊이만큼 루프를 돈다는 점이나 메모리 비친화적이라는 점, 매크로를 많이 쓴다는 등이 있겠네요. 이런 점등은 좀더 고민해봐서 개선을 해봐야 할 것입니다.
스샷 하나 없는 것이 심심해서 울겜 일러스트 한장 투척