[C++ Primer Plus] Chapter 15. (3) RTTI
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
RTTI (runtime type identification) #
- 실행 시간 데이터형 정보
- 실행 도중에 객체의 데이터형을 결정하는 표준 방법을 제공하는 것이 목적이다.
RTTI의 동작 방식 #
- C++은 RTTI를 지원하는 세 가지 요소를 가지고 있다.
- (1)
dynamic_cast
연산자 - (2)
type_id
연산자 - (3)
type_info
클래스
- (1)
- 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 #
- 어떤 값을
const
나volatile
로 또는 그 반대로 변경하려는 데이터형 변환을 위한 것이다. - 비교하는 두 데이터형은
const
나volatile
가 있느냐 없느냐가 다른 것 빼고는, 데이터형이 동일해야 한다.
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;
}