본문 바로가기

고급C,C++

RTTI(runtime type identification)

RTTI(runtime type identification) - 실행 시간 데이터형 정보
어떤 C++ 시스템들은 RTTI 기능을 켜거나 끌 수 있는 설정을 가지고 있다.
RTTI의 목적은, 프로그램이 실행 도중에 객체의 데이터형을 결정하는 표준 방법을 제공하는 것이다.

RTTI의 목적
하나의 공통 기초 클래스로부터 상속된 클래스 계층이 있다고 가정하자. 이 클래스 계층에 속해 있는 클래스들의 어떤 객체를 기초 클래스 포인터가 지시하도록 설정할 수 있다. 그 다음에, 어떤 정보를 처리한 후 이들 클래스들 중에서 어느 하나를 선택하고, 그 클래스형의 한 객체를 생성하고, 기초 클래스 포인터에 대입하기 위해 그 객체의 주소를 리턴하는 함수를 호출한다. 그 포인터가 지시하는 객체의 종류가 무엇인지 어떻게 알 수 있을까?
 이 질문에 대답하기 전에, 그 데이텨헝을 알고 싶은 이유에 대해 생각할 필요가 있다. 어쩌면 어떤 클래스 메서드의 정화갛ㄴ 버전을 호출하고 싶기 때문일 수도 있다. 그러한 경우에, 그 함수가 클래스 계층의 모든 멤버들에 의해 처리되는 가상함수라면, 굳이 그 객체형을 알 필요가 없다. 그러나 파생 객체가 상속되지 않은 메서드를 가지고 있을 수도 있다. 그러한 경우에는 일부 객체들만이 그 메서드를 사용할 수 있다. 또는 디버깅 목적으로, 어떤 종류의 객체들이 생성되는지 추적하고 싶을 수도 있다. RTTI는 마지막 두 경우에 해결책을 제시한다.

RTTI의 동작 방식 - C++는 RTTI를 지원하는 세 가지 요소를 가지고 있다.
  • dynamic_cast 연산자는, 가능하다면, 기초 클래스형을 지시하는 포인터로부터 파생 클래스형을 지시하는 포인터를 생성한다. 가능하지 않다면, 널 포인터인 0을 리턴한다. 
  • typeid 연산자는 어떤 객체의 정확한 데이터형을 식별하는 하나의 값을 리턴한다.
  • type_info 구조체는 특별한 데이터형에 대한 정보를 저장한다.
    RTTI는 가상함수를 가지고 있는 클래스에 대해서만 사용할 수 있다.



dynamic_cast 연산자
  • dynamic_cast<Type *>(pt)
    • 포인터 pt에 의해 지시되는 객체(*pt)가 Type형이거나, Type형으로부터 직접 또는 간접적으로 파생된 객체일 경우에, 포인터 pt를 Type *형의 포인터로 변환한다. 그렇지 않을 경우에, 이 표현은 널 포인터인 0으로 평가된다.

//rtti1.cpp -- RTTI dynamic_cast 연산자를 사용한다.
#include <iostream>
#include <cstdlib>
#include <ctime>


using std::cout;

class Grand
{
private:
int hold;
public:
Grand(int h = 0) : hold(h){}
virtual void Speak() const {cout<<"나는 Grand 클래스이다.\n";}
virtual int Value() const {return hold;}
};

class Superb : public Grand
{
public:
Superb(int h = 0) : Grand(h){}
void Speak() const {cout<<"나는 Superb클래스이다!!\n";}
virtual void Say() const {
cout<<"내가 가지고 있는 Superb 값은" << Value()<<"이다.\n";
}

};

class Manificent : public Superb
{
private:
char ch;
public:
Manificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}
void Speak() const {cout << "나는 Manificent 클래스이다\n";}
void Say() const {
cout<<"내가 가지고 있는 Manificent 값은" << Value()<<"이다.\n";
}
};

Grand * GetOne();

int main()
{
std::srand(std::time(0));
Grand * pg;
Superb * ps;
for(int i = 0; i<5; i++)
{
pg = GetOne();
pg->Speak();
if(ps = dynamic_cast<Superb*>(pg))
ps->Say();
}

std::cin.get();

return 0;
}




Grand * GetOne()
{
Grand * p;
switch(std::rand() % 3)
{
case 0: p = new Grand(std::rand() % 100);
break;
case 1: p = new Superb(std::rand() % 100);
break;
case 2: p = new Manificent(std::rand() % 100, 'A' + std::rand() % 26);
break;
}
return p;

}
//소스 설명 
 Grand, Superb, Magnificent라는 이름의 세 클래스를 정의한다. Grand클래스는 가상함수 Speak()를 정의한다. Superb 클래스와 Magnificant 클래스가 Speak() 함수를 각각 다시 정의한다. Superb 클래스는 가상 함수 Say()를 정의한다 Magnificant 클래스가 Say() 함수를 다시 정의한다 
 프로그램은 이들 세 클래스형 중의 어느 하나로 객체를 임의로 생성하고 초기화하는 GetOne()함수를 정의한다. 그리고 GetOne() 함수는 그 객체의 주소를 Grand * 형의 포인터로 리턴한다. 루프는 이 포인터를 pg라는 Grand* 형의 변수에 대입한다. 그러고 나서 pg를 사용하여 Speak() 함수를 호출한다. 이 함수는 가상 함수이기 때문에, 그 코드는 지시괴는 객체에 적절한 버전의 Speak() 함수를 정확하게 호출한다. 
 그러나 Say() 함수의 호출은 이와 같은 접근 방식을 사용할 수 없다. 그 이유는, Say() 함수가 Grand 클래스에는 정의되어 있지 않기 때문이다. 그러나 dynamic_cast 연산자를 사용하여, pg가 Superb를 지시하는 포인터형으로 변환 될 수 있는지 알아 볼 수 있다. 그 객체가 Superb형 또는 Magnificant형 둘 중 어느 하나일 때, 이것은 참이 된다. 어느 경우에나 Say() 함수를 안전하게 호출 할 수 있다.
 if(ps = dynimic_cast<Superb *>(pg))
ps->Say();


dynamic_cast를 참조와 함께 사용할 수도 있다. 사용 방법은 약간 다르다. 널 포인터에 해당하는 참조값은 존재하지 않는다. 그래서 실패를 나타내는 데 사용할 수 있는 틀별한 참조값이 없다. 그 대신에 부적절한 요청이 제시될 때 dynamic_cast는 bad_cast 형의 예외를 발생시킨다. 이 예외는 exception 클래스로부터 파생된 것이며, typeinfo 헤더파일에 정의되어 있다. 그래서 dynamic_cast 연산자를 다은과 같이 사용할 수 있다. 여기서 rg는 Grand 객체에 대한 참조이다.
#include <typeinfo> //bad_cast를 위해 
...
try
{
Superb & rs = dynamic_cast <Superb &>(rg);
...
}
catch(bad_cast & bc)
{
...
}


typeid 연산자와 type_info 클래스
typeid 연산자를 사용하여 두 객체의 데이터형이 같은지 결정할 수 있다. sizeof와 비슷하게, typeid 연산자는 두 종류의 전달인자를 받아들인다.
클래스 이름
객체로 평가되는 식
 typeid 연산자는 type_info 객체에 대한 참조를 리턴한다. type_info는 typeinfo 헤더파일에  정의되어 있는 클래스이다. type_info 클래스는 데이터형을 비교하는 데 사용할 수 있도록 == 와 != 연산자를 오버로딩한다. ( typeid(Magnificent) == typeid(*pg))
 pg 가 Magnificent 객체를 지시하는 포인터면 true로 평가된다. pg가 널 포인터면, 프로그램은 bad_typeid 예외를 발생시킨다. 이 예외 데이터형은 exception 클래스로부터 파생된 것이고, typeinfo 헤더파일에 선언되어 있다. 
 type_info 클래스의 구현은 업체마다 다르다. 그러나 일반적으로 클래스의 이름인, 시스템에 따른 문자열을 리턴하는 name() 멤버를 포함한다. 예를 들어 다은과 같은 명령문은 
cout<<"지금"<<typeid(*pg).name()<<"형을 처리하고 있습니다.\n";
포인터 pg가 지시하는 객체의 클래스 이름을 출력한다.