RTTI 란?
Run Time Type Information의 약자로 프로그램 실행 중에 개체의 형식이 결정될 수 있도록 하는 메커니즘이다. 다시 말하면 실행중 포인터가 가르키는 객체의 타입을 알 수 있게 해주는 하나의 방법이라고 보면 된다.
기본적으로 RTTI가 필요한 이유는 A 타입에서 B 타입으로 변경할 때 정보가 필요하기 때문이다.
컴파일시간에 타입 변환이 이루어진다면 굳이 RTTI가 필요없다. 컴파일 단계에서 충분히 알 수 있고 특정 타입으로 확정할 수 있기 때문이다.
일반적인 상속 관계에서 발생하는 타입 관계는 대부분 컴파일 시간에 해석되나, virtual 클래스로 상속받는 경우에는 컴파일 시간에 추적이 불가능하다.
virtual 클래스로부터 상속이 하나라도 존재하면 RTTI를 사용하고 있다고 보면 된다.
dynamic_cast의 경우 RTTI에 의존적인데 초기단계 분석에서는 static_cast 분석을 사용하고, 불가능하면 RTTI를 사용하는 구조로 설계되었다. 하지만 의존적이라 하여도 dynamic_cast를 사용한다고 해서 모두 RTTI와 직접 연결되는 것은 아니다.
RTTI와 다중 상속은 크게 관련이 없다. 오히려 RTTI와 가상 상속이 직접적인 관계가 존재한다.
스펙 시트상으로는 RTTI를 가상 함수 테이블에 남겨야 한다는 조항은 없으나, const 형태로 정보하라는 내용이 존재한다. 어떤 RTTI 정보를 사용해야 할지는 포인터 상수로 남겨야하고 구현 컴파일러 대부분이 가상함수 테이블에 그 정보를 남기는 경향이 있다.
RTTI는 원래부터 있던 메커니즘이 아니다. 왜냐하면 C와의 후방 호환성을 유지하며 C의 효율성을 해칠 수 있다는 우려때문이었다.
다중상속이 추가되며 RTTI에 대한 이슈가 다시 대두되었다는데, 그때도 가상함수라는 대안이 있었지만, 정적 타입체크와 가상함수들로는 해결할 수 없는 문제점들이 제시되었고 결국 C++ 표준화 위원회가 1998년 표준에 RTTI 추가를 승인했다.
그 이전엔 92년도쯤에 등장한 MFC의 RTTI가 CRuntimeClass와 몇가지 매크로등을 통해 구현되었었다. 현재까지도 사용되고 있다고 한다.
#include <iostream>
class Base {};
class MAN : public Base {};
class WOMAN : public Base {};
int main()
{
Base* base_m = new MAN();
Base* base_w = new WOMAN();
return 0;
}
RTTI가 없던 시절에는 base_m이나 base_w가 런타임에서 어떤 하위 클래스를 가르키고 있는지 알 수 없었다. 그래서 런타임일때, 타입을 알기 위해 개발된 메커니즘이 RTTI이다.
RTTI는 다형성을 가진 클래스 즉, C++ 기준에서 가상함수가 존재하는 클래스에서 사용할 수 있으며, 그에 대한 이유는 RTTI 정보가 가상함수 테이블인 vtable 에 첫번째 요소에 type_info를 참조하는 주소를 저장하기 때문이다. 그래서 가상함수가 사용된 클래에 한해서만 정보가 유지된다. 이에 대한 비용으로 type_info가 객체 메모리에 추가되어 vtable의 저장 공간이 소모된다.
RTTI는 일반적인 클래스 객체에 두게되면 C와의 하위호환성을 포기해야 한다.
C의 구조체와 C++의 클래스 객체는 바이너리 호환성을 지니기 때문에 C에서 클래스 객체에 접근이 가능한데, C++ 클래스가 멤버중 virtual 선언된 멤버를 가지게 되면 그 멤버에는 접근이 불가능해진다.
RTTI 기술을 이용해서 데이터 타입을 얻어올 수가 있는데 이때 사용하는 것이 typeid 연산자다. typeid 연산자는 <typeinfo> 헤더에 존재한다.
typeid(변수)
typeid(데이터 타입)
typeid 연산자의 결과로는 const std::type_info& 로 반환된다. std::type_info는 타입의 정보를 가지고 있는 클래스다. 이거는 typeid의 반환형으로만 사용이 가능하다. name()이라는 멤버 함수를 통해서 타입의 이름을 얻을 수 있다.
가상함수가 없는 경우
#include <iostream>
class Base { void C() {} };
class MAN : public Base { void A() {} };
class WOMAN : public Base { void B() {} };
int main()
{
Base* base_m = new MAN();
Base* base_w = new WOMAN();
return 0;
}
아까의 예제에 Base에는 C()를 MAN과 WOMAN 클래스에 각각 무의미한 A(), B() 함수를 추가했다. 결과는 예상한 대로 가상함수 테이블이 없어 RTTI 정보가 없기때문에 포인터의 타입을 가져온다.
virtual로 선언되지 않아서 단형성인 클래스가 되었고, 이는 곧 가상함수 테이블이 없으면 RTTI 정보를 갖지 못하는 한계에 부딪힌다.
가상함수가 있는 경우
위의 예제에 Base와 자식 클래스에 GO 라는 가상함수를 선언해두었다.
#include <iostream>
class Base
{
void C() {}
virtual void GO() {}
};
class MAN : public Base
{
void A() {}
virtual void GO() {}
};
class WOMAN : public Base
{
void B() {}
virtual void GO() {}
};
int main()
{
Base* base_m = new MAN();
Base* base_w = new WOMAN();
std::cout << typeid(*base_w).name() << std::endl;
std::cout << typeid(*base_m).name() << std::endl;
return 0;
}
결과는 가상함수가 생기며 가상함수 테이블이 생성되어 RTTI 정보가 제공되었다.
보이는 것 처럼 WOMAN과 MAN이 잘 출력되었다. 본인이 누구인지 확실히 알 수 있게 된것이다.
dynamic_cast 또한 C++ RTTI에 의존하기 때문에, dynamic_cast를 진행할 클래스들의 상속 관계 속에서도 하나 이상의 가상함수를 가지고있어야 하며, 컴파일러에서 RTTI 기능을 끄지 말아야 한다.
RTTI는 세가지 구성요소들로 이루어진다.
- 연산자 typeid : 개체의 정확한 형식을 식별하는데 사용
- 연산자 dynamic_cast : 다형 형식을 변환하는데 사용
- 클래스 std::type_info : typeid 연산자에서 반환된 형식 정보를 저장하는 것에 사용
'프로그래밍 언어 > C++' 카테고리의 다른 글
C++ typename의 두 가지 의미 (0) | 2022.06.16 |
---|---|
C++ r-value && (임시 객체) / l-value & (고유 객체, 주소값) (0) | 2022.06.15 |
C++ 출력(std::format)과 for-range loop 꿀팁 (0) | 2022.06.14 |
C++ RVO NRVO 반환값 최적화 / Copy Elision(복사 생략) (0) | 2022.06.14 |
C++ 가상함수 테이블 (Virtual Table) (0) | 2022.06.13 |