상세 컨텐츠

본문 제목

C++ RTTI

C++/C++98

by deulee 2023. 8. 9. 10:48

본문

1. 실시간 타입 정보

"RTTI(RunTime Type Information)"은 실시간 타입 정보를 의미한다.

 

일반적으로 변수의 이름이나 타입은 컴파일러가 컴파일을 하는 동안에만 필요한 것이지 이진 파일로 변역되고 나면 이 정보들은 필요가 없어진다.

 

변수의 타입은 읽어들일 길이와 비트를 해석하는 정보로만 사용되며 기계어 수준에서는 길이와 비트해석 방법에 따라 생성되는 기계어 코드가 달라진다.

 

클래스 또한 기계어로 바꾸면 구조체랑 별반 다를게 없다만 "가상 함수"가 존재할 시 "vtable"을 가리키는 "vptr" 하나만 추가된다. 또한, 멤버 함수도 첫 번째 인수가 "this"로 고정되어 있는 것 빼고는 일반 함수와 다를바가 없다.

 

즉, CPU는 타입을 인식하지 않고 메모리에 있는 값을 지정한 길이만큼 읽고 쓰고 할 뿐이다.

 

그럼 RTTI는 왜 필요할까?

 

#include <iostream>

class Parent
{
public:
	virtual void printMe()
	{
		std::cout << "I am Parent\n";
	}
};

class Child : public Parent
{
private:
	int num;
public:
	Child(int an) : num(an) {}
	virtual void printMe()
	{
		std::cout << "I am Child\n";
	}
	void printNum()
	{
		std::cout << "Hello Child = " << num << std::endl;
	}
};

void func(Parent* ptr)
{
	ptr->printMe();
	((Child *)ptr)->printNum();
}

int main(void)
{
	Parent p;
	Child c(5);

	func(&c);
	func(&p);
	return 0;
}

출력 결과

위의 예시를 보도록 하자.

 

이전에 다형성이라는 개념을 배웠을 것이다. 그곳에서 "부모 객체는 자식 객체를 가리킬 수 있다"는 것도 배웠을 것이다.

 

컴파일상으로는 "func()" 함수가 전혀 문제가 되지 않지만 그 본체에 "(Child *)"로 형 변환을 한 뒤 Child의 멤버 함수인 "printNum()"를 호출하는 장면이 보일 것이다.

 

이 전의 "printMe()"는 둘 다 가상 함수로 작성되어 있어 동적 바인딩으로 인하여 "ptr"이 가리키는 객체의 타입 즉, 동적 타입에 맞게 함수가 실행된 걸 볼 수 있다.

 

하지만 그 다음 줄의 경우 형 변환이 일어나고 "printNum()" 함수는 비가상 함수이기 때문에 "정적 바인딩"으로 인하여 포인터의 타입 즉, 정적 타입에 따라 함수가 실행되게 된다.

 

이때, "func(&p)"로 호출된 즉, 부모 객체로 호출된 함수 기준으로 본다면 "printNum" 함수 내에서 읽는 "num" 멤버에 대한 오프셋 위치(this->num)를 무조건 읽는 것이며 부모 객체 "p"에는 num 멤버가 없기 때문에 이 번지에 제대로 된 값이 있을리가 없으므로 어뚱한 값이 출력된다.

 

만약 "printNum()"을 가상 함수로 바꾸면 어떻게 될까?

 

컴파일은 되지만 바로 segmentation fault가 뜬다.

 

왜냐하면 가상 함수로 바꾼다는 것은 동적 타입에 맞는 "vptr"을 이용하여 "vtalbe"에서 해당 함수를 찾는 것인데 부모 객체에는 "printMe"라는 함수가 없기 때문이다.

 

그렇기 때문에 의도대로 작성하려면 다음 처럼 작성하는 것이 좋다.

void func(Parent* ptr)
{
	ptr->printMe();
	if (ptr가 Child를 가리킨다면) {
		((Child *)ptr)->printNum();
	}
}

하지만 포인터가 가리키고 있는 객체가 어떤 타입인지 어떻게 알 수 있을까?

 

사실 일반적으로는 불가능하다. 그래서 RTTI가 필요해진 것이다. RTTI는 가상 함수가 있는 클래스에 대해서만 동작하는데 그 이유는 클래스의 타입 관련 정보가 vtable에 같이 저장되기 때문이다.

 

사실 가상 함수가 없는 클래스는 정적으로만 호출되기 때문에 RTTI가 필요가 없다.


2. RTTI 직접 만들어보기

#include <iostream>
#include <cstring>

class Parent
{
protected:
	const char* Name;
public:
	Parent()
	{
		Name = "Parent";
	}
	virtual void printMe()	
	{
		std::cout << "I am Parent" << std::endl;
	}
	virtual const char* getName()
	{
		return Name;
	}
};

class Child : public Parent
{
private:
	int num;
public:
	Child(int an) : num(an)
	{
		Name = "Child";
	}
	virtual void printMe()
	{
		std::cout << "I am Child" << std::endl;
	}
	virtual const char* getName()
	{
		return Name;
	}
	void printNum()
	{
		std::cout << "Hello Child = " << num << std::endl;
	}

};

void func(Parent* ptr)
{
	ptr->printMe();
	if (strcmp(ptr->getName(), "Child") == 0)
		((Child *)ptr)->printNum();
	else
		std::cout << "이 객체는 num을 가지고 있지 않다." << std::endl;
};

int main(void)
{
	Parent p;
	Child c(1);

	func(&c);
	func(&p);
	return 0;
}

요런식으로 최상위 클래스에서 상수 포인터 Name을 가짐으로써 각 객체의 타입을 스스로 초기화한다.

 

그리고 가상 함수로 GetName 함수를 정의함으로써 동적 타입에 맞는 GetName이 호출되며 각각에 맞는 타입이 리턴될 것이다. 그리고 이를 비교하면 충분히 RTTI를 흉내낼 수 있다.

 

하지만 이렇게 한다면 효율적이지 못하며 빠르지도 않다.

 

이렇게 할 경우 객체(인스턴스)마다 하나씩 타입 정보를 가지고 있어서 용량적으로 낭비가 심하다.

 

물론 정적 멤버를 사용하면 클래스마다 하나씩의 타입 정보를 생성할 수 있지만 정적 멤버는 상속되지 않기 때문에 각 파생 클래스마다 고유의 멤버를 따로따로 만들어야하는 번거러움이 있다.


3. typeid 연산자

그래서 C++은 "typeid"라는 연산자를 사용한다.

 

인수  - 클래스의 이름, 객체 또는 객체를 가리키는 포인터

리턴 타입 - const type_info &

 

type_info 클래스의 정보

class type_info {
public:
     virtual ~type_info();
    int operator==(const type_info& rhs) const;
     int operator!=(const type_info& rhs) const;
     int before(const type_info& rhs) const;
     const char* name() const;
     const char* raw_name() const;
private:
    void *_m_data;
    char _m_d_name[1];
    type_info(const type_info& rhs);
    type_info& operator=(const type_info& rhs);
};

위의 객체는 연산자들이 오버로딩되어 있는데 통상 == 사용해도 원하는 타입인지 아닌지를 알 수 있다.

 

이때 피연산자가 NULL 포인터일 경우 bad_typeid 예외를 일으킨다.

 

사용 예시

#include <iostream>
#include <typeinfo>

class Parent
{
public:
	virtual void printMe()
	{
		std::cout << "I am Parent" << std::endl;
	}
};

class Child : public Parent
{
private:
	int num;
public:
	Child(int an) : num(an)
	{}
	virtual void printMe()
	{
		std::cout << "I am Child" << std::endl;
	}
	void printNum()
	{
		std::cout << "Hello Child = " << num << std::endl;
	}
};

int main(void)
{
	Parent p;
	Child c(1);

	Parent* pP = &p;
	Child* pC = &c;

	printf("p= %s, pP = %s, *pP = %s\n",
		typeid(p).name(), typeid(pP).name(), typeid(*pP).name());
	printf("c= %s, pC = %s, *pC = %s\n",
		typeid(c).name(), typeid(pC).name(), typeid(*pC).name());
	
	pP = &c;
	printf("p= %s, pP = %s, *pP = %s\n",
		typeid(p).name(), typeid(pP).name(), typeid(*pP).name());
	return 0;
}

출력 결과

 

그래서 다음처럼 맨 처음의 예제를 바꿀 수 있다.

void func(Parent* ptr)
{
	ptr->printMe();
	if (typeid(*ptr) == typeid(Child))
		((Child *)p)->printNum();
	else
		std::cout << "이 객체는 num을 가지고 있지 않다." << std::endl;
}

"type_info"는 "vtable"을 통해 각 클래스마다 하나씩 생성된다. 그리고 이것은 컴파일러가 만든 것이기 때문에 직접 만드는 것보다 훨씬 효율이 좋다.


출처

http://www.soen.kr/lecture/ccpp/cpplec.htm

 

C/C++ 강좌

 

www.soen.kr

 

 

'C++ > C++98' 카테고리의 다른 글

C++ 멤버 포인터 연산자  (0) 2023.08.09
C++ Cast 연산자  (0) 2023.08.09
C++ 예외의 비용  (0) 2023.08.09
C++ 예외 지정  (0) 2023.08.09
C++ 표준 예외  (0) 2023.08.08

관련글 더보기