C++ RTTI(Runtime Type Infomation or Runtime Type Identification)

프로그래밍/C++ 2019.11.04 댓글 Plorence

RTTI는 RunTime Type Identification 또는 RunTIme Type Information의 약자로 실행 시간 데이터형 정보라는 뜻입니다.

오래된 C++은 대다수가 지원 안 하며 어떤 C++은 켜거나 끌 수 있는 옵션이 있습니다.

(visual studio는 옵션이 있지만, 디폴트로 체크되어 있습니다. 만약 해제하고 사용한다면 런타임 에러가 발생합니다.)

(프로젝트 우클릭 -> 속성 -> C/C++ -> 언어 -> 런타임 형식 정보 사용)

 

RTTI

RTTI의 목적은 프로그램이 실행 도중에 객체의 데이터형을 결정하는 표준 방법을 제공하는 것입니다.

기초 클래스부터 상속된 클래스 계층이 있다고 하면 어떤 클래스의 객체인지에 따라 다른 처리를 해야 한다면 RTTI를 사용해야 합니다.

RTTI는 가상 함수들을 가지고 있는 클래스 계층에서만 사용이 됩니다.

파생 객체들의 주소를 기초 클래스 포인터들에 대입해야 하는 유일한 클래스 계층이기 때문입니다.

 

C++는 RTTI를 지원하는 세 가지 요소가 있습니다.

 

  • dynamic_cast 연산자(기초 클래스형을 지시하는 포인터로부터 파생 클래스형을 지시하는 포인터를 생성, 불가능하다면 널포인터 리턴)

  • typeid 연산자 (객체의 정확한 데이터형을 식별하는 값을 리턴)

  • type_info 클래스(특별한 데이터형에 대한 정보를 저장)

 

dynamic_cast 연산자

세 가지 요소 중에 가장 많이 사용됩니다.

dynamic_cast 연산자는 포인터가 지시하는 객체형이 무엇인지 알려주지 않지만 지시하는 객체의 주소를 특정형 포인터에 안전하게 대입할 수 있는지 알려줍니다.

명시적 데이터형 변환은 안전한지 여부에 상관없이 무조건 변환하기 때문에 사용 시 문제가 생길 수 있습니다.

#include <iostream>
class Animal
{
public:
       virtual void Move() {
              std::cout << "(움직인다.)" << std::endl;
       }
};
class Dog : public Animal
{
public:
       void Move() {
              std::cout << "멍멍멍멍!(움직인다.)" << std::endl;
       }
       void Say() {
              std::cout << "멍멍!!" << std::endl;
       }
};
int main(void) {
       Animal * animal = new Animal();
       Dog * dog = new Dog();
       Animal * animals[2];
       animals[0] = animal;
       animals[1] = dog;
       for (int i = 0; i < 2; i++) {
              animals[i]->Move();
              Dog * d = dynamic_cast<Dog *>(animals[i]);
              if (d != nullptr) { //Dog클래스로 변환이 가능할때
                     d->Say();
              }
       }
}

업 캐스팅 시 파생 클래스의 메서드는 호출할 수 없으니 이렇게 형 변환이 가능한지 확인하고(dynamic_cast) 가능하면 바꿔서 호출하도록 만듭니다.

변환될 수 없다면 널 포인터를 리턴하고 조건문에 의해 호출이 안됩니다.

가능하다면 가상 함수들을 사용하고 RTTI는 꼭 필요한 경우에만 사용해야 합니다.

 

dynamic_cast는 참조에서도 사용이 가능한데 참조에서는 널 포인터형에 해당하는 참조값이 없어서 변환될 수 없다면 bad_cast(typeinfo 헤더 파일, exception 클래스로부터 파생됨) 예외가 발생합니다.

 

typeid 연산자, type_info 클래스

먼저 typeinfo 헤더 파일에 있습니다.

typeid 연산자를 사용하여 두 객체의 데이터형이 같은지 결정할 수 있고 type_info 객체에 대한 참조를 리턴합니다.(type_info 클래스는 데이터형을 비교하는 데 사용할 수 있도록 ==와!= 연산자를 오버로딩 합니다.)

#include <iostream>
#include <typeinfo>
class Animal
{
public:
       virtual void Move() {
              std::cout << "(움직인다.)"  << std::endl;
       }
};
class Dog : public Animal
{
public:
       void Move() {
              std::cout << "멍멍멍멍!(움직인다.)" << std::endl;
       }
       void Say() {
              std::cout << "멍멍!!" << std::endl;
       }
};
int main(void) {
       Animal * animal = new Animal();
       Dog * dog = new Dog();
       Animal * animals[3];
       animals[0] = animal;
       animals[1] = dog;
       for (int i = 0; i < 2; i++) {
              animals[i]->Move();
              if (typeid(Dog) == typeid(*animals[i])) { //Dog와 *animals[i]가  지시하는 객체가 동일할때
                     Dog * d = (Dog *)animals[i];
                     d->Say();
                     std::cout << "처리하고 있는 데이터형은" <<  typeid(*animals[i]).name() << "입니다." << std::endl;
              }
       }
}

이때 animals[i]가 널 포인터이면 bad_typeid 예외가 발생합니다.(exception 클래스에서 파생됨)

 

RTTI의 문제

프로그램 효율을 떨어트리거나 잘못된 프로그래밍 습관을 초래한다고 많은 비판을 받습니다.

만약 typeid만 사용하게 된다면 코드도 길어지고 클래스의 이름을 명시적으로 지정해야 합니다.(그래서 또다시 파생시키는 클래스가 있을 때 조건문을 추가해줘야 됨.)

하지만 dynamic_cast는 변환하려는 클래스의 모든 파생 클래스에서 동작하기 때문에 typeid보다 좋습니다.

 

두 번째 문제는, type_info클래스 name 멤버 함수의 결괏값(문자열)에 대해 보장하지 않습니다.

#include <typeinfo>
#include <iostream>


template <typename T>
struct Base {
        virtual ~Base() = default;
};


void pt(Base<void>& b) {
        std::cout << typeid(b).name() << '\n';
        std::cout << typeid(b).name() << '\n';
}


struct Derived : Base<void> { };


int main() {
        Derived d;
        pt(d);
}
struct Base<void>
struct Derived

관련 자료

 

댓글