본문 바로가기
Programming/MFC,C++

MFC에서 RTTI 의 실제 구현

by 기적 2021. 1. 28.

퍼옴 : anster.egloos.com/2172866

RTTI, Runtime Type Information 은 실행시간에 객체를 파악할 수 있도록 도와주는 시스템이다.

RTTI 를 이용하기 위해서는 가상함수를 위해 생성되는 vftbl 이 필요하다. 즉, 클래스 내에 가상 함수가 존재할때에만 RTTI 정보가 클래스에 포함 된다.

 

다형성을 위해 가상 함수를 이용하면 굳이 런타임에 객체의 타입을 알 필요가 없을지도 모른다. 하지만, 분명히 어떤 경우에는 설계상의 오류로 인해 RTTI를 사용할 수 밖에 없다. 자세한 내용은 아래의 포스트를 참조하라.
http://blog.naver.com/durenmarine?Redirect=Log&logNo=20017529755


C 구조체와 C++ 클래스 객체는 바이너리 호환성이 있는데, 클래스 내에 vftbl 이 생성되면, 호환성이 사라진다.
이러한 C와 C++ 사이의 후방 호환성 문제로 인해 RTTI 가 C++ 표준에 포함되는것에 대해 많은 논란이 있었다. (하지만 결국 포함 됐다.)

위에서 언급했듯이, RTTI 정보는 vftbl 에 저장되기 때문에 가상함수가 없는 비 다형성 클래스 객체는 RTTI 정보가 없다.
아래의 코드를 참조하면 다형성과 비 다형성을 가진 클래스가 typeid 를 이용할때 어떻게 차이 나는지 알 수 있다.

( 참조 - http://www.anycoding.com/bbs/board.php?bo_table=acProgram_Cpp&wr_id=3816 )


01: #include <iostream>
02: #include <typeinfo> //이 헤더가 포함되어 있어야 한다.
03:
04: #include <stdlib.h>
05: #include <tchar.h>
06:
07: class Parent {
08: //가상 함수가 없다면, vftbl 이 생성되지 않고 RTTI 정보는 포함되지 않는다.
09: virtual void a(void) { std::cout << "Parent" << std:: endl; };
10: };
11:
12: class Child1 : public Parent {
13: virtual void a(void) { std::cout << "Child1" << std:: endl; };
14: };
15:
16: class Child2 : public Parent {
17: virtual void a(void) { };
18: };
19:
20: class GrandChild : public Child1 {
21: virtual void a(void) { std::cout << "GrandChild" << std:: endl; }
22: };
23:
24: using namespace std;
25:
26: int main(void)
27: {
28: Parent* pcParent1 = new Child1();
29: Parent* pcParent2 = new Child2();
30:
31: //만약 virtual 함수가 없다면 클래스는 RTTI 정보를 가지지 못해
32: //'Parent' 라는 출력 결과가 나온다.
33: std::cout << typeid(*pcParent1).name() << std::endl; //Child1 출력
34: std::cout << typeid(*pcParent2).name() << std::endl; //Child2 출력
35:
36: GrandChild* pcGrandChild = new GrandChild();
37:
38: if ( typeid(*pcGrandChild) == typeid(Parent) ) //같지 않으므로 Equal은 출력되지 않음
39: std::cout << "Equal" << std::endl;
40:
41: _gettchar();
42:
43: return 0;
44: }
이러한 RTTI 는 꽤 시간이 흐른 후에서야 C++ 표준에 포함되었기 때문에, 표준에 포함되기 전까지 벤더들은 컴파일러 수준에서
RTTI 를 지원 해 왔다. MFC 도 그러한 경우인데, 이번시간에는 Visual Studio 에서 지원하는 RTTI (RTCI, Runtim Class Information 이라고도 한다.) 에 대해서 알아보자.



MFC 에서는 클래스의 동적인 생성과 확인을 위해 CObject 라는 추상 클래스를 구현 해 놓았다. 이 클래스는 직접 사용할 수 없으며, 상속 용도로만 이용 가능하다. 생성자가 protected 멤버에 존재하기 때문이다.

CObject 클래스를 상속한 후 클래스 내에 DECLARE_DYNCREATE 매크로를 이용해 필요한 구조체와 함수를 선언하고, IMPLEMENT_DYNCREATE 매크로를 이용해 구조체와 함수의 내용을 채운다. 아래는 MFC 를 이용해 만든 GUI 프로그램의 헤더파일과, CPP 파일의 일부이다.

01: //DemoView.h : CDemoView 클래스의 인터페이스
02: //
03:
04: #pragma once
05:
06:
07: class CDemoView : public CView
08: {
09: protected: //serialization에서만 만들어집니다.
10: CDemoView();
11: DECLARE_DYNCREATE(CDemoView)
12:
13:
14: //DemoView.cpp : CDemoView 클래스의 구현
15: //
16:
17: //CDemoView
18:
19: IMPLEMENT_DYNCREATE(CDemoView, CView)
클래스 내에 DECLARE_DYNCREATE 가 선언되고, CPP 파일 내에 IMPLEMENT 매크로로 구현한 것을 볼 수 있다. 그렇다면, 이러한 매크로들을 이용해 어떻게 클래스를 동적생성할 수 있을까? 동적 생성이 가능하다면, 클래스 이름만으로도 다음과 같이 생성할 수 있어야 한다.


1:
2: pcBaseClass = CREATE_MACRO(CHILD_CLASS)


이러한 일이 가능하려면, 클래스가 존재 하지 않아도, 클래스가 생성 가능 해야 한다. 즉, STATIC 멤버 함수를 이용하면 된다.
클래스 내에 클래스를 생성하는 STATIC 멤버 함수와 클래스의 이름을 확인할 STATIC 멤버 변수가 존재하면 된다.

자, 다음과 같은 클래스를 설계 해 보자. 위에서 언급했던 CObject 와 같이 생성자를 protected 멤버로 만들어
CObject* pcObject = new CObject 와 같은 방식으로 사용할 수 없으며, 상속용도로만 사용되는 클래스로써
내부적으로 RTTI를 구현하여 이 클래스를 상속받은 클래스들은 RTTI 기능을 사용할 수 있도록 해 보자.

클래스 이름은 CBaseObject 로 하겠다.

그리고 이 클래스 내부에 Static 멤버로 클래스의 이름과, 클래스를 생성해서 리턴 해 주는 변수와 함수를 만들고, 이것을 구조체로 묶자.
이러한 멤버 구성 요소들은 매크로로 선언할 수 있도록 함으로써 사용자가 CObject 를 상속받아도 RTTI 를 지원할지는 프로그래머의 선택으로 남겨두자.


01:
02: struct CRuntimeClass;
03:
04: class CBaseObject
05: {
06: public:
07: ~CBaseObject(void);
08:
09: public:
10: //파생 클래스에서 반드시 구현 할 필요가 없으므로 순수 가상 함수가 아니다.
11: virtual CRuntimeClass* GetRuntimeClass() const { return NULL; }
12: static CRuntimeClass classCBaseObject;
13:
14: protected:
15: //이 클래스를 직접 생성해서 사용 할 수 없다. 오직 상속으로만 사용 가능하다.
16: CBaseObject(void);
17: };
18:
19:
20: struct CRuntimeClass {
21: TCHAR m_pszClassName[21];
22: int m_nObjectSize;
23: CBaseObject* (*pfnCreateObject)();
24: CBaseObject* CreateObject();
25: };

자세한 내용은 주석을 보도록 하자. CRuntimeClass 의 3, 4번째 멤버 변수를 주목하자. 

CBaseObject* (*pfnCreateObject)(); 는 함수 포인터로써 MFC RTCI 의 핵심이다. 구현부를 보자.


01:
02: //BaseObject.cpp
03: #include "BaseObject.h"
04:
05: CBaseObject::CBaseObject(void)
06: { //생성자
07: }
08:
09: CBaseObject::~CBaseObject(void)
10: { //소멸자
11: }
12:
13: CBaseObject* CRuntimeClass::CreateObject()
14: { //Static 멤버인 CRuntimeClass 구조체 내부의 CreateObject 함수의 구현부
15: return (*pfnCreateObject)();
16: }
17:
18: CRuntimeClass CBaseObject::classCBaseObject = {
19: //Static 멤버인 CRuntimeClass 구조체 멤버의 값 할당
20: _T("CBaseObject"), sizeof(CBaseObject), NULL
21: };
CRuntimeClass 내부의 CreateObject 함수는 pfnCreateObject 함수 포인터를 호출한다. 만약, pfnCreateObject 함수가
CBaseObject 객체를 리턴한다고 가정 해 보자. (현재 코드상에서 pfnCreateObject 함수는 어느 함수도 가리키고 있지 않지만)

왜 이런 비 정상적인, 꼬인, 난해한 구조로 RTTI 가 MFC 에서 구현 되어있을까? 이유는 C++ 문법에 있다.

우리는 가장 처음에 RTTI 를 구현할때 클래스가 동적으로 생성되어야 한다고 했다. 따라서 런타임에 클래스가 생성될 수 있어야 하며
클래스의 인스턴스가 존재하지 않아도 클래스를 생성할 수 있어야 한다.

클래스가 존재하지 않아도 생성할 수 있으려면, 클래스 내부에 자신의 클래스를 new 로 생성해서 리턴하는 Static 멤버 변수가 존재하면 된다. 하지만 정적 멤버는 Virtual 일 수 없기에(This 포인터가 없다.)  CBaseObject 를 상속받은 클래스가 CreateObject 를 호출하면
자신의 클래스 인스턴스가 생성되서 리턴되는 것이 아니라, CBaseObject 클래스 인스턴스만 리턴된다.

Virtual Function, 가상함수를 사용할 수 없다. 따라서 MFC 의 설계자들은 CBaseObject 를 상속받은 하위 클래스 마다 존재하는 Static 멤버 함수 CreateObject를 호출할 수 있도록 pfnCreateObject 를 두어, 이 함수 포인터가 하위 클래스의 CreateObject 를 가리키 도록 해 두었다.

이 이론의 실제 구현인 CAlpha 클래스를 보자.



01:
02:
03: //CAlpha.h
04:
05: class CAlpha : public CBaseObject
06: {
07: public:
08: virtual CRuntimeClass* GetRuntimeClass() const { return &classCAlpha; }
09:
10: static CRuntimeClass classCAlpha;
11: static CBaseObject* CreateObject();
12:
13: protected:
14: //이 클래스를 직접 생성해서 사용 할 수 없다. 동적 생성을 이용하라.
15: CAlpha();
16: };
17:
18: //CAlpha.cpp
19:
20: CAlpha::CAlpha()
21: {
22:
23: }
24:
25: CRuntimeClass CAlpha::classCAlpha = {
26: _T("CAlpha"), sizeof(CAlpha), CAlpha::CreateObject
27: };
28:
29: CBaseObject* CAlpha::CreateObject() {
30: return new CAlpha;
31: }
32:


CAlpha::classCAlpha.pfnCreateObject 는 CAlpha::CreateObject 를 호출하고, CreateObject 는 CAlpha 클래스 인스턴스를 생성해서 리턴한다.

자, 그러면 이 클래스를 사용하는 방법을 확인 해 보자.


01:
02:
03: //Main.cpp
04:
05: int main(void)
06: {
07:
08: CRuntimeClass* pcRTC = &CAlpha::classCAlpha;
09: CBaseObject* pcBaseObject = pcRTC->CreateObject();
10:
11: return 0;
12: }
13:
14:


여기서 일정부분을 매크로로 치환하면, 클래스 이름만으로도 객체를 생성할 수 있다.


01:
02:
03: // MACRO
04:
05: #define GET_RUNTIME_CLASS(class_name) (&class_name::class##class_name);
06: #define DECLARE_RUNTIME_CLASS(class_name) virtual CRuntimeClass* GetRuntimeClass() const \
07: { return &class##class_name; }
08:
09: #define DECLARE_DYNAMIC(class_name) static CRuntimeClass class##class_name;
10: #define DECLARE_DYNCREATE(class_name) static CBaseObject* CreateObject();
11:
12: #define IMPLEMENT_DYNAMIC(class_name) \
13: CRuntimeClass class_name::class##class_name = { _T(#class_name), sizeof(class_name), class_name::CreateObject };
14: #define IMPLEMENT_DYNCREATE(class_name) \
15: CBaseObject* class_name::CreateObject() { return new class_name; }
16:
17:
18: // CBeta.h
19:
20: class CBeta : public CBaseObject
21: {
22: public:
23:
24: DECLARE_RUNTIME_CLASS(CBeta)
25: DECLARE_DYNAMIC(CBeta)
26: DECLARE_DYNCREATE(CBeta)
27:
28: protected:
29: // 이 객체는 직접 생성할 수 없다. 동적으로만 생성 가능하다.
30: CBeta() {}
31:
32: };
33:
34: // CBeta.cpp
35:
36: IMPLEMENT_DYNAMIC(CBeta)
37: IMPLEMENT_DYNCREATE(CBeta)
38:
39: // Main.cpp
40:
41: int main(void)
42: {
43:
44: CRuntimeClass* pRTCBeta = GET_RUNTIME_CLASS(CBeta)
45: CBaseObject* pcBaseObject = pRTCBeta->CreateObject();
46:
47: return 0;
48: }



 

참고 - http://rhea.pe.kr/260 

       - http://blog.naver.com/PostView.nhn?blogId=carfedm&logNo=140070752898

       - http://www.anycoding.com/bbs/board.php?bo_table=acProgram_Cpp&wr_id=3816

       - [MFC 구조와 원리] http://www.google.co.kr/url?sa=t&source=web&cd=2&ved=0CDcQFjAB&url=http%3A%2F%2Fwww.hanb.co.kr%2Fbook%2Flook.html%3Fisbn%3D89-7914-324-9&ei=_RgxTpiYO6P2mAXfhOnMCQ&usg=AFQjCNH-ofeL_KklUc94lrkka1nwi-uCGg

댓글