기술자료

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

C++ 프로그래밍 : 런타임 타입 정보(Runtime Type Information)

기술자료
DBMS별 분류
Etc
작성자
dataonair
작성일
2015-12-23 00:00
조회
5321



C++ 프로그래밍

런타임 타입 정보(Runtime Type Information)



클래스 사이 타입 변환에 대해 두 번에 걸쳐 자세한 소개를 했다. 간단히 떠올려보면 강제 타입 변환을 비롯해 static_cast, reinterpret_cast에 관련한 성질을 중점적으로 알아봤다. 몇몇 개발자는 C++ 타입 변환 중에 자바(Java)에 필적할만한 RTTI(Runtime Type Information)를 추가적으로 소개해주면 좋겠다는 의견을 제시했다. 비록 RTTI가 C++ 개발에 많이 사용되는 편은 아니지만 C++이 현대화하고 있는 추세에 맞춰 RTTI를 한번쯤 이해하고 가는 것은 많은 도움이 될 수 있을 거라는 생각이 들었다. 따라서 이번 글에서는 C++의 RTTI를 자세히 알아보겠다.



1. 다이나믹 캐스트(dynamic_cast)

지난 글에 타입 변환을 소개했는데, 소개하지 않은 타입 변환이 하나 더 있다. 바로 다이나믹 캐스트(dynamic_cast)다. 왜 지금 갑자기 다이나믹 캐스트를 따로 소개하는지 궁금할 수 있는데, 바로 다이나믹 캐스트가 RTTI와는 떼려야 뗄 수가 없는 관계기 때문이다. 다이나믹 캐스트가 C++ 개발에 일반적으로 사용되는 것은 아니다. 일반적으로 사용되지 않는다는 의미는 다이나믹 캐스트를 사용하기 위해서는 컴파일러에 특별한 설정을 해야만 한다는 것이다. 즉, 옵션의 개념과 같은 것이라고 볼 수 있다. 왜 C++ 컴파일러는 이것을 옵션으로 남겨둔 것일까 이유야 여러 가지가 있겠지만 다이나믹 캐스트 사용을 위한 환경이 C/C++하면 떠오르는 특징인 고성능, 고효율이라는 성질과 그리 잘 어울리지는 않기 때문이다. 즉, 기본으로 포함시키기엔 성능과 효율을 떨어트릴 수 있기에 옵션 정도로만 제공한다고 생각하면 좋을 것 같다. 그러나 반대로 생각해보면 컴퓨터의 성능이 비약적으로 발전하고, 자바(Java)와 C# 같이 동적 타입 정보를 이용하는 프로그래밍 언어가 인기를 끈다는 점에서 C++에서 다이나믹 캐스트의 사용은 더욱 필요한 것이 아닐까 생각도 든다. 그럼 이제부터 다이나믹 캐스트를 심층 분석해보자.

다이나믹 캐스트는 런타임 타입 정보를 사용해 타입 변환을 하는 연산자다. 런타임 타입 정보가 바로 RTTI(Runtime Type Information)다. 즉, 실행시간에 타입 정보를 검사해 제대로 된 타입 변환만을 허용하기 때문에 안정성 면에서는 뛰어나지만 그만큼 성능에서는 뒤처질 수밖에 없다. 따라서 명백하게 안전한 포인터만을 사용하도록 잘 설계된 코드에서는 굳이 다이나믹 캐스트를 사용할 필요는 없다. 그럼에도 다이나믹 캐스트가 사용되는 이유는 런타임에 실제 객체의 타입 정보를 이용하는 코드가 증가하고 있기 때문이다. 물론 가상 함수를 통해서도 충분히 비슷한 코드를 구현할 수 있지만, 초·중급 개발자에게 쉬운 일은 아니다.

지난 글의 내용을 떠올려보자. 상속 관계 클래스 타입 변환에서 자식 클래스는 부모 클래스로 언제든지 타입 변환이 가능했다. 왜냐하면 자식 클래스가 부모 클래스를 온전하게 포함하고 있기 때문이다. 당연히 자식 클래스 포인터 타입에서 부모 클래스 포인터 타입 변환도 전혀 문제가 없었다. 그러나 반대의 경우인 부모 클래스에서 자식 클래스로 타입 변환은 거부됐으며, 부모 클래스 포인터 타입에서 자식 클래스 포인터 타입으로 변환은 기본적으로는 거부되지만 강제 타입 변환이나 static_cast의 경우는 허용해줬다. 물론 이것은 가상 상속이 아닌 일반 상속인 경우를 말한다.

상식적으로 생각해보자. 부모 클래스에서 자식 클래스로의 타입 변환은 허용되지 않는다. 그런데 포인터 타입에 대해서는 강제 타입 변환과 static_cast에 대해서만 허용을 해준다. 뭔가 불공정한 느낌이 들지 않는가 정말 위험하다면 애초에 포인터 타입 변환마저 허용하지 말았어야 한다. 그럼에도 허용을 하는 이유는 그렇게 사용해야만 하는 경우가 있기 때문이다. 그럼 예제 코드를 살펴보자.



<리스트 1> 위험한 타입 변환 허용class CParent
{
public:
char m_Type; // Type Information
};class CChildA : public CParent
{
public:
CChildA()
{
m_Type = 'A';
m_ChildA = 1;
}
int m_ChildA;
};class CChildB : public CParent
{
public:
CChildB()
{
m_Type = 'B';
m_ChildB = 2;
}
int m_ChildB;
};int GetValue(CParent* pParent) // ①
{
if(pParent->m_Type == 'A')
{
CChildA* pC = static_cast< CChildA*>(pParent); //②
return pC->m_ChildA;
}
else if(pParent->m_Type == 'B')
{
CChildB* pC = static_cast< CChildB*>(pParent); //③
return pC->m_ChildB;
}
return 0;
}void main()
{
CChildA ca;
int VA = GetValue(&ca); CChildB cb;
int VB = GetValue(&cb); CParent p;
p.m_Type = 'A';
int VP = GetValue(&p); // ④
}



<리스트 1>에서 CParent를 상속하는 자식 클래스 CChildA와 CChildB가 있다. main에서 각각의 객체 ca, cb에 대하여 GetValue를 호출한다. ①을 살펴보자. 인자 타입이 CParent*이다. 따라서 객체 ca, cb의 주소가 넘어갈 때 자동으로 부모 클래스 포인터 타입으로 변환된다. GetValue 안에서는 ②, ③처럼 부모 클래스 포인터 타입인 CParent*에서 각각의 자식 클래스 포인터 타입인 CChildA*, CChildB*로 변환을 수행한다. 당연히 프로그램 결과인 VA, VB에는 각각 1, 2가 들어있을 것이다.
이 예제를 제시한 이유는 간단하다. 이 예제처럼 부모 클래스 포인터 타입에서 자식 클래스 포인터 타입으로 변환하는 코드들이 의외로 많이 쓰이기 때문이다. 즉, 이런 코드들이 원활하게 돌아가도록 하기 위해서 C++ 컴파일러는 강제 타입 변환이나 static_cast에 대해서 변환을 허용하는 것이다. 그러나 ④처럼 애초에 CParent인 객체에 대해서 GetValue를 호출할 경우 실행 중 어떤 일이 벌어질지는 장담할 수 없는 것이다. 결국 강제 타입 변환이나 static_cast를 사용함으로 인한 결과와 책임은 전적으로 개발자에게 있다. 이런 위험을 회피하기 위해서 필요한 기능은 무엇이 있을까 아마도 GetValue 함수 안에서 타입 변환 시점에 실제 객체의 타입을 알 수 있다면 좋을 것 같다. 즉, 원래 CChildA, CChildB 객체에 대해서만 타입 변환을 허용해준다면 더욱 안전할 수 있지 않을까 바로 그런 생각에서 나온 개념이 dynamic_cast인 것이다.

지금부터 독자들이 실제 C++ 설계자라고 생각하고 dynamic_cast를 직접 설계해보자. ① GetValue 함수의 인자 CParent* pParent가 실제 어떤 객체로부터 변환된 포인터인지 알아낼 방법은 없을까 CParent의 멤버로 m_Type이 있다. 이 값을 통해서 어느 정도 객체를 구분할 수는 있을 것 같다. 이미 CChildA, CChildB를 m_Type을 통해서 구분하고 있기 때문이다자가 추가하기도 어려우며, 외부에 공개되면 잘못된 값이 설정될 위험도 충분하다. 이미 ④처럼 잘못된 타입 정보가 쓰이는 것을 보지 않았는가! p.m_Type = ‘A’처럼 말이다.

그래서 새로운 방식을 고안하게 됐는데, 모든 클래스가 정의될 때 컴파일러는 외부에 숨겨진 타입 정보 객체를 클래스 멤버로 추가하고, 컴파일러만이 해당 멤버의 타입 정보를 설정할 수 있도록 하는 것이다. 그리고 dynamic_cast 연산자는 입력으로 들어온 객체의 타입 정보 객체를 읽어서 실제 타입을 알아낼 수 있도록 하는 것이다. 나름 괜찮은 것 같다. 그러나 이 방식에는 약간의 효율성 문제가 있는데, 타입 정보를 전혀 사용하지 않는 경우에도 모든 클래스 객체에 타입 정보 객체가 무조건 포함된다면 객체의 크기만 커지는 비효율적인 문제가 발생할 수 있다. 그래서 추가적인 결정을 내리기로 했다. 컴파일러에게 타입 정보를 사용하기로 지시하는 경우에만 클래스 객체에 타입 정보 객체를 멤버로 추가하기로 말이다.

위의 방법이 사실상 RTTI와 dynamic_cast의 실제 설계 방식이다. 컴파일러마다 세부적인 방식이 달라지기는 하지만 큰 틀에서는 비슷한 기조를 유지한다.

이제 RTTI와 dynamic_cast가 어떻게 구현되는지 자세히 알아보자. 일단 컴파일러에게 RTTI를 사용하겠다고 지시하는 방법을 알아보자. 이것은 컴파일러마다 조금씩 다르기 때문에 각 컴파일러의 설명을 참조하면 될 것이다. Visual C++의 경우에는 다음 그림과 같이 [프로젝트 > 속성 > 구성속성 > C/C++ > 언어 > 런타임 형식 정보 사용 > 예(/GR)]를 설정하면 된다. 기본값은 RTTI를 사용하지 않는다.

tech_img4226.png

컴파일러에게 RTTI를 사용하겠다고 알려도 컴파일러가 모든 클래스 객체 안에 타입 정보 객체를 포함시키는 것은 아니다. C++ 명세에 의하면 클래스에 가상 함수가 하나라도 있을 경우에만 타입 정보 객체가 포함되도록 구현돼 있다. 즉, RTTI를 사용하려는 클래스는 가상 함수를 하나 이상은 선언해야 한다. 그러나 보통 일반적으로 클래스의 소멸자는 특별한 경우가 아니면 가상 함수로 지정하기 때문에 RTTI 사용을 위한 제약이 그리 큰 것은 아니다. 그렇다면 어떤 구조로 타입 정보 객체가 포함되는지 살펴보자. 참고로 타입 정보 객체가 포함되는 구조는 각 컴파일러 제작사마다 다르게 구현돼 있다. 그러나 일반적으로 비슷한 방식으로 구현되므로 마이크로소프트의 Visual C++ 컴파일러를 기준으로 설명할 것이다. 다른 컴파일러와 큰 차이는 없기 때문에 RTTI의 원리를 이해하는데 큰 도움이 될 것이라고 생각한다.



2. 타입 정보(type_info)

타입 정보 객체에 대해서 먼저 알아보자. 말 그대로 해당 타입에 대한 정보가 담겨있는 객체다. 각 클래스에 대해서 타입 정보 객체 단 하나만 정의된다. 즉, 타입 정보 객체는 싱글톤(Singleton)이라고 할 수 있다. 따라서 같은 클래스의 수많은 객체들은 클래스 타입 정보를 가진 싱글톤을 가리키는 포인터를 가지고 있고, 해당 포인터를 통해서 타입 정보를 공유할 수 있다. 그럼 타입 정보 객체를 가리키는 포인터는 어디 있는 것일까 바로 가상 함수 테이블에 포함되어 있다. 직접 소스 코드와 그림을 보면서 살펴보자.



<리스트 2> 가상 함수 테이블class CTest
{
public:
virtual void Func1() {}
virtual void Func2() {}
int m_Test;
};



<리스트 2>는 클래스 CTest의 정의를 보여준다. 아무것도 하지 않는 함수 Func1과 Func2는 모두 가상 함수로 선언돼 있다. 가상 함수에 대해서는 언젠가 마이크로소프트웨어를 통해서 아주 자세히 다룰 예정이지만 현재 주제와 어느 정도 관계가 있기 때문에 어느 정도는 기본 개념을 알고 있을 필요가 있다. 만일 가상 함수에 대해서 전혀 모르는 독자라면 C++ 기본 서적을 통해서 기초적인 지식을 먼저 습득하길 추천한다. 그럼 CTest의 메모리 구조를 살펴보자.

tech_img4227.png

<그림 2>에서 알 수 있듯이, 클래스에 가상 함수가 선언될 경우 클래스 메모리 시작 위치에는 vfptr이라는 가상 함수 테이블 포인터가 생성된다. 말 그대로 가상 함수 테이블을 가리키는 역할을 한다. 테이블에는 클래스에서 선언된 가상 함수의 주소들이 적혀있고, 그 주소를 따라가면 실제 함수 본체 코드 영역에 도달할 수 있다.

C++ 설계자들은 위와 같은 구조에서 타입 정보 객체를 가리키는 포인터를 어디다 넣을지 고민했다. 클래스 안에다가 직접 넣자니 포인터 크기만큼 객체의 크기가 증가하는 단점이 있다. 그래서 내린 결정은 가상 함수 테이블에 추가하기로 한 것이다. 그렇게 할 경우 기존 클래스의 구조는 전혀 바뀌지 않으니 하위 호환성을 유지하는데도 큰 이점을 가질 수 있었다. 그렇다면 가상 함수 테이블 어디에 넣을 것인가 첫 번째 아니면 제일 마지막 이건 말 그대로 컴파일러 제작사가 결정할 문제지만 마이크로소프트의 컴파일러 설계자들은 좀 더 색다른 결정을 하게 된다. 바로 테이블 첫 번째 이전에 포인터 공간을 마련하는 것이었다(이 아이디어가 어디에서 나왔는지 필자가 확인할 수 없었지만 VC++ 컴파일러는 이 아이디어를 채택했다. 그리고 다른 컴파일러도 역시 채택했을 수 있다).

그럼 실제 추가된 구조를 살펴보자. tech_img4228.png

<그림 3>을 보면 한눈에도 복잡하다는 것이 느껴질 것이다. 참고적인 자료가 될 수 있도록 VC++ RTTI의 실제 구조를 그대로 그려보았다.

일단 가상 함수 테이블을 살펴보자! CTest의 vfptr은 여전히 가상 함수 테이블의 첫 번째 항목을 가리키고 있다. 즉, [0] Address of Func1 항목을 가리키고 있다. 실제 RTTI 객체를 가리키는 포인터는 테이블 첫 번째 항목 바로 전에 위치하고 있다. 이렇게 함으로써 기존 가상 함수 테이블 구현과의 호환성을 유지할 수 있다. 새롭게 추가된 RTTI 객체 포인터에는 실제 RTTI 객체의 주소가 담겨있다. 그 주소를 따라가 보자! VC++는 RTTI 객체에 이름을 붙여놓았는데 그 이름이 RTTI Complete Object Locator이다. 해당 객체의 4번째 멤버를 살펴보자! 이름이 pTypeDescriptor이다. 이름에서 알 수 있듯이 포인터 변수다. 그리고 이 포인터가 가리키는 대상이 바로 실제 타입 정보가 기록되어 있는 type_info 객체인 것이다.

약간 헷갈릴 수 있겠지만 VC++는 타입 정보 객체를 2단계에 걸쳐서 구현해 놓은 것이다. 참고로 타입 정보 객체는 싱글톤이라고 했는데 그것이 바로 type_info 객체이다. 따라서 복수의 RTTI Complete Object Locator 객체가 하나의 type_info 객체를 가리킬 수 있는 N:1의 관계가 성립한다. RTTI Complete Object Locator도 타입 정보 객체라고 할 수 있지만, 엄밀히 말하면 이름에서도 알 수 있듯이 중간 매개 역할을 하는 객체라고 할 수 있다. 따라서 실제 런타임에서 타입 정보를 이용할 때는 type_info 객체를 사용한다. 그럼 실제로 type_info를 사용하는 코드를 살펴보자.



<리스트 3> type_infovoid main()
{
CTest t;
const type_info& ti = typeid(t); // ① cout < < _T("Type Name: ") < < ti.name(); // ②
}



<리스트 3>의 ①처럼 typeid 함수를 이용하여 type_info 객체를 구할 수 있다. 보통 반환을 받을 때 const 키워드와 함께 참조 타입 &을 사용하는데, 타입 정보 객체는 절대 변경할 수 없기에 const가 사용되며, 복사 생성자의 호출을 피하기 위해 참조 타입(&)이 사용되는 것이다. type_info는 타입과 관련된 몇 가지 함수들을 제공하는데 그 중에서 ②처럼 name 함수는 실제 클래스의 타입 이름을 반환해준다. 이 예제의 출력 결과를 확인해보자.

Type name: class CTest

이제 RTTI를 이해했으니, RTTI를 이용한 타입 변환 연산자인 dynamic_cast에 대해서 실제 코드를 보면서 살펴보자.



<리스트 4> dynamic_castclass CParentA
{
public:
virtual ~CParentA() {} // ⓐ
};class CParentB
{
public:
virtual ~CParentB() {} // ⓑ
};class CChild : public CParentA, public CParentB
{
public:
};void main()
{
{
CChild c;
CParentA* pA = dynamic_cast< CParentA*>(&c);//① OK
CParentB* pB = dynamic_cast< CParentB*>(&c);//② OK
}
{
CParentA a;
CChild* pC1 = dynamic_cast< CChild*>(&a); //③ NULL CParentB b;
CChild* pC2 = dynamic_cast< CChild*>(&b); //④ NULL
}
{
CChild c;
CParentA* pA = &c; CChild* pC = dynamic_cast< CChild*>(pA); //⑤ OK
CParentB* pB = dynamic_cast< CParentB*>(pA);//⑥ OK
}
{
CChild c;
CParentB* pB = &c; CChild* pC = dynamic_cast< CChild*>(pB); //⑦ OK
CParentA* pA = dynamic_cast< CParentA*>(pB);//⑧ OK
}
{
CChild c;
CParentA a = dynamic_cast< CParentA&>(c); //⑨ OK
CParentB b = dynamic_cast< CParentB&>(c); //⑩ OK
}
{
CParentA a;
CChild c1 = dynamic_cast< CChild&>(a); // ⑪ 예외 CParentB b;
CChild c2 = dynamic_cast< CChild&>(b); // ⑫ 예외
}
}



<리스트 4>에서 중요한 점은 ⓐ, ⓑ처럼 가상 함수를 선언해야 한다는 것이다. 여기서는 소멸자를 가상 함수로 선언했다. 만일 가상 함수가 없는 상태로 dynamic_cast를 사용할 경우 컴파일 자체가 되지 않는다. dynamic_cast를 해석하는 가장 쉬운 방법이 있는데, 바로 변환하고자 하는 객체의 타입을 실제 객체의 타입으로 여기고 변환을 수행하면 된다는 것이다. 의미가 잘 파악되지 않을 수 있는데 소스 코드의 각각의 경우를 통해서 확인해보자.

①, ②에서 &c의 실제 타입은 CChild*이다. 따라서 부모 클래스 포인터 타입으로 변환하는 것은 항상 안전하다. 그러므로 변환은 성공한다.

③, ④에서 &a, &b의 실제 타입은 CParentA*와 CParentB*이다. 여기서 자식 클래스 포인터 타입인 CChild*로 변환하는 것은 메모리 접근 위반이 발생할 수 있으므로 위험하다. 따라서 변환은 실패한다. 보통 변환을 허용하지 않을 경우 컴파일러는 변환을 거부하면서 컴파일 타임에 에러를 발생시킨다. 그러나 dynamic_cast는 런타임 타입 변환 연산자다. 즉, 실행시점에 변환 허용 여부가 결정된다. 따라서 실행되기 전에는 성공과 실패 여부를 확인할 수가 없다. 따라서 실행시점에 변환 성공과 실패를 구분할 필요가 있으며, 변환이 실패할 경우 NULL을 반환하거나 예외를 던지도록 설계되어 있다. 예외를 던지는 경우는 나중에 살펴보기로 하고, 일단 변환이 실패할 경우는 NULL이 반환된다고 기억하자.

⑤, ⑥의 pA의 타입은 CParent*이지만 실제 타입은 CChild*이다. pA가 CChild 객체로부터 나온 것이기 때문이다. 따라서 ⑤는 CChild*에서 CChild*로 타입 변환을 하라는 것이다. 이것은 당연히 성공할 수밖에 없다. ⑥도 같은 방식으로 해석할 수 있다. CChild*에서 CParentB*로 타입 변환하라는 것이다. 당연히 자식 클래스 포인터에서 부모 클래스 포인터로 변환되는 것이므로 성공한다. 보통 ⑥과 같은 상황을 부모 클래스 포인터에서 다른 부모 클래스 포인터로 변환된다고 해서 Cross Type Casting이라는 말로 부르기도 하는데, 엄밀히 말하면 자식 클래스 포인터에서 부모 클래스 포인터 타입으로 변환하는 것이기 때문에 Up Casting일 뿐이다. 이것은 static_cast에서는 되지 않는데, static_cast는 상속 관계의 클래스 사이에서만 가능하기 때문이다.

⑦, ⑧도 앞의 ⑤, ⑥과 같은 이유로 타입 변환이 성공한다.

지금까지는 포인터 타입으로만 변환을 하였는데 클래스 참조 타입으로 변환을 해보자! 참고로 dynamic_cast에서 변환할 타입은 오직 타입 정보를 가질 수 있는 클래스의 포인터 타입 및 참조 타입만 가능하다.

⑨, ⑩에서 c의 실제 타입은 CChild이다. 따라서 부모 클래스 참조 타입인 CParentA&, CParentB&으로 변환하는 것은 성공한다.

⑪, ⑫에서 a, b의 실제 타입은 CParentA, CParentB이다. 이것을 자식 클래스 참조 타입인 CChild&로 변환하려고 한다. 여기서 중요한 사실을 기억해보자. 부모 클래스에서 자식 클래스로 변환하는 것은 메모리 접근 위반이 발생할 수 있으므로 위험하다고 했다. 따라서 자식 클래스 참조 타입으로 변환하는 것은 실패해야 한다. 그러나 변환 타입이 참조 타입이기 때문에 NULL을 반환할 수가 없다. 그래서 이런 경우는 어쩔 수 없이 bad_cast 예외를 던지게 돼있다.

정리하자면 dynamic_cast는 런타임 타입 변환을 하는 것이므로 무조건 실행돼야 하고, 실행시점에 변환 성공과 실패 여부가 결정된다. 따라서 타입 변환이 실패할 경우에는 포인터 타입일 경우 NULL을 반환하고, 참조 타입일 경우 bad_cast 예외를 던지게 된다. 따라서 타입 변환 실패 처리를 하기 위해서는 NULL 검사를 수행해야 됨은 물론이고, bad_cast를 처리하기 위해서 try ~ catch로 예외 처리를 해야 한다. 이제 충분히 RTTI와 dynamic_cast를 이해했으니, 실제로 가장 먼저 제시된 예제 <리스트 1>을 dynamic_cast를 사용하여 변경해보자.



<리스트 5> RTTI & dynamic_cast 실습class CParent
{
public:
virtual ~CParent() {} // ①
};class CChildA : public CParent
{
public:
CChildA()
{
m_ChildA = 1;
}
int m_ChildA;
};class CChildB : public CParent
{
public:
CChildB()
{
m_ChildB = 2;
}
int m_ChildB;
};int GetValue(CParent* pParent)
{
CChildA* pCA = dynamic_cast< CChildA*>(pParent); //②
if(pCA)
{
return pCA->m_ChildA;
} CChildB* pCB = dynamic_cast< CChildB*>(pParent); //③
if(pCB)
{
return pCB->m_ChildB;
}
return 0; // ④
}void main()
{
CChildA ca;
int VA = GetValue(&ca); CChildB cb;
int VB = GetValue(&cb); CParent p;
int VP = GetValue(&p); // ④
}



<리스트 5>는 <리스트 1>에서 몇 군데를 수정한 것이다. ①을 보자. 먼저 CParent의 소멸자를 가상 함수로 선언했다. RTTI를 이용하기 위한 것이다. 이제 CParent의 메모리 시작위치에 vfptr이 생성된다. 물론 vfptr은 CParent를 상속하는 자식 클래스 CChildA, CChildB에도 그대로 내려온다. 따라서 CChildA와 CChildB가 정의될 때 컴파일러는 실제 타입 정보 객체를 생성하여 vfptr과 연결시켜 놓을 것이다. 가장 많이 변한 부분은 ②, ③ 부분이다. 더 이상 강제 타입 변환이나 static_cast를 사용할 필요가 없다. 변환이 성공하면 변환된 포인터 값이 반환될 것이다. 따라서 반환된 포인터 값의 NULL 체크가 필요하다. ④를 따라가 보면 결국 GetValue 함수에서 NULL 체크에 걸려서 0을 반환하게 될 것이다.



정리

이것으로 RTTI 소개가 충분히 되었다고 생각한다. 솔직히 필자는 오랜 기간 프로그래밍을 하면서 수많은 C++ 소스 코드를 보았지만 dynamic_cast를 사용하는 경우를 거의 보지 못했다. 그 이유는 아마도 RTTI를 사용하기 위해서는 컴파일러 옵션을 지정해야 되기 때문인 것 같다. 보통 대부분의 사용자들은 Default Option을 선호하기 마련이다. 또 하나 원인을 찾는다면 C++이 주로 사용되는 Windows 프로그래밍에서는 MFC라는 뛰어난 프레임워크가 있기 때문이 아닐까 생각된다. C++에 RTTI 개념이 들어오기 전에 이미 MFC는 자체적으로 RTTI를 구현해놓았다. 또한 MFC의 RTTI가 효용성이나 사용면에서 훨씬 뛰어나기 때문에 굳이 C++ RTTI를 쓸 필요가 없었다. 그만큼 MFC가 잘 만들어진 선구적인 프레임워크라는 것이다. MFC를 들여다보면 프레임, 도큐먼트, 뷰 구조에서 직렬화(Serialize)까지 RTTI가 안 쓰이는 곳이 없다. 사실상 MFC가 RTTI를 제대로 사용하는 것이라고 할 수 있다. 나중에 기회가 된다면 MFC의 RTTI에 대해서도 소개하면 좋겠다는 생각이 든다.

출처 : 마이크로소프트웨어 11월호

제공 : 데이터 전문가 지식포털 DBguide.net