반응형
출처 : http://blog.naver.com/lovinghc?Redirect=Log&logNo=30013832987


주제 : 실행시간 타입정보(RTTI, RunTime Type Information)

 

5.1 서론

 원래, C++는 RTTI(runtime type information)을 위한 표준화된 지원을 제공하지 않았다. 더욱이, 그것의 창시자들은 적어도 두 가지 이유들 때문에 RTTI 지원을 추가하는 아이디어를 주저하였다. 첫째로, 그들은 C와의 후방 호환성(backward compatibility)을 유지하기를 원했다. 둘째로, 그들은 효율성에 대해서 우려하였다. Smalltalk 및 Lisp와 같은 다른 RTTI를 사용할 수 있는 언어들은 그들의 저 악명 높은 느린 성능에 의해 특징 지워졌다. 동적 타입 체크의 성능 페널티는 시스템이 모든 타입을 저장하기 위하여 필요한 추가적인 정보뿐만 아니라 실행시간에 객체타입을 검색하는 상대적으로 느린 과정으로부터 결과한다. C++ 설계자들은 C의 효율성을 유지하기를 원했다.

언어에 대한 RTTI의 추가에 대한 또 다른 주장은 많은 경우들에 가상 멤버 함수들의 사용이 명시적인 실행시간 타입체크에 대한 대안으로서 이용될 수 있다는 것이었다. 그러나, C++에 대한 다중상속(및 결과적으로 가상상속(virtual inheritance))의 추가는 RTTI의 옹호자들에 대한 압도적인 무기를 제공하였다(다중상속은 5장, “객체지향 프로그래밍 및 설계”에서 논의된다); 어떤 환경들 하에서, 정적 타입체크 및 가상함수들이 불충분하였다는 것이 분명해졌다.

결국, C++ 표준화 위원회는 언어에 RTTI의 추가를 승인하였다. 두 가지의 새로운 연산자인 dynamic_cast<>와 typeid가 도입되었다. 추가적으로, 클래스 std::type_info가 Standard Libary에 추가되었다.

 

5.2 RTTI 없이 뭔가를 하는 것(Making Do Without RTTI)

가상 멤버함수들은 추가적인 RTTI 지원을 위한 필요 없이 동적 typing의 합리적인 수준을 제공할 수 있다. 잘 설계된 클래스 계층은 기저클래스(base class)에서 선언된 모든 가상 멤버함수를 위한 의미있는 연산을 정의할 수 있다. 당신이 GUI-기반의 운영체제의 하나의 컴포넌트로서 파일관리자를 개발해야 한다고 가정하자. 이 시스템에 있는 파일들은 ‘열기’,‘닫기’,‘읽기’ 등등과 같은 옵션들을 갖는 메뉴를 디스플레이하는, 마우스의 오른쪽 버튼의 클릭에 대해 응답하는 아이콘으로서 표현된다. 파일시스템의 기본적인 구현은 다양한 종류들의 파일들을 표현하는 클래스 계층에 의존한다. 잘 설계된 클래스 계층에는 보통 인터페이스로서 사용되는 추상적인 클래스가 존재한다:

class File               // 추상, 모든 멤버들이 순수가상 함수들이다 
{ 
public: virtual void open() =0;  
public: virtual void read() =0;
 
public: virtual void write() =0; 
public: virtual ~File () =0; 
};

File::~File ()     // 순수가상 소멸자가 정의되어야만 한다 
{}

계층의 더 낮은 레벨에서, 당신은 그들이 File로부터 상속하는 공통 인터페이스를 구현하는 파생된 클래스들의 집합을 가지게 된다. 각 하위클래스들 각각은 파일들의 다른 family를 나타낸다. 논의를 단순화하기 위해서 이 시스템에는 오직 두 가지의 파일종류들이 존재한다고 가정하자: 이진 .exe 파일들 및 텍스트 파일들.

class BinaryFile : public File 
{ 
public: 
        void open () {OS_execute(this);}  // 순수 가상함수를 구현 
        //...다른 멤버함수들 
};

class TextFile : public File 
{ 
public: 
        void open () {Activate_word_processor (this); }   
        //...File의 다른 멤버함수들이 여기에서 구현된다 
        void virtual print();  // 추가적인 멤버 함수 
};

순수 가상함수 open()은 파일의 타입에 따라서 모든 파생된 클래스에서 구현된다. 이리하여, TextFile 객체에서, open()은 워드 프로세서를 활성화하는 반면에, BinaryFile 객체는 운영체의 API 함수인 OS_execute()를 invoke하고, 이어서 이진파일로 저장된 프로그램을 실행한다.

이진파일과 텍스트 파일사이에는 몇 가지 차이들이 존재한다. 예를 들어, 텍스트 파일은 그것이 인쇄가능한 문자들의 시퀀스로 구성되기 때문에 스크린 또는 프린터에 직접적으로 인쇄될 수 있다. 이와는 대조적으로 .exe 확장자를 갖는 이진파일은 비트들의 스트림을 포함한다; 그것은 인쇄되거나 스크린상에 직접적으로 디스플레이될 수 없다. 그것은 보통 바이너리 데이터를 그것들의 기호적 표현들로 변환하는 유틸리티에 의해 먼저 텍스트파일로 변환되어야만 한다. (예를 들면, 실행파일에서의 시퀀스 0110010은 대응하는 어셈블리 디렉티브인 move esp, ebp로 대치될 수 있다.) 다른 말로 하면, 실행파일은 보여지거나 인쇄되기 위해서 텍스트파일로 변환되어야만 한다. 그래서 멤버함수 print()는 오직 클래스 TextFile안에서만 나타난다.

이 파일관리자에서, 파일 아이콘위에서 마우스의 오른쪽 버튼을 클릭하면 객체가 응답할 수 있는 메시지들(옵션들)의 메뉴를 연다. 그 목적을 위하여, 운영체제는 File에 대한 레퍼런스를 취하는 함수를 가진다:

OnRightClick (File & file);  // 운영체제의 API 함수

분명히, 클래스 File의 어떤 객체도 인스턴스화될 수 없는데 그 이유는 File이 추상 클래스이기 때문이다. 그러나 함수 OnRightClick()은 File로부터 파생된 어떤 객체도 받아들일 수 있다. 예를 들어, 사용자가 파일 아이콘위에서 마우스의 오른쪽 버튼을 클릭하고 옵션 Open을 선택할 때, OnRightClick은 그것의 argument의 가상 멤버함수 open을 invoke하고 적절한 멤버함수가 호출된다. 예를 들면,

OnRightClick (File & file) 

        switch (message) 
        { 
        //... 
        case m_open: 
                file.open(); 
                break; 
        } 
}

지금까지는 좋다. 당신은 다형적인 클래스 계층 그리고 그것의 argument의 동적 타입에 의존하지 않는 함수를 구현하였다. 이러한 경우에, 언어는 당신의 목적들에 충분한 가상 함수들을 지원한다; 당신은 어떤 명시적인 실행시간 타입정보(RTTI)를 필요로 하지 않는다. 자, 정확하게는 아니다. 당신은 파일 인쇄지원이 결여한 것을 인지했을 수도 있다. 다시 클래스 TextFile의 정의를 보자:

class TextFile : public File 
{ 
public: 
        void open () {Activate_word_processor (this);}  
        void virtual print(); 
};

멤버함수 print()는 당신 시스템에 있는 모든 파일들에 의해 구현된 공통 인터페이스의 일부분이 아니다. print()를 추상클래스 File로 옮기는 것은 설계오류가 될 것인데 그 이유는 이진파일들이 프린트가능하지 않으며 그것을 위하여 의미있는 동작을 정의할 수 없기 때문이다. 그러면 다시 OnRightClick()은 그것이 텍스트 파일을 다룰 때 파일 인쇄를 지원해야만 한다. 이러한 경우에, 가상 멤버 함수들의 형태로 보통의 다형성(polymorphism)은 동작하지 않을 것이다. OnRightClick()은 오직 그것의 argument가 File로부터 파생된다는 것만을 안다. 그러나 이 정보는 실제 객체가 인쇄가능한지 어떤지를 말하기에는 충분치 않다. 분명히, OnRightClick()은 파일인쇄를 적절하게 다루기 위해서 그것의 argument의 동적 타입에 대한 더 많은 정보를 필요로 한다. 이것이 RTTI를 위한 필요가 발생하는 지점이다. OnRightClick()의 구현을 탐구하기 이전에, RTTI 구성요소들 및 그것들의 역할에 대한 개관이 필요하다.

 

5.3 RTTI 구성요소들(1)

연산자들 typeid 및 dynamic_cast<>는 그들의 오퍼랜드의 실행시간 타입정보를 액세스하는 두 가지의 보충적인 형태들을 제공한다. 오퍼랜드의 실행시간 타입정보 그 자체는 type_info 객체 속에 저장된다. 이 절은 어떻게 이들 세 가지 구성요소들이 사용되는지를 예를 들어 설명한다.

5.3.1 RTTI는 배타적으로 다형적 객체들에 적용가능하다

RTTI 단독으로 다형적 객체들에 적용가능하다는 것을 깨닫는 것이 중요하다. 하나의 클래스는 그것의 객체들을 위한 RTTI 지원을 가지기 위하여 적어도 하나의 가상 멤버함수를 가져야만 한다. C++는 비-다형적 클래스들 및 프리미티브 타입들을 위한 RTTI 지원을 제공하지 않는다. 이 제한은 상식이다 - double과 같은 기본적인 타입 또는 string과 같은 구체적인 클래스는 실행시간에 그것의 타입을 바꿀 수 없다. 그래서 그들이 그들의 정적 타입들과 동일하기 때문에 그들의 동적 타입들을 발견할 필요가 없다. 그러나 당신이 곧 보게 되듯이 RTTI 지원을 다형적 클래스들로 배타적으로 한정하는 구체적인 이유가 존재한다.

당신도 알고 있듯이, 적어도 하나의 가상 멤버함수를 가지는 모든 객체는 또한 컴파일러에 의해 추가되는 특수한 데이터 멤버를 포함한다(이 이상의 정보는 13장, “C 언어 호환성 문제들”을 보라). 이 멤버는 가상함수 테이블에 대한 포인터이다. 실행시간 타입정보는 std::type_info 객체에 대한 포인터로서 이 테이블에 저장된다.

5.3.2 클래스 type_info

모든 별개의 타입을 위하여, C++는 필요한 실행시간 타입정보를 포함하는 대응하는 RTTI 객체를 인스턴스화한다. RTTI 객체는 표준클래스 std::type_info 또는 그것으로부터 파생된 구현-정의된 클래스의 인스턴스이다.(std::type_info는 표준헤더 <typeinfo>에서 정의된다). 이 객체는 구현에 의해 소유되며 어떤 식이든 프로그래머에 의해서 변경될 수 없다. type_info의 인터페이스는 다음과 유사하게 보인다(namespaces는 8장, “Namespaces"에서 다루어질 것이다):

namespace std  //class type_info is declared in namespace std 
class type_info 
{ 
public: 
  virtual ~type_info();       // type_info는 기저클래스로 사용된다 
  bool operator==(const type_info&  rhs ) const; // enable comparison 
  bool operator!=(const type_info&  rhs ) const; // return !( *this == rhs) 
 
 bool before(const type_info&  rhs ) const; // ordering 
  const char* name() const; // 타입이름을 포함하는 C-string 반환
private: 
        // 이러한 타입의 객체들은 복사될 수 없다 
        type_info(const type_info&  rhs ); 
        type_info& operator=(const type_info&  rhs); 
}; //type_info

일반적으로, 동일한 타입의 모든 인스턴스들은 type_info 객체를 공유한다. type_info의 가장 폭넓게 사용되는 멤버함수들은 name()과 연산자 ==이다. 그러나 당신이 이들 멤버함수들을 invoke할 수 있기 전에, 당신은 type_info 객체 자체를 액세스해야만 한다. 그것은 어떻게 이루어지는가?

5.3.3 연산자 typeid

연산자 typeid는 그것의 argument로서 객체 또는 타입 이름을 취하고 일치하는 const type_info 객체를 반환한다. 객체의 동적 타입은 다음처럼 조사될 수 있다:

OnRightClick (File & file)  
{
 
        if( typeid( file)  == typeid( TextFile ) ) 
        { 
                // TextFile 객체를 받는다; 인쇄는 가능상태가 됨. 
        } 
        else 
        { 
                // TextFile 객체가 아님. 인쇄는 불능상태가 됨. 
                //not a TextFile object, printing disabled 
        } 
} 

그것이 어떻게 동작하는가를 이해하기 위해서는 강조된 소스 라인을 보라:

        if ( typeid( file)  == typeid( TextFile ) ).

if 문은 argument 파일의 동적타입이 TextFile인지 아닌지를 테스트한다(물론 파일의 정적타입은 File이다). 왼편에 있는 표현식 typeid(file)는 객체파일과 연관된 필요한 실행시간 타입정보를 담고 있는 type_info 객체를 반환한다. 오른편에 있는 표현식 typeid(TextFile)은 클래스 TextFile과 연관된 타입정보를 반환한다. typeid가 객체보다는 클래스 이름에 적용뒬 때, 그것은 항상 그 클래스 이름에 대응하는 type_info 객체를 반환한다. 당신이 앞에서 보았듯이, type_info는 연산자 ==를 오버로드한다. 그래서 왼편에 있는 typeid 표현식에 의해 반환되는 type_info 객체는 오른편의 typeid 표현식에 의해서 반환되는 type_info 객체와 비교된다. 만일 정말로 파일이 TextFile의 인스턴스라면 if 문은 true로 평가된다. 이 경우에, OnRightClick은 메뉴에 있는 추가적인 옵션 print()를 디스플레이한다. 다른 한편으로, 만일 파일이 TextFile이 아니라면, if 문은 false로 평가하고 print() 옵션은 사용할 수 없게 된다. 이것은 모두 좋지만 typeid 기반의 해법은 결점을 가진다. 당신이 새로운 타입의 파일들, 예를 들어 HTML 파일들을 위한 지원을 추가하길 원한다고 가정하자. 파일 관리자 응용이 확장되어야만 할 때 무슨 일이 발생하는가? HTML 파일들은 본질적으로 텍스트 파일들이다. 그들은 읽어질 수 있고 프린트될 수 있다. 그러나, 그들은 어떤 견지에서는 일반적인 텍스트 파일들과 다르다. HTML 파일에 적용되는 open 메시지들은 워드프로세서가 아니라 브라우저를 시작한다. 덧붙여서, HTML 파일들은 그들이 인쇄될 수 있기 전에 인쇄 가능한 형태로 변환되어야만 한다. 최소의 비용으로 시스템의 기능성을 확장할 필요는 소프트웨어 개발자들이 매일 직면하게 되는 도전이다. 객체 지향적인 프로그래밍 및 설계는 이러한 작업을 용이하게 할 수 있다. TextFile을 서브클래싱함으로써 당신은 그것의 존재하는 행위를 재사용하고 HTML 파일들을 위하여 요구되는 추가적인 기능만을 구현할 수 있다:

class HTMLFile : public TextFile 
{ 
        void open () {aunch_Browser ();}  
        void virtual print();  // 프린트가능한 형태로의 필요한 변환을 수행하고 
                               // 파일을 프린트
 
};

그러나 이것은 이야기의 절반에 불과하다. OnRightClick()은 불운하게도 HTMLFile 타입의 객체를 받아들일 때 실패한다. 왜 그런지 다시 보자:

OnRightClick (File & file) //operating system's API function 
{ 
        if ( typeid( file)  == typeid( TextFile ) ) 
        { 
                // 우리는 TextFile 객체를 받는다; 인쇄는 활성화된다. 
        } 
        else //OOPS!  우리는 파일이 HTMLFile 타입일 때 여기에 도달 
        { 
        } 
}

typeid는 그것의 argument의 정확한 타입정보를 반환한다. 그래서 OnRightClick()에 있는 if 문이 argument가 HTMLFile일 때 false로 평가한다. 그러나 false 값은 이진파일을 암시한다. 결론적으로 인쇄는 사용불능(disable)이 된다. 이 힘든 버그는 당신이 새로운 파일 타입을 위한 지원을 추가할 때 마다 나타날 것 같다. 물론, 당신은 그것의 또 다른 테스트를 수행하도록 OnRightClick()을 수정할 수 있다:

OnRightClick (File & file) // 운영체제의 API 함수
{ 
   if ( (typeid( file)  == typeid( TextFile ))  
   || (typeid( file)  == typeid( HTMLFile)) ) //check for HTMLFile as well 
   { 
           // we received either a TextFile or an HTMLFile; 
           // printing should be enabled
 
   } 
   else // it's a binary file, no print option 
   { 
   } 
}

그러나 이러한 해법은 성가시며 에러를 만들어내는 경향이 있다. 게다가 그것은 이 함수를 유지하는 프로그래머들에게 받아들일 수 없는 부담들을 강요한다. 그들은 새로운 클래스가 File로부터 파생될 때마다 추가적인 코드로 OnRightClick()를 혼란스럽게 할 것을 요구할 뿐만 아니라 그들은 또한 최근에 File로부터 파생되었던 어떤 새로운 클래스를 뱔견하기 위해 조심해야만 한다. 다행하게도 C++는 이러한 상황을 다루기 위한 매우 뛰어난 방법을 제공한다.

 

5.4 RTTI 구성요소들(2) - dynamic_cast<>

OnRightClick()이 모든 있음직한 클래스 타입을 다루도록 허용하는 것은 실수이다. 그렇게 하면, 당신은 당신이 새로운 파일 클래스를 추가하거나 존재하는 클래스를 수정할 때 마다 OnRightClick()을 수정하도록 강요된다. 소프트웨어 설계에서, 그리고 특히 객체지향적 설계에서, 당신은 그러한 의존성들을 최소화하기를 원한다. 만일 당신이 OnRightClick()을 밀접하게 조사한다면, 당신은 그것의 argument가 클래스 TextFile의(또는 어떤 다른 클래스의) 인스턴스인지 어떤지를 정말로 알지 못한다는 것을 알 수 있다. 차라리, 알 필요가 있는 모든 것은 그것의 argument가 TextFile인지 어떤지 하는 것이다. 둘 사이에는 큰 차이가 존재한다 - 만일 그것이 클래스 TextFile의 인스턴스라면 또는 만일 그것이 TextFile로부터 파생된 어떤 클래스의 인스턴스라면 객체 is-a TextFile. 그러나, typeid는 객체의 파생 계층(derivation hierarchy)을 시험할 수 있는 능력이 없다. 이러한 목적으로, 당신은 연산자 dynamic_cast<>를 사용해야만 한다. dynamic_cast<>는 두 개의 arguments를 가진다: 첫 번째는 타입 이름이며 두 번째 argument는 dynamic_cast<>이 실행시간에 희망하는 타입으로 캐스트하도록 시도하는 객체이다. 예를 들면

dynamic_cast <TextFile &> (file); // file을 TextFile 타입의 객체에 대한 
                                  // 레퍼런스로 캐스트하려고 시도

만일 시도되는 캐스트가 성공한다면 두 번째 argument가 두 번째 argument로서 나타나는 클래스 이름의 인스턴스이거나 그것으로부터 파생되는 객체이거나 둘 중의 하나가 된다. 선행하는 dynamic_cast<> 표현식은 만일 파일이 is-a TextFile이라면 성공한다. 이것은 적절하게 동작하기 위해서 OnRightclick에 의해서 요구되는 바로 그 정보이다. 그러나 dynamic_cast<>가 성공적이었는지 어떤지를 어떻게 아는가?

5.4.1 Pointer Cast 및 Reference Cast

두 가지 종류의 dynamic_cast<>가 존재한다. 하나는 포인터들을 사용하며 다른 것은 레퍼런스들을 사용한다. 따라서 dynamic_cast<>는 그것이 성공할 때 희망하는 타입의 포인터 또는 레퍼런스를 반환한다. dynamic_cast<>가 캐스트를 수행할 수 없을 때, 그것은 NULL 포인터를 반환하거나 레퍼런스의 경우에는 std::bad_cast 타입의 예외를 던진다. 다음의 포인터 캐스트 예제를 보자:

TextFile * pTest = dynamic_cast < TextFile *> (&file);    
// 파일주소를 TextFile에 대한 포인터로 캐스트하기 위해 시도 
if (pTest)    // dynamic_cast 성공하였다. 파일은 is-a TextFile 

              // pTest를 사용 
} 
else // 파일은 TextFile이 아님; pText는 NULL 값을 가진다 
{ 
} 

C++은 NULL 레퍼런스들을 갖지 않는다. 그래서 레퍼런스 dynamic_cast<>가 실패할 때, 그것은 std::bad_cast 타입의 예외를 던진다. 그것은 항상 try-블록 내에 레퍼런스 dynamic_cast<> 표현식을 놓고 std::bad_cast 예외들을 다루기 위하여 적절한 catch-문장을 포함하는 것이 필요한 이유이다(6장, “예외처리”를 보라). 예를 들면,

try 
{ 
        TextFile  tf = dynamic_cast < TextFile &> (file); 
        // tf를 안전하게 사용 
} 
catch (std::bad_cast) 
{ 
        // dynamic_cast<>가 실패함 
}

이제 당신은 HTMLFile 객체들을 적절하게 다루기 위하여 OnRightClick()을 바꿀 수 있다:

OnRightClick (File & file)  
{
 
   try { 
        TextFile temp = dynamic_cast<TextFile&> (file); 
        // 옵션들을 디스플레이, “print"를 포함 
        switch (message) 
        { 
        case m_open: 
           temp.open();  // TextFile::open 또는 HTMLFile::open 
           break; 
        case m_print: 
           temp.print(); // TextFile::print 또는 HTMLFile::print 
           break; 
 
       }  //switch 
   } //try

   catch (std::bad_cast& noTextFile) 
   { 
       // 파일을 BinaryFile로서 다룸; “print"를 배제 
 
  } 
} // OnRightClick

OnRightClick()의 고쳐진 버전은 HTMLFile 타입의 객체가 is-a TextFile이기 때문에 HTMLFile 타입의 객체를 적절하게 다룬다. 사용자가 파일 관리자 애플리케이션에서 open 메시지를 클릭할 때, 함수 OnRightClick()은 그것의 argument의 멤버 함수인, 그것이 클래스 HTMLFile에서 오버라이드되었기 때문에 기대했던 대로 동작하는, open()을 invoke한다. 마찬가지로, OnRightClick()이 그것의 argument가 TextFile임을 발견할 때, 그것은 print 옵션을 디스플레이한다. 만일 사용자가 이 옵션을 클릭하면, 그것의 argument에 대한 메시지 print를 보내며, OnRightClick()은 기대했던 대로 반응한다.

 

5.5 RTTI 구성요소들(3) -  dynamic_cast<>의 다른 사용들

동적인 타입 캐스트들은 객체의 동적인 타입 -정적 타입이 아니라-이 적절하게 캐스트를 수행할 필요가 있을 경우에 요구된다. 이들 경우들에서 정적 캐스트를 사용하고자 하는 어떤 시도는 컴파일러에 의해서 에러로 표시되거나 그렇지 않으면 -더 나쁘게-그것은 실행시간에 정의되지 않은 행동을 결과할지도 모른다.

5.5.1 교차 캐스트들

교차 캐스트(cross cast)는 다중으로 상속된 객체를 그것의 2차적인 기저 클래스들 중 하나로 변환한다. 교차 캐스트가 어떤 일을 하는지를 설명하기 위해서, 다음의 클래스 계층을 고려하자:

struct A 
{ 
        int i; 
        virtual ~A () {} // 다형성을 강제 : dynamic_cast에 의해서 요구됨 
};

struct B 
{ 
        bool b; 
};

struct D : public A, public B 
{ 
        int k; 
        D() {b = true; i = k = 0;} 
};

A *pa = new D; 
B *pb = dynamic_cast<B*> pa; // cross cast; 다중상속된 객체의 
                             // 2차 기저클래스에 액세스

pa의 정적타입은 “A에 대한 포인터”인 반면에, 그것의 동적타입은 “D에 대한 포인터”이다. 단순한 static_cast<>는 A에 대한 포인터를 B에 대한 포인터로 변환할 수 없는데 그 이유는 A와 B가 관계없기 때문이다(당신의 컴파일러는 이러한 경우에 에러 메시지를 보일 것이다). 강제적인 캐스트(예를 들어 reinterpret_cast<> 또는 C-스타일 캐스트)는 컴파일러가 단순히 pa를 pb로 할당하기 때문에 실행시간에 피해가 막심한 결과들을 가져온다. 그러나 B 하위객체는 D내에 있는 A 하위객체와는 다른 주소에 위치한다. 교차 캐스트를 적절하게 수행하기 위하여, pb의 값이 실행시간에 계산되어야 한다. 결국, 교차 캐스트는 클래스 D가 존재한다는 것조차도 알지 못하는 번역단위(translation unit)에서 수행될 수 있다. 다음 프로그램은 왜 컴파일 시간 캐스트가 아니라 동적 캐스트가 요구되는지를 설명한다:

int main() 
{ 
    A *pa = new D; 
    B *pb = (B*) pa;  // disastrous; pb는 d 내에 있는 하위객체 A를 지시 
    bool bb = pb->b;  // bb는 정의되지 않은 값을 가진다 
    cout<< "pa: " << pa << " pb: " <<pb <<endl;  
    // pb는 적절하게 조정되지 않았다; pa와 pb는 같다 
    pb = dynamic_cast<B*> (pa); // 교차 캐스트 ; pb를 정확하게 조정 
    bb= pb->b; // OK, bb 는 참이다 
    cout<< "pa: "<< pa << " pb: “ << pb <<endl; 
    // OK, pb가 적절하게 조정되었다; pb는 구별되는 값들을 가진다 
    return 0; 
}

프로그램은 두 라인의 출력을 디스플레이한다; 첫 번째 라인은 pa와 pb의 메모리 주소들이 동일함을 보여준다. 두 번째 라인은 pa 및 pb의 메모리 주소들이 요구되는 동적 캐스트를 수행한 후에 다르다는 것을 보여준다.

5.5.2 가상 기저로 부터의 다운캐스팅(Downcasting From a Virtual Base)

downcast는 기저(base)로부터 파생된 객체로의 캐스트이다. 언어로의 RTTI 도입 이전에 다운캐스트들은 나쁜 프로그래밍 습관들로 간주되었다. 그것들은 안전하지 않았으며, 어떤 사람들은 객체의 동적 타입에 대한 의존을 객체지향 원칙들의 위반으로 보았었다(“Standard Briefing: the Latest Addenda to ANSI/ISO C++"를 보라). dynamic_cast<>는 가상 기저(virtual base)로부터 그것의 파생된 객체로의 안전하고 표준화되었으며 간단한 downcasts를 사용할 수 있도록 한다. 다음의 예를 보라:

struct V 
{ 
        virtual ~V ()     // ensure polymorphism 
};

struct A: virtual V {}; 
struct B: virtual V {}; 
struct D: A, B {};

#include <iostream> 
using namespace std; 
int main() 
{ 
        *pv = new D; 
        A* pa = dynamic_cast<A*> (pv); // downcast 
        cout<< "pv : "<< pv << " pa: “ << pa <<endl; 
        // OK, pv 와 pa는 다른 주소들을 가진다 
        return 0; 
}

V는 클래스 A 및 B를 위한 가상 기저이다. D는 A 및 B로부터 다중으로 상속된다. main() 안쪽에서, pv는 “V에 대한 포인터”로서 선언되고 그것의 동적 타입은 “D에 대한 포인터”이다. 여기에서 다시, 교차 캐스트 예제에서처럼, pv의 동적 타입은 그것을 A에 대한 포인터로 적절하게 다운캐스트하기 위하여 필요해진다. static_cast<>는 컴파일러에 의해 거부당할 것이다. 당신이 5장에서 읽었듯이, 가상 하위객체의 메모리 배치(layout)는 비 가상 하위객체의 메모리 배치와는 다를 것이다. 결론적으로, 컴파일 시간에 pv에 의해서 지시되는 객체 안에 있는 하위객체 A의 주소를 계산하는 것은 불가능하다. 프로그램의 출력이 보여질 때, pv 및 pa는 정말로 다른 메모리 주소들을 가리킨다.  

 

5.6 RTTI 구성요소들(4) - RTTI의 비용

RTTI는 공짜가 아니다. 성능의 측면에서 그것이 얼마나 비싼지를 추정하기 위해서 그것이 배후에서 어떻게 구현되는지를 이해하는 것이 중요하다. 기술적 세부들 몇몇은 플랫폼에 종속적이다. 여전히, 이곳에서 보여지는 기본적인 모델은 메모리 오버헤드 및 실행속도의 견지에서 RTTI의 성능 페널티들의 공정한 아이디어를 당신에게 줄 수 있다.


5.6.1 메모리 오버헤드

모든 기본적인 그리고 사용자에 의해서 정의된 타입의 type_info 객체를 저장하기 위해서 추가적인 메모리가 요구된다. 이상적으로, 구현은 단일의 type_info 객체를 모든 별개의 타입과 결합시킨다. 그러나 이것은 요구가 아니며 어떤 상황들 하에서, 예를 들면 동적으로 연결되는 라이브러리들에서, 클래스 당 오직 하나의 type_info 객체가 존재한다는 것을 보장하는 것은 불가능하다. 그래서 구현은 타입 당 하나 이상의 type_info 객체를 생성할 수 있다.

전에 언급되었듯이, dynamic_cast<>가 오직 다형적 객체들에만 적용 가능한 구체적인 이유가 존재한다: 객체는 그것의 실행시간 타입정보를 직접적으로 저장하지 않는다(예를 들면, 데이터 멤버로서).

5.6.2 다형적 객체들의 RTTI

모든 다형적 객체는 그것의 가상 함수들의 테이블에 대한 포인터를 가진다. 전통적으로 vptr이라는 이름을 갖는 이 포인터는 이 클래스에 있는 모든 가상 함수의 메모리 주소들을 포함하는 dispatch 테이블의 주소를 갖는다. 트릭은 이 테이블에 또 다른 항목을 추가하는 것이다. 이 항목은 클래스의 type_info 객체를 가리킨다. 다른 말로 하자면, 다형적 객체의 vptr 데이터 멤버는 type_info의 주소가 고정된 위치에 유지되는, 포인터들의 테이블을 가리킨다. 이 모델은 메모리 사용의 견지에서 매우 경제적이다; 그것은 하나의 type_info 객체와 모든 다형적 클래스를 위한 포인터를 요구한다. 그곳에는 실제로 클래스의 얼마나 많은 인스턴스들이 프로그램속에 존재하는가와 무관하게 고정비용이 존재한다는 것에 주목하라. 그래서 객체의 실행시간 타입정보를 얻어내는 비용은 단일 포인터 indirection 인데, 그것은 데이터 멤버에 대한 직접적인 액세스보다 덜 효율적일 수도 있다; 그렇지만 여전히 그것은 가상함수 invocation과 동일하다.

5.6.3 부가적인 오버헤드

pointer indirection, type_info 객체, 그리고 클래스당 포인터는 RTTI 지원을 위해 지불해야하는 합리적인 가격같이 들린다. 그러나 이것이 전부는 아니다. 어떤 다른 객체와 마찬가지로 type_info 객체들은 생성되어야만 한다. 수백 개의 별개의 다형적 클래스들을 포함하는 큰 프로그램들은 또한 동일한 수의 type_info 객체들을 생성해야만 한다.

5.6.4 RTTI 지원은 보통 토글될 수 있다

이 오버헤드는 비록 당신이 당신의 프로그램들에서 결코 RTTI를 사용하지 않을 지라도 부과된다. 이러한 이유 때문에, 대부분의 컴파일러들은 당신이 RTTI 지원을 끌 수 있도록 한다(당신 컴파일러의 디폴트 RTTI 설정과 그것이 어떻게 수정될 수 있느지를 보기 위하여 사용자 매뉴얼을 체크하라). 만일 당신이 당신의 프로그램에서 RTTI를 결코 사용하지 않는다면 당신은 컴파일러의 RTTI 지원을 꺼버릴 수 있다. 결과들은 더 작은 실행파일들과 약간 더 빠른 코드이다.

5.6.5 typeid 대 dynamic_cast<>

지금까지, 이 장은 RTTI 지원의 간접적인 비용을 논의하였다. 이제는 그것의 직접적인 사용 - 즉, typeid 및 dynamic_cast<>를 적용하는-의 비용을 조사할 시간이다.

typeid invocation은 상수시간 연산이다. 그것의 파생적인 복잡도와 무관하게, 모든 다형적 객체의 실행시간 타입정보를 얻는데 동일한 길이의 시간이 걸린다. 본질적으로 typeid를 호출하는 것은 가상 멤버함수를 invoke하는 것과 유사하다. 예를 들면, 표현식 typeid(obj)는 다음과 같이 유사한 무엇인가로 평가된다:

  return *(obj->__vptr[0]); // 주소가 obj의 가상 테이블속에서 옵셋 0에 
                            // 저장되는 type_info 객체를 반환

클래스의 type_info 객체에 대한 포인터가 가상테이블속에서 고정된 옵셋에 저장된다는 것에 주목하라(보통 0이지만 이것은 구현에 종속적이다).

typeid와는 다르게, dynamic_cast<>는 상수시간 연산이 아니다. T가 타겟 타입이고 obj가 오퍼랜드일 때, 표현식 dynamic_cast<T&>(obj)에서, 오퍼랜드를 타겟 타입으로 캐스트하는데 요구되는 시간은 obj의 클래스 계층의 복잡도에 의존한다. dynamic_cast<>는 그것이 그것 속에 있는 목표 객체(target object)의 위치를 찾을 때 까지 obj의 파생트리를 탐색해야만 한다. 타겟이 가상 기저(virtual base)일 때, 동적 캐스트는 더 복잡해진다(당신이 보는 바와 같이 비록 피할 수 없을 지라도); 결론적으로, 더 긴 실행시간이 소요된다. 최악의 경우의 시나리오는 오퍼랜드가 깊게 파생된(deeply derived) 객체이고 타겟이 연관되지 않는 클래스 타입일 때이다. 이러한 경우에, dynamic_cast<>는 그것이 obj가 T로 캐스트될 수 없다고 자신있게 결정할 수 있기 전 까지는 obj의 전체 파생트리를 탐색해야 한다. 다른 말로 하자면, 실패한 dynamic_cast<>는 O(n) 연산인데, 여기에서 n은 오퍼랜드의 기저 클래스들의 수이다.

당신은 설계의 관점으로부터 dynamic_cast<>가 typeid보다는 선호되는데 그 이유는 전자가 더 큰 유연성과 확장성을 가능하도록 하기 때문이라는 것을 기억할 것이다. 그럼에도 불구하고 포함되는 엔티티들의 파생적 복잡도에 의존하여, typeid의 실행시간 오버헤드는 dynamic_cast<> 보다 덜 비싸질 수 있다. 

 

5.7 결론들

C++의 RTTI 메커니즘은 세 가지 구성요소들로 이루어진다: 연산자 typeid, 연산자 dynamic_cast<>, 그리고 클래스 std::type_info. RTTI는 C++에서 상대적으로 새로운 것이다. 몇몇 존재하는 컴파일러들은 아직도 그것을 지원하지 않는다. 게다가, 그것을 지원하는 컴파일러들은 보통 RTTI 지원을 불능으로 만들도록 설정될 수 있다. 심지어 프로그램에서 RTTI의 명시적인 사용이 존재하지 않을 때조차도, 컴파일러는 결과하는 실행파일에 필요한 “scaffolding"을 자동적으로 추가한다. 이것을 피하기 위하여, 당신은 대개 컴파일러의 RTTI 지원을 꺼버릴 수 있다.

객체지향적인 설계관점으로부터, 연산자 dynamic_cast<>는 당신이 보았듯이 그것이 더 많은 유연성과 강인성을 가능하도록 하기 때문에 typeid보다 선호된다. 그러나 dynamic_cast<>는 그것의 성능이 오퍼랜드의 파생적 복잡도 뿐만 아니라 그것의 타겟 및 오퍼랜드의 근접성(proximity)에 의존하기 때문에 typeid보다 더 느려질 수 있다. 복잡한 파생적 계층들이 사용될 때, 초래되는 성능 패널티는 두드러질 수 있다. 그래서 RTTI를 현명하게 사용하는 것이 권고된다. 많은 경우들에서, 가상 멤버함수는 필요한 다형적 행동을 얻기에 충분하다. 오직 가상 멤버함수들이 불충분할 때에만 RTTI가 고려되어야 한다.

다음은 RTTI를 사용할 때 명심해야하는 몇 가지의 추가적인 정보들이다:

 RTTI 지원을 가능하게 하기 위하여, 객체는 적어도 하나의 가상 멤버함수를 가져야만 한다. 게다가, 만일 그 기능이 이미 켜있지 않다면 당신의 컴파일러의 RTTI 지원을 켜야만 한다(그 이상의 정보는 당신이 사용하고 있는 컴파일러의 사용자 매뉴얼을 참조하라).

 당신의 프로그램이 당신이 참조를 가지고 dynamic_cast<>를 사용할 때 마다 std::bad_cast 예외들을 다루기 위해 catch-문장을 가짐을 보장하라. 또한 p가 NULL일 때 typeid(*p)에서처럼, typeid 표현식속에 null 포인터를 역참조(dereference)하려는 시도는 std::bad_typeid 예외가 던져지는 결과를 초래한다는 것에 유의하라.

 당신이 포인터를 가지고 dynamic_cast<>를 사용할 때, 항상 반환되는 값을 체크하라.

반응형

'C & C++ 관련' 카테고리의 다른 글

2차원 배열이 더블포인터인가?  (0) 2011.01.13
RTTI 란?  (0) 2010.11.15
C++ dlopen mini HOWTO  (0) 2010.10.29
Posted by Real_G