실시간 타입 정보 RTTI ( RunTime Type Information )
RTTI(RunTime Type Information)은 실시간 타입 정보를 알아내는 기술이다.
변수의 이름이나 클래스 계층 구조는 컴파일할 때만 필요하며 실행 파일로 번역한 후에는 사용되지 않는다.
이름은 구분을 위한 명칭일뿐이고 타입은 길이와 비트 구조를 해석하는데 참조할 뿐이다.
클래스도 마찬가지로 컴파일되면 멤버의 오프셋으로 참조하되 가상 함수가 있으면 가상테이블을 가지는 정도만 특이하다.
컴파일러는 이름으로 변수를 구분하지 않고 타입으로 액세스 방법을 결정하여 적절한 기계어 코드를 생성한다.
CPU는 타입을 인식하지 않으며 메모리에 있는 값을 지정한 길이만큼 읽고 쓸 뿐이다.
타입과 관련된 정보는 컴파일 중에만 사용되며 기계어로 바뀌면 남아 있지도 않는다.
컴파일이 끝나면 참조할 수 없고 참조할 필요도 없다.
그러나 클래스 계층을 다룰때는 가끔 타입 정보가 유용하게 사용되는 경우가 있다.
// 파일이름 : RTTI.cpp
#include <stdio.h>
class Parent
{
public:
virtual void PrintMe() { printf("I am Parent\n"); }
};
class Child : public Parent
{
private:
int num;
public:
Child(int anum) : num(anum) {}
virtual void PrintMe() { printf("I am Child\n"); }
void PrintNum() { printf("Hello Child = %d\n", num); }
};
void func(Parent *p)
{
p->PrintMe();
((Child*)p)->PrintNum();
}
int main()
{
Parent p;
Child c(5);
func(&c);
func(&p);
}
상속 관계의 두 클래스가 정의되어 있다.
Parent는 PrintMe 가상함수만 가지며 Parent로 부터 파생된 Child는 num 멤버 변수, 생성자와 PrintNum 멤버 함수를 가지고 PrintMe 가상 함수를 재정의한다.
func 함수는 Parent 파생 객체의 포인터를 전달받아 PrintMe 함수를 호출하고 Child일 경우 Child* 타입으로 강제 캐스팅하여 PrintNum 비가상함수도 호출한다.
main에서 각 타입의 두 객체 p와 c를 생성한 후 func 함수로 전달했다.
Child 객체를 전달했을 때는 두 함수 모두 정상적으로 동작하지만 Parent 타입의 객체를 전달할때는 PrintNum이 엉뚱한 값을 출력한다.
가상 함수인 PrintMe는 객체의 동적 타입에따라 정확하게 호출되지만 비가상함수인 PrintNum은 정적 타입을 따르므로 항상 Child::PrintNum이 호출된다.
Parent 객체는 num 멤버가 없으므로 쓰레기값이 출력된다.
이때 PrintNum 함수를 가상으로 바꾸면 컴파일은 잘 되지만 p가 가리키는 가상 테이블에는 PrintNum함수가 번지가 없어 다운된다.
부모 클래스는 자식이 정의한 가상 함수를 가지지 않는다.
아무리 가상으로 선언하더라도 없는 함수를 호출할 수는 없다.
문제의 원인은 func 함수가 전달받은 객체 p를 무조건 Child로 강제 캐스팅한 것인데 p가 진짜 Child 타입일때만 캐스팅해야한다.
void func(Parent *p)
{
p->PrintMe();
if(p가 Child 객체를 가리키면)
{
((Child*)p)->PrintNum();
}
}
p를 조사하여 이 포인터가 진짜 Child 객체를 가리키고 있을때만 캐스팅하면 안전하다.
그러나 포인터만 가지고 이 번지에 있는 객체가 무엇인지 알 수 있는 방법이 없다.
왜 불가능한지 단순 타입으로 바꿔서 생각해보자.
void sub(int *pi)
{
//pi가 누구를 가리키는지?
}
int a;
unsigned b;
sub(&a);
sub((int *)&b);
sub 함수는 정수형 포인터 pi를 전달받는다.
이 번지에 정수형 변수가 있는지 정수 배열의 한 요소를 가리키는지 구조체의 정수형 멤버인지 알 수 없다.
단지 이 번지에 정수가 있다는 것만 알고 있으며 * 연산자로 그 값을 읽거나 변경할 수 있을 뿐이다.
심지어 정수가 아닌 unsigned 형 변수의 번지를 캐스팅해서 넘겨도되고 정수라고 믿을 수 밖에 없다.
RTTI의 내부
실행 중에 객체의 타입을 조사하려면 타입 관련 정보가 클래스에 기록되어야한다.
이 정보는 가상 함수 테이블에 같이 기록되며 그러다 보니 RTTI는 가상 함수를 가진 클래스에 대해서만 사용할 수 있다.
단독 클래스나 비가상함수만 가진 클래스는 실행 중에 타입 정보를 조사할 필요가 전혀 없어 별다른 문제는 없다.
RTTI는 C++를 최초 설계할 때부터 포함된 것이 아니라 뒤늦게 추가된 확장 기능이어서 언어와의 통합성이 떨어진다.
타입 정보 저장을 위해 별도의 추가적인 정보가 필요해 용량이나 속도면에서 불이익이 있다.
이 기능이 항상 필요한 것도 아니어서 컴파일러 옵션으로 꼭 필요할때만 선택적으로 사용하도록 되어있다.
사용 여부가 옵션이라는 것은 필수적이지 않다는 뜻이며 RTTI가 도입되기 전에도 여러가지 방법으로 객체의 타입을 판별할 수 있는 기능을 자체적으로 만들어 사용하기도 했다.
원리는 비슷한데 클래스 어딘가에 자신이 누구라는 표식을 남겨 두면 된다.
// 파일이름 : CStyleRTTI.cpp
#include <stdio.h>
#include <string.h>
class Parent
{
public:
char classname[32];
Parent()
{
strcpy_s(classname, "Parent");
}
virtual void PrintMe()
{
printf("I am Parent\n");
}
};
class Child : public Parent
{
private:
int num;
public:
Child(int anum) : num(anum)
{
strcpy_s(classname, "Child");
}
virtual void PrintMe() { printf("I am Child\n"); }
void PrintNum() { printf("Hello Child = %d\n", num); }
};
void func(Parent* p)
{
p->PrintMe();
if (strcmp(p->classname, "Child") == 0)
{
((Child*)p)->PrintNum();
}
else
{
puts("이 객체는 num을 가지고 있지 않습니다.");
}
}
int main()
{
Parent p;
Child c(5);
func(&c);
func(&p);
}
Parent 클래스에 classname 문자열 배열을 선언하고 생성자에서 이 문자열에 "Parent"를 저장해 놓는다.
파생 클래스인 Child는 이 멤버를 상속받아 생성자에서 "Child"를 저장한다.
객체 스스로 자신의 이름표를 가지고 있으니 실행 중에 이 정보를 읽어 보면 누구인지 금방 알 수 있다.
포인터의 타입과 상관없이 객체 자체를 직접 참조하니 항상 정확하다.
이름표를 문자열로 붙여 비교가 번거로운데 숫자로 붙이면 속도를 높일 수 있다.
하지만 문자열은 클래스 이름과 일치시킬 수 있지만 숫자는 관리가 필요해 직관성이 떨어진다.
형식성을 더 따진다면 타입을 조사하는 멤버 함수를 가상으로 선언해서 함수로 비교할 수도 있고 == 연산자를 오버로딩하여 객체끼리 타입을 비교할 수도 있다.
RTTI도 사실 이 예제의 방식과 유사하게 동작한다.
typeid 연산자
RTTI 기능의 핵심은 객체의 타입을 조사하는 typeid 연산자이다.
피연산자로 객체나 객체의 포인터 또는 클래스 이름을 전달하면 타입에 대한 정보를 가지는 type_info 객체를 리턴한다.
type_info 클래스는 컴파일러 제작사마다 약간 차이가 있는데 비주얼 C++의 경우 typeinfo 헤더 파일에 다음과 같이 선언되어 있다.
class type_info
{
public:
type_info& operator=(type_info const&);
bool operator==(type_info const& _Other) const;
bool operator!=(type_info const& _Other) const;
bool before(type_info const& _Other) const;
const char* name() const;
const char* raw_name() const;
private:
mutable __std_type_info_data _Data;
};
name 함수는 클래스의 이름을 조사한다.
raw_name 함수는 장식명을 조사하는데 사람이 직접 읽기는 어려우며 비교에만 사용한다.
타입 정보끼리 대입, 비교하는 연산자가 정의되어 있는데 == 연산자로 원하는 타입인지 알아낸다.
다음 예제는 두 객체에 대해 실시간 타입 정보를 조사한다.
// 파일이름 : typeid.cpp
#include <stdio.h>
#include <typeinfo>
class Parent
{
public:
virtual void PrintMe() { printf("I am Parent\n"); }
};
class Child : public Parent
{
private:
int num;
public:
Child(int anum) : num(anum) {}
virtual void PrintMe() { printf("I am Child\n"); }
void PrintNum() { printf("Hello Child = %d\n", num); }
};
void func(){}
int main()
{
Parent P, * pP;
Child C(123), * pC;
pP = &P;
pC = &C;
int val;
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("pP = %s, *pP = %s, val = %s\n",
typeid(pP).name(), typeid(*pP).name(), typeid(val).name());
printf("func = %s\n", typeid(func).name()); // 함수의 타입 확인 (cdecl : 함수 규약)
}
main에서 두 개의 객체 P와 C를 생성하고 두 객체를 가리키는 포인터를 선언하여 대응 객체를 가리키도록 했다.
부모 타입의 포인터인 pP는 두 객체를 번갈아 가리킨다.
이 상태에서 typeid 연산자로 각 객체와 포인터, 포인터의 대상체에 대한 타입을 조사하여 이름을 출력했다.
앞 두 줄의 결과는 당연하고 상식적이다.
객체 P나 이 객체를 가리키는 포인터 pP나 포인터의 대상체 *pP는 모두 Parent 타입으로 조사된다.
차일드인 C, pC의 경우도 마찬가지이다.
세번째 줄의 결과를 주목해보자.
부모 타입인 pP는 자식 객체인 C를 가리킬 수 있는데 이 상태에서 pP의 타입과 *pP의 타입이 다르게 조사된다.
pP는 포인터 자체의 타입이므로 Parent * 라고 조사되며 현재 Child 객체를 가리키고 있으므로 대상체는 *pP는 Child로 조사된다.
부모 타입의 포인터가 자식 객체를 가리키고 있는 상황을 정확하게 구분한다.
RTTI 정보가 없다면 pP가 누구를 가리키는지 알 수 없지만 이 정보를 참조하는 typeid 연산자는 가리키는 객체를 정확히 구별해낸다.
이 기능을 사용하면 작성한 예제의 func 함수를 올바르게 작성할 수 있다.
typeinfo 헤더 파일을 포함하고 함수를 다음과 같이 수정하면 된다.
// 파일이름 : RTTI2.cpp
#include <stdio.h>
#include <typeinfo>
class Parent
{
public:
virtual void PrintMe() { printf("I am Parent\n"); }
};
class Child : public Parent
{
private:
int num;
public:
Child(int anum) : num(anum) {}
virtual void PrintMe() { printf("I am Child\n"); }
void PrintNum() { printf("Hello Child = %d\n", num); }
};
void func(Parent* p)
{
p->PrintMe();
if (typeid(*p) == typeid(Child))
{
((Child*)p)->PrintNum();
}
else
{
puts("이 객체는 num을가지고 있지 않습니다.");
}
}
int main()
{
Parent p;
Child c(5);
func(&c);
func(&p);
}
인수로 전달받은 p의 타입 정보와 Child의 타입 정보를 == 연산자로 비교해본다.
원하는 타입일때만 Child로 캐스팅하여 멤버 함수를 호출하고 그렇지 않으면 타입이 맞지 않다는 메세지를 대신 출력한다.
실행 중에 타입을 조사하여 적용하므로 항상 안전하다.
'C++ & C#' 카테고리의 다른 글
[C++] C++의 Casting (1) | 2024.01.25 |
---|---|
[C++] 스마트 포인터 (0) | 2022.03.07 |
[C#] 가비지 컬렉터 (0) | 2021.11.22 |
[C++/STL] algorithm헤더의 sort함수 (0) | 2021.11.09 |
[C++] Pointer에 관하여 (0) | 2021.10.26 |