[C++] RTTI(Run-Time Type Information)
0. 다형성(Polymorphism)이란?
다형성은 객체 지향 프로그래밍의 중요한 특징 중 하나로, 상속관계에 있는 클래스 사이에 서로 형변환이 가능한 성질을 말한다.
플레이어와 몬스터가 있는 게임을 예시로 보자.
class Object
{
public:
virtual void Update() {};
};
class Player : public Object
{
public:
virtual void Update() override
{
// 플레이어 행동 로직
}
void Dance()
{
// 춤추기
}
};
class Monster : public Object
{
virtual void Update() override
{
// 몬스터 행동 로직
}
};
int main()
{
Player player;
Monster monster1;
Monster monster2;
Monster monster3;
Object* objArr[4] = {&player, &monster1, &monster2, &monster3};
while (true)
{
for (int i = 0; i < 4; ++i)
{
objArr[i]->Update();
}
// if (게임 종료 조건)
// break;
}
return 0;
}
Player
와 Monster
는 엄연히 다른 클래스지만, 부모 클래스인 Object
로 Up-Casting
해 단순히 Update()
함수 호출을 통해 로직을 실행하고 있다. 겉보기에는 Object의 Update() 함수가 실행될 것 같지만 디버깅을 통해 동작을 살펴보면, 실제 객체의 타입에 맞는 Update() 함수가 호출되는 것을 볼 수 있다.
만약 다형성이 없었다면 player 배열 따로, monster 배열 따로 만들어서 호출하거나, 최악의 경우(여러 클래스 종류가 있지만 객체가 한개씩인 경우) 모든 객체에 대해 Update()를 일일이 호출해야 할 수도 있다.
위의 예시에서 알 수 있듯이, 다형성은 객체 지향에서 설계와 유지보수 측면에서 매우 큰 장점을 제공한다.
그런데 어떻게 이런 방식이 가능할까?
1. virtual, vpointer, vtable
컴파일러는 만약 클래스 내에 virtual
함수가 하나라도 있다면, 객체를 만들 때 내부에 vpointer
(이하 vptr
)을 생성한다(따라서 빈 클래스라도 virtual 함수가 있는 경우 8바이트(x64)를 차지한다 https://hyun-soon.github.io/cpp/EBO/).
이 vptr은 각 클래스 종류 별로 한 개씩 생성되는 vtable
을 가리키는데, vtable에는 각 클래스가 가지는 virtual 함수들의 주소가 담겨있다(vtable은 .rdata
영역에 저장된다).
즉,
- virtual 함수가 있는 클래스별로 vtable이 생성되고
- 객체 생성 시 내부에는 vptr이 생성되어
- 다형성을 사용하더라도 vptr을 통해 vtable에서 함수의 주소를 확인하고, 실제 타입에 맞는 함수를 호출할 수 있는 것이다.
하지만 추가 메모리와 작업을 필요로 하므로 약간의 오버헤드가 발생한다.
1. dynamic_cast
그런데 다형성을 사용하다가도, Player::Dance() 함수를 호출하고 싶으면 Object 포인터를 Player 포인터로 Down-Casting
해야 한다.
만약 Player의 정확한 인덱스를 알고 있다면, static_cast<Player*>(objArr[idx])
와 같이 변환할 수 있을 것이다. 하지만 게임이 점점 커지고 플레이어, 몬스터, NPC 등 여러 오브젝트들이 하나의 배열에 담겨있다고 상상해본다면, 인덱스 전부를 추적하기가 쉽지 않을 것이란 확신이 든다. 클래스 내부에 메모리를 조금 더 써서 타입을 저장하는 방법도 있지만, C++에서 제공하는 dynamic_cast
를 사용할 수도 있다.
dynamic_csat를 사용해 objArr에서 임의의 한 포인터를 Player 포인터로 캐스팅해보자.
int randIdx = rand();
Player* playerPtr = dynamic_cast<Player*>(objArr[randIdx]);
if (playerPtr)
playerPtr->Dance();
만약 objArr[randIdx]
가 가리키는 실제 객체가 Player라면, dynamic_cast는 Player 포인터로 캐스팅된 결과를 반환하고, 실제 객체가 Player가 아니었다면 nullptr을 반환한다.
dynamic_cast를 레퍼런스로도 사용할 수 있는데, 레퍼런스는 선언과 동시에 값을 정해줘야 한다. 그런데 캐스팅하려는 타입이 잘못된 경우 리턴으로 사용할 값이 마땅치 않으므로, 이 경우에는 std::exception을 throw한다.
virtual 함수는 vtable 확인을 통해 실제 함수를 호출할 수 있다고 했다. 그럼 dynamic_cast는 런타임에 어떻게 실제 타입 검사를 하는걸까?
2. RTTI(Run-Time Type Information)
RTTI
는 말 그대로 프로그램 실행 중에 객체의 실제 타입 정보를 확인할 수 있게 해주는 기능이다. RTTI가 제공하는 기능은 typeid
와 dynamic_cast
가 있다.
typeid의 사용 예시도 잠깐 살펴보자.
#include <iostream>
#include <typeinfo>
class Base { virtual void dummy() {} };
class Derived : public Base {};
int main() {
Base* b = new Derived;
// typeid() : 인자로 넣어준 변수 혹은 타입에 해당하는 type_info 클래스의 객체를 반환한다.
std::cout << typeid(*b).name() << '\n'; // Derived의 타입이 출력됨
return 0;
}
typeid를 사용하여 객체의 실제 타입 정보를 얻을 수 있으며, 타입 정보를 비교하거나 로그를 찍는데 유용하게 사용된다.
RTTI는 어떻게 작동할까?
RTTI를 사용하기 위한 조건으로, 클래스에 반드시 virtual 함수가 1개 이상 있어야 한다. 위에서 virtual 함수가 하나라도 있으면, 컴파일러는 vtable을 생성한다고 했다. 이 때, vtable 근처 혹은 내부(컴파일러마다 다름)에 RTTI 정보들을 담아놓은 RTTI Complete Object Locator
를 가리키는 포인터가 같이 생성된다.
아래 코드로 디버깅을 통해 RTTI를 확인해보자.
#include <iostream>
#include <typeinfo>
class Base
{
virtual void test() {};
};
class Derived : public Base
{
virtual void test() override {};
};
int main()
{
Base base;
std::cout << typeid(base).name() << '\n'; // breakpoint
return 0;
}
주석이 있는 라인을 실행한 후, base 객체의 vptr 주변을 탐색해 봤더니, -1 인덱스에 RTTI Complete Object Locator가 있는 것을 확인할 수 있었다.
즉, virtual 함수처럼 RTTI도 런타임에 정보가 있는 주소를 찾아가, 실제 타입을 확인함으로써 typeid, dynamic_cast 같은 기능을 제공하는 것이다.
RTTI 역시 약간의 오버헤드가 발생한다.
Leave a comment