RTTI는 Run Time Type Information의 약자로 프로그램 실행 중에 개체의 형식이 결정될 수 있도록 하는 메카니즘입니다. 정확히 말하면, 실행중에 기반 클래스 타입 포인터의 실체를 밝혀내는 목적으로 하고 있으며, 다형성을 가진 객체에 대한 정확한 타입을 알아내기 위해 만들어졌습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class Object
{
public:
virtual void print()
{
cout << "Object" << endl;
}
};
class Monster : public Object
{
public:
virtual void print()
{
cout << "Monster" << endl;
}
};
int main()
{
Object* pObject = new Monster;
return 0;
}
| cs |
위와 같은 코드가 있을 때 실행중에 pObject가 가르키고 있는 객체가 Monster라는 것을 알 수 없습니다. 하지만 RTTI를 사용하면 Monster타입이라는 것을 정확하게 알 수 있지요.
RTTI는 다형성 객체들에 적용 가능하다는 것이 중요합니다. 그러기 위해서 RTTI지원을 가지기 위하여 적어도 하나의 가상 멤버함수를 가져야 합니다.
C++은 비 다형성 클래스들 및 프리티미브 타입(정수형, 실수형)에 대해 RTTI 지원을 제공하지 않습니다. double과 같은 기본적인 타입 또는 string과 같은 구체적인 클래스는 실행시간에 타입을 바꿀 수 없습니다.
이전에 C++ 가상함수에서 말씀드렸듯이 가상 멤버함수를 가지는 모든 객체는 컴파일러에 의해 특수한 데이터 멤버를 포함하게 됩니다. 이는 가상함수 테이블 포인터이며, 실행시간에 타입정보는 type_info객체에 대한 포인터로서 이 테이블에 저장됩니다.
모든 별개의 타입을 위해 C++은 실행시간에 타입정보를 포함하는 RTTI 객체를 인스턴스화 합니다. RTTI 객체는 표준 클래스 type_info으로부터 파생된 클래스의 인스턴스 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
class type_info
{
public:
type_info(type_info const&) = delete;
type_info& operator=(type_info const&) = delete;
size_t hash_code() const throw()
{
return __std_type_info_hash(&_Data);
}
bool operator==(type_info const& _Other) const throw()
{
return __std_type_info_compare(&_Data, &_Other._Data) == 0;
}
bool operator!=(type_info const& _Other) const throw()
{
return __std_type_info_compare(&_Data, &_Other._Data) != 0;
}
bool before(type_info const& _Other) const throw()
{
return __std_type_info_compare(&_Data, &_Other._Data) < 0;
}
char const* name() const throw()
{
#ifdef _M_CEE_PURE
return __std_type_info_name(&_Data, static_cast<__type_info_node*>(__type_info_root_node.ToPointer()));
#else
return __std_type_info_name(&_Data, &__type_info_root_node);
#endif
}
char const* raw_name() const throw()
{
return _Data._DecoratedName;
}
virtual ~type_info() throw();
private:
mutable __std_type_info_data _Data;
};
| cs |
일반적으로, 동일한 타입의 모든 인스턴스들은 type_info 객체를 공유합니다. type_inf의 가장 폭넓게 사용되는 멤버함수들은 name()과 ==연산자 입니다. 하지만 이러한 멤버함수들을 사용하기전에, typeid 연산자로 type_info객체에 접근할 수 있습니다.
typeid 연산자는 객체 또는 타입 이름을 인자로 받아 일치하는 const
type_info& 객체를 반환합니다.
typeid를 사용방법에 대해서 한번 보겠습니다.
1
2
3
4
5
|
Object* pObject = new Monster;
const type_info& rInfo1 = typeid(pObject);
const type_info& rInfo2 = typeid(*pObject);
| cs |
위 코드는 pObject의 객체 타입과 포인터 타입을 얻어오는데 반환타입이 const
type_info& 이며 반환된 type_info의 정보입니다.
역참조를 입력하면 객체의 타입정보를 알 수 있습니다.
1
2
3
4
5
6
7
|
Object* pObject = new Monster;
Monster* pMonster = dynamic_cast<Monster*>(pObject);
const char* pName1 = typeid(pObject).name();
const char* pName2 = typeid(*pObject).name();
const char* pName3 = typeid(pMonster).name();
const char* pName4 = typeid(*pMonster).name();
| cs |
위 코드는 name() 메서드를 호출하여 각 변수들의 포인터 타입과 할당된 객체의 이름을 보여줍니다.
1
2
3
4
5
6
7
8
|
if (typeid(*pMonster ) == typeid(Monster))
{
cout << "같은 객체 타입" << endl;
}
else
{
cout << "다른 객체 타입" << endl;
}
| cs |
이 처럼 같은 타입 정보를 가진 type_info들은 == 비교연산자로 확인할 수 있습니다.
이제 dynamic_cast<>에 대해서 보겠습니다. dynamic_cast<>에는 두 가지의 종류가 존재합니다. 하나는 포인터를 사용하는 것이고, 다른 하나는 레퍼런스를 사용합니다. 따라서 dynamic_cast<>에서 캐스팅하길 원하는 타입의 포인터 또는 레퍼런스를 반환합니다. 만약의 경우 캐스팅 실패시 포인터의 경우 null을 반환하고 레퍼런스의 경우 std::bad_cast 타입의 예외를 던집니다.
포인터 캐스팅 사용법에 대해 먼저 알아보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
|
Object* pObject = new Monster;
Monster* pMonster = dynamic_cast<Monster*>(pObject);
if (pMonster != nullptr)
{
// 캐스팅 성공
}
else
{
// 캐스팅 실패
}
| cs |
이번에는 레퍼런스 타입의 캐스팅입니다.
1
2
3
4
5
6
7
8
9
10
11
|
Monster mon;
Object& rObject = mon;
try
{
Monster& rMon = dynamic_cast<Monster&>(rObject);
}
catch (bad_cast)
{
// 레퍼런스 캐스팅 실패
}
| cs |
다운캐스팅외에 dynamic_cast<>는 다중상속에도 사용할 수 있습니다.
typeinfo나 bad_cast 사용을 위해선 아래와 같은 헤더파일 인클루드가 되어있어야 합니다.
1
|
#include <typeinfo.h>
| cs |
다중상속에서의 dynamic_cast<>를 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class Object
{
public:
int i;
virtual ~Object(){}
};
class dynamic
{
public:
bool b;
};
class Monster : public Object, dynamic
{
public:
int k;
Monster() { b = true; i = k = 0; }
};
int main()
{
Object* pObject = new Monster;
dynamic* pDynamic = dynamic_cast<dynamic*>(pObject);
return 0;
}
| cs |
pObject의 포인터 타입은 Object인 반면, 할당된 객체타입은 Monster입니다. 이러한 상속관계에서 만약 static_cast<>를 사용해 Object의 포인터를 Dynamic포인터로 변환할 수 없는데, 그 이유는 Object와 dynamic은 관계가 없기 때문입니다. 이 때 컴파일 에러가 발생합니다.
혹은 reinterpret_cast<>또는 C스타일 캐스트를 사용했을 때는 pObject를 pDynamic에 할당하기 때문에 런타임에 예측할 수 없는 문제들이 생깁니다.
Object와 Dynamic의 하위객체인 Monster는 각각 다른 주소에 위치합니다. 다중상속 캐스트를 적절하게 수행하기 위해서는 pDynamic값이 실행시간에 계산되어야 합니다. 결국 다중상속 캐스트는 클래스 Monster가 존재한다는 것조차도 알지 못하는 번역단위(translation unit)에서 수행될 수 있습니다.
1
2
|
Object* pObject = new Monster;
dynamic* pDynamic = (dynamic*)(pObject);
| cs |
C캐스팅 방식으로 다중상속관계에 있는 객체를 캐스팅하면 Object*의 포인터일 때와, dynamic*일때 가리키는 주소값이
같은걸 확인할 수 있습니다.
1
|
dynamic* pDynamic = dynamic_cast<dynamic*>(pObject);
| cs |
하지만 다중상속간 안전한 캐스팅을 제공하는 dynamic_cast<>를 사용해 캐스팅을 할 경우
각 Object에 있는 Monster의 객체 주소값과 dynamic에 속한 Monster의 객체 주소값이 다른 메모리 공간에 위치하는것을
확인할 수 있습니다.
이처럼 가상 하위객체의 매모리 배치는 비 가상 하위객체의 메모리 배치와는 다릅니다. 컴파일시간에 pObject에 의해서 지시되는 객체 안에 있는 하위객체 dynamic의 주소값을 계산하는 것은 불가능합니다.
RTTI의 비용은 아무런 비용없이 제공되는 기능이 아닙니다. 성능에서 얼마난 비용을 치루는지에 대해서는 내부 구조가 어떻게 구현되는지를 이해하는것이 중요합니다. 위에서 알아본 내용만으로도 기본적으로 메모리 오버헤드 및 실행 속도의 견지에서 RTTI의 성능 저하의 요인이 될 수도 있습니다.
메모리 오버헤드
사용자에 의해서 정의된 모든 타입은 type_info 객체를 저장하기 위해서 추가적인 메모리가 요구됩니다. 그리고 클래스당 오직 하나의 type_info 객체가 존재한다는 것을 보장하기도 어렵습니다. 그래서 하나 이상의 type_info 객체를 생성할 수 있습니다.
dynamic_cast<>가 오직 다형성 객체들에만 적용 가능한 구체적인 이유가 있습니다. 객체는 실행시간에 타입정보를 직접적으로 저장하지 않습니다. 예를들면 데이터 멤버로서,
다형성 객체들의 RTTI
모든 다형성 객체는 가상 함수 테이블에 대한 포인터를 가집니다. Vptr이라는 이름을 갖는 이 포인터는 이 클래스에 있는 모든 가상 함수의 메모리 주소들을 포함하는 dispatch테이블의 주소를 가집니다. 그리고 이 테이블에 또 다른 항목을 추가하는데 type_info의 객체를 가리킵니다.
typeid vs dynamic_cast<>
typeid는 상수시간 연산이며, 클래스들의 파생적인 복잡도와는 무관합니다. 모든 다형성 객체의 실행시간 타입정보를 얻는데 동일한 길이의 시간이 걸립니다. 본질적으로 typeid를 호출하는 것은 가상 멤버함수를 호출하는것과 유사합니다. 예를 들면 표현식 typeid(obj)는 아래와 같이 유사합니다.
return *(obj)->__vptr[0]; // 주소가 obj의 가상 테이블에서 옵셋 0에 저장되는 type_info 객체를 반환
클래스의 type_info객체에 대한 포인터가 가상테이블속에 고정된 옵셋에 저장된다는 것에 주목해야합니다.
typeid와 다르게, dynamic_cast<>는 상수시간 연산이 아닙니다. T가 타겟 타입이고 obj가 오퍼랜드일 때, 표현식
dynamic_cast<T&>(obj)에서 오퍼랜드를 타겟 타입으로 캐스트하는데 요구되는 시간은 obj의 클래스 계층의 복잡도에 의존합니다.
dynamic_cast<>는 목표 객체의 위치를 찾으 때 까지 obj의 파생트리를 탐색해야만 합니다. 타겟이 가상 최상의 클래스일 때, 동적 캐스트는 더 복잡해집니다. 결론적으로 더 긴 실행시간이 소요됩니다. 최악의 경우는 깊게 파생된 객체이고 타겟이 연관되지 않은 클래스 타입일 때 입니다.
dynamic_cast<>는 obj가 T로 캐스트될 수 없다고 자신있게 결정할 수 있기 전 까지는 obj의 전체 파생트리를 탐색해야 합니다.
즉, 실패한 dynamic_cast<>는
o(n)의 연산속도를 가지며, 여기서 n은 가상 클래스들의 수 입니다.
일반적으로 설계의 관점으로부터
dynamic_cast<>가 typeid보다는 선호되는데 그 이유는 전자가 더 큰 유연성과 확장성을 가능하도록 하기 때문입니다. 그럼에도 불구하고 포함되는 엔티티들의 파생적 복잡도에 의존하여, typeid의 실행시간 오버헤드는 dynamic_cast<>보다 덜 비싸질 수 있습니다.
결론
C++의 RTTI 메카니즘은 세 가지 구성요소들로 이루어집니다. typeid, dynamic_cast<>,
type_info.
RTTI지원을 가능하게 하기 위해서는 먼저,
객체는 적어도 하나의 가상 멤버함수를 가지고 있어야 합니다.
그리고 컴파일러의 RTTI지원을 켜야하며,
참조 즉, 레퍼런스를 가지고 dynamic_cast<>를 사용할 때 bad_cast예외들을 다루기 위해 catch 문장을 사용해야 합니다.
항상 포인터를 가지고
dynamic_cast<>를 사용할 때, 반환되는 값을 체크해야합니다.
다시 정리하면 런타임 형식 정보에는 다음 세 가지 기본 c++ 언어 요소가 있습니다.
1. dynamic_cast 연산자
- 클래스 상속관계에서 다운 캐스팅 및 다중 상속간 형변환을 안전하게 도와줍니다.
2. typeid 연산자
- 개체의 정확한 형식을 식별하는데 사용됩니다.
3. type_info 클래스
- typeid 연산자에서 반환된 형식 정보를 저장하는 데 사용됩니다.
* 아래 블로그와 사이트를 참조하여 작성하였습니다.