Skip to main content

[C++ Primer Plus] Chapter 15. (3) RTTI

C++ 기초 플러스 책을 읽고 공부한 노트입니다.




RTTI (runtime type identification) #

  • 실행 시간 데이터형 정보
  • 실행 도중에 객체의 데이터형을 결정하는 표준 방법을 제공하는 것이 목적이다.



RTTI의 동작 방식 #

  • C++은 RTTI를 지원하는 세 가지 요소를 가지고 있다.
    • (1) dynamic_cast 연산자
    • (2) type_id 연산자
    • (3) type_info 클래스

  • RTTI는 가상함수들을 가지고 있는 클래스들에 대해서만 사용할 수 있다.
    • 그 이유는, 그들이 파생 객체들의 주소를 기초 클래스 포인터들에 대입하는 유일한 클래스 계층이기 때문이다.

RTTI는 가상함수들을 가지고 있는 클래스들에 대해서만 사용할 수 있다.



dynamic_cast 연산자 #

  • 기초 클래스형을 지시하는 포인터로부터 파생 클래스형을 지시하는 포인터를 생성한다.
  • 포인터가 지시하는 객체형이 무엇인지 알려주지 않는다.
  • 대신에, 그 객체의 주소를 특정형의 포인터에 안전하게 대입할 수 있는지 알려준다.
  • 가능하지 않다면, 널 포인터인 0을 리턴한다.
class A {};
class B : public A {};
class C : public B {};

int main()
{
    A* a = new A;
    A* b = new B;
    A* c = new C;

    C* cc = (C*)c; // 안전하다
    B* bc = (B*)c; // 안전하다
    C* ca = (C*)a; // 안전하지 않다. 런타임에 이러한 데이터형 변환이 안전한지 물어볼 수는 없을까?
}
#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;

class Grand
{
private:
    int hold;

public:
    Grand(int h = 0) : hold(h) {}

    // Say() 함수가 없다. 
    virtual void Speak() const { cout << "Grand 클래스입니다. \n"; }
    virtual int Value() const { return hold; }
};

class Superb : public Grand
{
public:
    Superb(int h = 0) : Grand(h) {}

    void Speak() const { cout << "Superb 클래스입니다.\n"; }
    virtual void Say() const
    {
        cout << "내가 가지고 있는 값은 " << Value() << "!\n";
    }
};

class Magnificent : public Superb
{
private:
    char ch;

public:
    Magnificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}

    void Speak() const { cout << "Magnificent 클래스입니다.\n"; }
    void Say() const
    {
        cout << "내가 가지고 있는 문자는 " << ch << "이고, 정수는 " << Value() << "!\n";
    }
};

Grand* GetOne() // 세 종류의 객체 중에 하나를 임의로 생성한다. 
{
    Grand* p = new Grand();

    switch (rand() % 3)
    {
    case 0: p = new Grand(rand() % 100);
        break;
    case 1: p = new Superb(rand() % 100);
        break;
    case 2: p = new Magnificent(rand() % 100, 'A' + rand() % 26);
        break;
    }

    return p;
}

int main()
{
    srand(time(0));

    Grand* pg;
    Superb* ps;

    for (int i = 0; i < 5; i++)
    {
        pg = GetOne();
        pg->Speak();

        // Grand 클래스에는 Say()가 없다.
        // 하지만 dynamic_cast연산자를 사용해서 Superb* 형으로 변환시킬 수 있는지 알아볼 수 있다.
        // 그러면 Superb형이나 Magnificent형 둘 중 하나일 때 참이 된다.
        if (ps = dynamic_cast<Superb*>(pg))
            ps->Say();
    }
}

  • dynamic_cast를 참조와 함께 사용할 수도 있다.
    • 널 포인터형에 해당하는 참조값은 존재하지 않는다.
    • 따라서 실패를 나타내기 위해 bad_cast형의 예외를 발생시킨다.
      • 이것은 exception클래스로부터 파생된 것이다.
      • typeinfo 헤더파일에 정의되어 있다.
#include <typeinfo>

//...

try
{
    Superb& rs = dynamic_cast<Superb&>(rg);
}
catch (bad_cast&)
{
    //...
}



typeid 연산자와 type_info 클래스 #

  • typeid연산자를 사용하여 두 객체의 데이터형이 같은지 결정할 수 있다.
    • 어떤 객체의 정확한 데이터형을 식별하는 하나의 값을 리턴한다.
    • 매개변수 종류
      • (1) 클래스의 이름
      • (2) 객체로 평가되는 식
    • 만약에 매개변수로 주어진 것이 nullptr이면, bad_typeid 예외를 발생시킨다.
    • type_info 객체에 대한 참조를 리턴한다.
      • 어떤 특별한 데이터형에 대한 정보를 저장한다.
      • 이 클래스는 ==!=연산자를 오버로딩한다.
      • 안에는 name()멤버가 포함되어 있어서, 가리키는 객체의 클래스 이름을 출력한다.
int main()
{
    srand(time(0));

    Grand* pg;
    Superb* ps;

    for (int i = 0; i < 5; i++)
    {
        pg = GetOne();

        // typeid 클래스에는 가리키는 객체의 클래스의 이름을 리턴하는 name()멤버가 있다. 
        cout << "지금 처리중인 데이터형: " << typeid(*pg).name() << ".\n";

        pg->Speak();

        if (ps = dynamic_cast<Superb*>(pg))
            ps->Say();

        // typeid연산자는 매개변수로 클래스 이름이나 객체로 평가되는 식을 받아들인다. 
        // 만약에 pg가 널 포인터이면 bad_typeid 예외를 발생시킨다. 
        if (typeid(Magnificent) == typeid(*pg))
            cout << "그래, 너는 Magnificent형이구나! \n";
    }
}



데이터형 변환 연산자 #

  • C의 데이터형 변환 연산자는 너무 느슨하다.
struct Data
{
    double data[200];
};

struct Junk
{
    int junk[100];
};

int main()
{
    Data d = { 2.5, 3.5, 20.2 };

    // 그 어떤 것도 이치에 맞지 않는다.
    // 하지만 허용된다. 
    char* pc = (char*)&d;
    char ch = (char)&d;
    Junk* pj = (Junk*)&d;
}

  • 그래서 데이터형 변환을 더욱 엄격하게 규정하는 4개의 데이터형 변환 연산자를 추가하였다.
    • dynamic_cast
      • 앞서 보았듯, 어떤 클래스 계층 내에서(is-a 관계시) 업캐스트를 허용하고, 다른 데이터형 변화는 허용하지 않는 것이다.
    • const_cast
    • static_cast
    • reinterpret_cast



const_cast #

  • 어떤 값을 constvolatile로 또는 그 반대로 변경하려는 데이터형 변환을 위한 것이다.
  • 비교하는 두 데이터형은 constvolatile가 있느냐 없느냐가 다른 것 빼고는, 데이터형이 동일해야 한다.
class High {};
class Low {};

int main()
{
    High h;
    const High* phc = &h;

    High* ph = const_cast<High*>(phc); // const High* 를 High*로 (O)

    const Low* pl = const_cast<const Low*>(phc); // const High* 를 const Low*로 (X)
}

  • 포인터의 접근을 변경할 수 있지만, const로 선언된 양을 변경하려는 시도를 그 결과가 정의되지 않는다.
void change(const int* pt, int n)
{
    int* pc;
    pc = const_cast<int*>(pt); // const int*형을 int*형으로 변경한다. 
    *pc += n; // 하지만 원래 형이 const 인경우 값을 증가시키지 않는다. 
}

int main()
{
    int pop1 = 20;
    const int pop2 = 20;

    cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl; // 20, 20

    change(&pop1, -5);
    change(&pop2, -5); // 원래 const 이므로 제대로 동작하지 않는다. 

    cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl; // 15, 20
}



static_cast #

  • 암시적으로 데이터형 변환이 된다면 유효하다.
    • 예를 들어, 열거값은 암시적으로 정수값으로 변환할 수 있기 때문에 static_cast를 사용해서 열거값을 정수값으로 변환할 수 있다.
class Base {};
class Derived : public Base {};
class Other {};

int main()
{
    Base base;
    Derived derived;

    // 유효한 업캐스트. 명시적으로 이루어질 수 있으므로 유효하다. 
    Base* pb = static_cast<Base*> (&derived);    

    // 유효한 다운캐스트. 반대 방향의 변환이 암시적으로 이루어질 수 있으므로 유효하다. 
    Derived* pd = static_cast<Derived*> (&base);  
    
    // 유효하지 않다. 
    Other* po = static_cast<Other*> (&derived);  
}



reinterpret_cast #

  • 이것은 본래부터 위험한 데이터형 변환을 위한 것이다.
  • 일반적으로 시스템 의존적인 저수준 프로그래밍에서 사용한다.
    • 그러나 이식성이 없다.
    • 예를 들면, 멀티바이트 값을 서로 다른 바이트 순서를 사용하여 저장하는 두 개의 다른 시스템이 있을 수 있다.
  • 모든 것을 허용하지는 않는다.
    • 예를 들면, 포인터형을 포인터형을 충분히 저장할 수 있는 큰 정수형으로 캐스트할 수는 있지만,
    • 포인터형보다 작은 정수형 또는 부동소수점형으로 캐스트할 수는 없다.
    • 또한 함수 포인터를 데이터 포인터로 또는 그 반대로 캐스트할 수 없다.
struct Dat
{
    // 2바이트
    short a;
    short b;
};

int main()
{
    // lval의 처음 2바이트를 출력한다. 
    long lval = 0xA224B118;
    Dat* pd = reinterpret_cast<Dat*> (&lval);
    cout << pd->a;
}