RTTI에 대한 이해

이번에는 C++의 RTTI (Runtime Type Indentification) 에 대해 소개하고자 합니다.

다형성(Polymorphism)을 갖는 C++ class 들(하나 이상의 가상함수를 포함)에 대해 적용 가능합니다.

클래스 다형성은 가상함수에 의해 구현되므로, RTTI는 가상함수를 갖는 클래스의 상속관계에서만 사용가능합니다.

아마도 이 주제를 찾아 보는 사용자라면 알고 있겠지만, C++ 포인터와 클래스 상속에 대한 이해가 요구됩니다.

RTTI

  • 런타임에 상속 관계를 갖는 클래스들의 객체 타입을 체크하는 메커니즘.
  • dynamic_cast or typeid 연산자로 구성.
  • 가상함수(Virtual Fucntion)를 가지는 클래스들만 사용 가능. 


왜 RTTI 가 필요한지 코드를 통해 살펴보겠습니다.

안전하지 않은 타입 캐스팅

#include <iostream>

using namespace std;

class CWnd
{
    virtual void draw() { cout << "CWnd" << endl; }
};

class CView:public CWnd
{
public:
    int m_view;
    void draw() { cout << "View" << endl; }        
};

class CTreeView :public CView
{
public:
    int m_treeview;
    void draw() { cout << "Tree View" << endl; }
};

int main()
{
    // 부모클래스 포인터는 자식 클래스 객체를 가르킬 수 있다.
    CWnd *pw = new CWnd;
    CWnd *pv = new CView;
    CWnd *pt = new CTreeView;

    // 느슨한 형변환
    CTreeView    *p1 = (CTreeView*)pt; // (안전 O)
    CTreeView    *p2 = (CTreeView*)pw; // (안전 X)
    CView        *p3 = (CTreeView*)pt; // (안전 O)

    // pw는 자식클래스의 변수가 없음(안전 X)
    p2->m_treeview = 0;    

    delete pw;
    delete pv;
    delete pt;
}

상속관계를 갖는 3개의 클래스 CWnd, CView, CTreeView 가 존재합니다.

MFC의 클래스와 이름만 같을 뿐 별 의미없는 단순 클래스 입니다.

[예제 클래스 상속구조]

부모 클래스 포인터로 아래 클래스의 객체들을 생성합니다.
CWnd *pw = new CWnd;
CWnd *pv = new CView;
CWnd *pt = new CTreeView;

부모 클래스 포인터는 자식 객체를 가르킬 수 있으므로, 현재까지 아무 문제도 없습니다.

이제 형변환을 해보겠습니다.
CTreeView *p1 = (CTreeView*)pt; // (안전 O)
CTreeView *p2 = (CTreeView*)pw; // (안전 X)
CView  *p3 = (CTreeView*)pt; // (안전 O)

첫번째는 CtreeView 형 포인터가 CTreeView형 객체를 지시하게 되므로 안전(O)

두번째는 CWnd형 객체의 주소를 자식(CTreeView) 클래스 포인터로 대입되므로 안전(X)

세번째는 CTreeView 형 포인터가 부모(CView) 클래스 포인터로 대입되므로 안전(O)

두번째 형변환은 문제를 야기하지만, 컴파일 에러를 유발하지 않습니다.
p2->m_treeview = 0;
위 코드는 런타임에 문제를 발생시킵니다.

p2 포인터는 실제 부모 클래스(CWnd)를 가르키는 포인터이기 때문에, CTreeView의 m_treeview라는 변수를 갖고 있지 않기 때문입니다.

해결방안 1 (dynamic_cast 연산자)

int main()
{
    // 부모클래스 포인터는 자식 클래스 객체를 가르킬 수 있다.
    CWnd *pw = new CWnd;
    CWnd *pv = new CView;
    CWnd *pt = new CTreeView;


    // RTTI(Runtime Type Identification) Case 1
    CTreeView    *p11 = dynamic_cast<CTreeView*>(pt);
    CTreeView    *p22 = dynamic_cast<CTreeView*>(pw);
    CView        *p33 = dynamic_cast<CTreeView*>(pt);

    if (p11)
        cout << "p11 is safe" << endl;
    if (p22)
        cout << "p22 is save" << endl;
    if (p33)
        cout << "p33 is safe" << endl;

    if(pw)
        delete pw;
    if(pv)
        delete pv;
    if(pt)
        delete pt;
}

dynamic_cast<클래스*>(포인터) 로 형변환을 시도합니다.

이 연산자는 포인터가 지시하는 객체의 클래스 타입이 무엇인지 알려주진 않지만, 형변환이 안전한지 체크해 *p22의 경우 null 을 리턴해 형변환이 안전하지 않다고 알려줍니다.

위 코드의 수행 결과는 아래와 같습니다.

p22의 포인터가 null 이므로 출력되지 않습니다.

즉 부모객체를 가리키는 포인터를 자식객체 타입의 포인터에 대입되지 않도록 합니다.


해결방안 2 (typeid 연산자)

#include <typeinfo>
int main()
{
    // 부모클래스 포인터는 자식 클래스 객체를 가르킬 수 있다.
    CWnd *pw = new CWnd;
    CWnd *pv = new CView;
    CWnd *pt = new CTreeView;

    // 느슨한 형변환
    CTreeView    *p1 = (CTreeView*)pt; // (안전 O)
    CTreeView    *p2 = (CTreeView*)pw; // (안전 X)
    CView        *p3 = (CTreeView*)pt; // (안전 O)

    // pw는 자식클래스의 변수가 없음(체크 후 사용)
    if (typeid(CTreeView) == typeid(*p2))
        p2->m_treeview = 0;
    else
        cout << typeid(*p2).name() << endl;

    delete pw;
    delete pv;
    delete pt;
}
typeid 연산자를 이용해 포인터가 가르키는 곳의 타입(*p2)이 CTreeView와 일치하는지 판단해 형변환을 시도합니다.

typeid(*p2).name() 은 클래스의 타입을 문자열로 리턴하며 결과는 아래와 같습니다.


감사합니다.

댓글

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기