I'm FanJae.

[C++] RTTI, Dynamic Cast 본문

C++/Basic

[C++] RTTI, Dynamic Cast

FanJae 2024. 8. 22. 16:06

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

1. RTTI (Run Time Type Information)

#include <iostream>
#include <typeinfo>

int main()
{
    int n1 = 10;
    auto n2 = n1; // n2의 타입은? int
    
    const std::type_info& t1 = typeid(n2);
    
    std::cout << t1.name() << std::endl;
}

- 실행시간에 타입의 정보를 얻을 때 사용하는 기술이다.

 

1-1. RTTI 기술의 사용법

- <typeinfo> 헤더를 사용한다.

- typeid 연산자를 사용한다.

- 타입의 정보를 담은 type_info 객체를 얻을 수 있다.

- type_info 객체의 멤버 함수 name()을 사용

 

① Typeid

#include <iostream>
#include <typeinfo>

int main()
{
    auto n1 = 10;
    
    typeid(n1);
}

- typeid는 타입에 대한 정보를 얻을 때 사용하는 연산자이다.

- 다양한 형태로 사용 가능하다.

- typeid 연산자의 결과로 const std::type_info&가 반환된다.

typeid(변수) typeid ( n1 )
typeid(타입) typeid ( int )
typeid(표현식) typeid ( 3 + 4.2 )

 

② std::type_info

- 타입 정보를 담고 있는 클래스이다.

- 사용자가 직접 객체를 만들 수는 없고, typeid() 연산자를 통해서만 얻을 수 있다.

// std::type_info t2;
const std::type_info& t = typeid(3 + 4.2);

- 위와 같이 직접 객체를 만드는 경우는 컴파일 에러가 발생한다.

- 또한, typeid() 연산자를 통해 얻어온다고 하더라도 type_info 상수 참조로만 얻어올 수 있다.

 

③ type의 비교

const std::type_info& t1 = typeid(n1);
const std::type_info& t2 = typeid(int);

if ( t1 == t2 )
{
    std::cout << "n1은 int" << std::endl;
}

if ( typeid(n1) == typeid(int) )
{
    std::cout << "n1은 int" << std::endl;
}

- 위 2개의 if는 논리적으로 동일하다.

- 따라서 실제로는 아래 코드가 더 많이 사용된다.


2. Dynamic_cast

2-1. Downcasting

#include <iostream>
#include <typeinfo>

class Animal{};

class Dog : public Animal
{
public:
	int color;
};

void foo(Animal* p)
{
   // p가 Dog라면?
   p->color = 10;
}
int main()
{
    Animal a; foo(&a);
    Dog    d; foo(&d);
}

- 함수가 인자로 기반 클래스의 포인터를 받으면, 모든 파생 클래스의 전달도 가능하다.

- 문제는 기반 클래스 포인터로 파생 클래스의 고유 멤버에 접근할 수 없다는 문제가 있다.

 

※ 이에 따라서, 파생 클래스의 고유 멤버에 접근하려면 파생 클래스 타입으로 캐스팅(Downcasting) 해야 한다.

void foo(Animal* p)
{
    // p가 Dog라면?
    const std::type_info& t = typeid(*p);

    std::cout << t.name() << std::endl;
}

- 이와 같이 수정하더라도, typeid는 객체 a,d에 대해서 모두 Animal 타입으로 인지한다.

- 그 이유는 가상함수가 없는 객체(non polymorphic type)는 컴파일 시간에 포인터 타입으로 조사한다.

- 이에 반면, 가상함수가 있는 객체(polymorphic type) 는 실행시간 타입으로 조사한다. 즉, 가상함수 테이블을 사용.

 

※ 즉, RTTI를 제대로 사용하기 위해서는 기반 클래스가 1개 이상의 가상함수를 가지고 있어야 한다. 보통은 가상 소멸자가 존재하기 떄문에, 문제가 되지 않는다.

class Animal 
{
public:
    virtual ~Animal() {}
};

 

- 이를 정리하여, 다음과 같이 정리 해본다.

#include <iostream>
#include <typeinfo>

class Animal{};

class Dog : public Animal
{
public:
	int color;
};

void foo(Animal* p)
{
   const std::type_info& t = typeid(*p);
   std::cout << t.name() << std::endl;
   
   if (typeid(*p) == typeid(dog))
   {
       Dog* pDog = static_cast<Dog*>(p);
       pDog->color = 10;
       std::cout << "Dog" << std::endl;
   }
}
int main()
{
    Animal a; foo(&a);
    Dog    d; foo(&d);
}

 

2-2. static_cast의 문제점

#include <iostream>
#include <typeinfo>

class Animal{};

class Dog : public Animal
{
public:
	int color;
};

void foo(Animal* p)
{
   Dog* pDog = static_cast<Dog*>(p);
   std::cout << pDog << std::endl;
}
int main()
{
    Animal a; foo(&a);
    Dog    d; foo(&d);
}

- 위 함수 foo()를 보면, Animal 클래스의 포인터를 Dog로 변환하는 시도를 하고 있다. 이는 안전하지 않을 수 있다.

- 여기서, downcasting과 캐스팅 연산자에 차이에 대해서 보고 정리한다.

① upcasting vs downcasting

upcasting 파생 클래스 포인터를 기반 클래스 타입으로 캐스팅하는 것. 항상 안전하다.
downcasting 기반 클래스 포인터를 파생 클래스 타입으로 캐스팅하는 것. 안전하지 않을 수도 있다.

② downcasting과 캐스팅 연산자

static_cast 잘못된 downcasting을 조사할 수 없다.
단, 컴파일 시간에 캐스팅을 수행하므로 오버헤드가 없다.
dynamic_cast 잘못된 downcasting을 하면 0을 반환한다.
실행 시간에 캐스팅을 수행
하므로 약간의 오버헤드가 있다.

- 정리 하면, static_cast는 위 downcasting이 잘못된 것인지 판단할 수가 없다. 따라서, 정상 실행 될수도 있다.

- 또는 이것이 runtime error를 발생할 수도 있다. 즉, 위험한 코드이다.

void foo(Animal* p)
{
     Dog* pDog = dynamic_cast<Dog*>(p);
     
     if ( pDog != 0 )
     {
         pDog->Color = 10;
     }
     std::cout << pDog << std::endl;
}

- 위와 같이 처리해주어야 한다. 단, dynamic_cast도 반드시 1개 이상의 가상함수를 가지고 있어야 사용할 수 있다.

- dynamic_cast가 꼭 좋은 것은 아니지만, foo()에 들어가는 객체가 Dog 타입임을 장담할 수 없다면, 이와 같이 처리한다.

 

Comments