헤더파일

캐스팅 정리 본문

C++

캐스팅 정리

헤더파일 2019. 11. 17. 17:18

런타임 형식 정보

 

RTTI(런타임 형식 정보)는 프로그램 실행 중에 개체의 형식이 결정될 수 있도록 하는 메커니즘입니다. 많은 클래스 라이브러리 공급업체가 이 기능을 자체적으로 구현하고 있었기 때문에 RTTI가 C++ 언어에 추가되었습니다. 이 때문에 라이브러리 간에 호환되지 않는 문제가 발생하게 되었으므로 언어 수준에서 런타임 형식 정보에 대한 지원이 필요하다는 사실이 명백해졌습니다.

명확성을 위해 여기에서 RTTI에 대한 설명은 거의 전적으로 포인터에 국한됩니다. 하지만 설명된 개념은 참조에도 적용됩니다.

런타임 형식 정보에는 다음 세 가지 기본 C++ 언어 요소가 있습니다.

  • dynamic_cast 연산자

    다형 형식을 변환하는 데 사용됩니다.

  • typeid 연산자

    개체의 정확한 형식을 식별하는 데 사용됩니다.

  • type_info 클래스

    typeid 연산자에서 반환된 형식 정보를 저장하는 데 사용됩니다.

bad_typeid 예외

 

  • typeid의 피연산자가 NULL 포인터인 경우 typeid 연산자 bad_typeid 예외를 throw합니다.
// expre_bad_typeid.cpp
// compile with: /EHsc /GR
#include <typeinfo>
#include <iostream>

class A{
public:
   // object for class needs vtable
   // for RTTI
   virtual ~A();
};

using namespace std;
int main() {
A* a = NULL;

try {
   cout << typeid(*a).name() << endl;  // Error condition
   }
catch (bad_typeid){
   cout << "Object is NULL" << endl;
   }
}

type_info 클래스

typeid를 호출할 때 만들어지는 객체로 타입의 정보를 얻을 수 있습니다.

  • name(), rawname() : 이름 반환
  • before() : 자료형의 정렬된 순서에 따라 비교대상보다 이전이면 1, 아니면 0을 반환합니다. 기본자료형을 지원하고 어떤 클래스가 상속관계에서 이전인지 확인할 수 있습니다.
  • hash_code() : 해쉬테이블등에 인덱스에 사용되는 해쉬코드를 얻을 수 있습니다.
  • ==, != : virtual 함수가 있을 때 부모포인터가 어떤 자식을 가리키는지 확인할 수 있습니다.

 


캐스트 연산자

 

캐스트 연산자에는 C++ 언어 전용 연산자가 몇 가지 있습니다. 이 연산자는 예전 스타일의 C 언어 캐스트에 있는 일부 모호함과 위험성을 제거하는 데 목적이 있습니다. 그 종류는 다음과 같습니다.

  • dynamic_cast 다형 형식 변환에 사용합니다.

  • static_cast 비 다형 형식 변환에 사용합니다.

  • const_cast const, volatile  __unaligned 특성을 제거하는 데 사용됩니다.

  • reinterpret_cast 비트의 단순 재해석에 사용합니다.

  • safe_cast 사용 C++/CLI 안정형 MSIL을 만듭니다.


dynamic_cast 연산자


dynamic_cast < type-id > ( expression )

type_id는 포인터나 레퍼런스여야 합니다. 이전에 정의되어 있어야 하며 void*도 가능합니다. expression에 들어가는 타입은 type_id가 포인터라면 포인터여야 하고 레퍼런스라면 레퍼런스여야 합니다.

 

열거형의 기본유형에 대한 포인터는 런타임에 실패하여 0을 반환합니다.

type_id 런타임에 캐스트가 실패한 경우 nullptr이 반환됩니다.

 

// dynamic_cast_1.cpp
// compile with: /c
class B { };
class C : public B { };
class D : public C { };

void f(D* pd) {
   C* pc = dynamic_cast<C*>(pd);   // ok: C is a direct base class
                                   // pc points to C subobject of pd
   B* pb = dynamic_cast<B*>(pd);   // ok: B is an indirect base class
                                   // pb points to B subobject of pd
}

이 유형의 변환은 포인터를 파생클래스에서 위 계층으로 변환하는 것이기 때문에 업캐스트라고 합니다.

 

 

class A {virtual void f();};
class B {virtual void f();};

void f() {
   A* pa = new A;
   B* pb = new B;
   void* pv = dynamic_cast<void*>(pa);
   // pv now points to an object of type A

   pv = dynamic_cast<void*>(pb);
   // pv now points to an object of type B
}

 

type_id가 void*인 경우 실제 유형을 확인하기 위해 런타임 검사가 수행됩니다. 결과로는 expression에 따른 완전한 오브젝트 포인터가 반환됩니다. void*를 사용하려면 reinterpret_cast나 static_cast를 사용해야 합니다. 예를 들어 typeid가 void*가 아니라면 런타임 검사는 typeid가 가리키는 포인터로 expression을 바꿨을 것입니다.

 

class B {virtual void f();};
class D : public B {virtual void f();};

void f() {
   B* pb = new D;   // unclear but ok
   B* pb2 = new B;

   D* pd = dynamic_cast<D*>(pb);   // ok: pb actually points to a D
   D* pd2 = dynamic_cast<D*>(pb2);   // pb2 points to a B not a D
}

 

만약 expressiontypeid의 부모클래스라면 런타임검사는 expression이 가리키는 객체가 typeid와 완벽히 일치하는 객체인지 확인입니다. 맞다면 typeid를 가리키는 포인터를 반환합니다. 이런 변환을 다운캐스트라고 합니다. 왜냐하면 클래스 상속구조의 아래 방향으로 변환되기 때문입니다. 

 

 

class A {virtual void f();};
class B : public A {virtual void f();};
class C : public A {virtual void f();};
class D : public B, public C {virtual void f();};

void f() {
   D* pd = new D;
   B* pb = dynamic_cast<B*>(pd);   // first cast to B
   A* pa2 = dynamic_cast<A*>(pb);   // ok: unambiguous
   A* pa3 = dynamic_cast<A*>(pd);   // ambiguous
}

 

 

여러 상속이 이루어진 경우 모호함이 발생할 수 있습니다. D를 가리키는 포인터는 안전하게 B나 C로 변환 될 수 있습니다. 하지만 D를 바로 A로 변환하고자 할 때 모호함이 발생될 수 있습니다.

 

class A {virtual void f();};
class B : public A {virtual void f();};
class C : public A { };
class D {virtual void f();};
class E : public B, public C, public D {virtual void f();};

void f(D* pd) {
   B* pb = dynamic_cast<B*>(pd);   // cross cast
   A* pa = pb;   // upcast, implicit conversion
}

 

dynamic_cast는 크로스 캐스트가 가능합니다. 동일한 클래스 상속구조에서 완벽한 E객체에 대해 B에서 D로 변환을 할 수 있습니다. D포인터를 B로 바꾼 뒤 A로 바꾸는 연산도 가능합니다.

 

 

포인터 타입에 경우 nullptr을 반환하고 레퍼런스 타입의 경우 bad cast exception 예외가 발생합니다.

 

 

 

#include <stdio.h>
#include <iostream>

struct A {
    virtual void test() {
        printf_s("in A\n");
   }
};

struct B : A {
    virtual void test() {
        printf_s("in B\n");
    }

    void test2() {
        printf_s("test2 in B\n");
    }
};

struct C : B {
    virtual void test() {
        printf_s("in C\n");
    }

    void test2() {
        printf_s("test2 in C\n");
    }
};

void Globaltest(A& a) {
    try {
        C &c = dynamic_cast<C&>(a);
        printf_s("in GlobalTest\n");
    }
    catch(std::bad_cast) {
        printf_s("Can't cast to C\n");
    }
}

int main() {
    A *pa = new C;
    A *pa2 = new B;

    pa->test();

    B * pb = dynamic_cast<B *>(pa);
    if (pb)
        pb->test2();

    C * pc = dynamic_cast<C *>(pa2);
    if (pc)
        pc->test2();

    C ConStack;
    Globaltest(ConStack);

   // will fail because B knows nothing about C
    B BonStack;
    Globaltest(BonStack);
}

 

출력

in C
test2 in B
in GlobalTest
Can't cast to C

 


static_cast


static_cast <type-id> ( expression )

static_cast는 포인터를 부모클래스로 변환할 때 사용할 수 있습니다. 하지만 그런 변환은 항상 안전하지는 않습니다.

static_cast는 주로 숫자 데이터 형식을 변환하는데 쓰일 수 있습니다. 열거형을 정수형으로 바꾸거나 정수형을 부동소수점형으로 바꾸는 등 데이터의 변환이 확실히 가능한 곳에 쓸 수 있습니다. static_cast는 런타임 검사를 하지 않으므로 dynamic_cast보다 안전하지 않습니다. dynamic_cast의 경우 모호한 포인터는 실패합니다. 하지만 static_cast는 이상이 없다고 판단합니다. 

 

class B {};

class D : public B {};

void f(B* pb, D* pd) {
   D* pd2 = static_cast<D*>(pb);   // Not safe, D can have fields
                                   // and methods that are not in B.

   B* pb2 = static_cast<B*>(pd);   // Safe conversion, D always
                                   // contains all of B.
}

 

D* pd2 = static_cast<D*>(pb); 는 안전하지 않습니다. D가 B에 없는 변수나 함수를 가지고 있을 수 있기 때문입니다. 하지만 D에서 B로 변환하는 것은 안전합니다. D는 항상 B의 모든 것을 들고 있기 때문입니다. pb가 가리키는 실제 객체는 B가 아닐수도 있습니다. 

 

 

	char str[] = "monkey";
	int* i;
	i = (int*)str;// success
	i = static_cast<int*>(str);//compile error

char str[] = "monkey" 같은 문자열을 int*에 넣는 것 같은 명시적 오류를 컴파일 타임에 잡아낼 수 있습니다. C스타일의 형변환에서는 발견하지 못하는 오류입니다.

 

이외에도 이런 특성들이 있습니다.

  • static_cast는 int형을 enum형으로 바꿀 수 있습니다. int값이 enum범위를 넘는다면 결과는 정의되지 않습니다.
  • nullptr 값을 목적지 타입의 nullptr로 변환할 수 있습니다.
  • expression은 void 타입으로 변환 될수 있습니다. void 타입은 const, volatile, unaligned 속성을 포함할 수 있습니다.
  • static_cast는 const, volatile, unaligned 속성을 제거할 수 없습니다.

 


const_cast Operator


const_cast <type-id> (expression)

 

 

 어떤 오브젝트를 가리키는 포인터나 데이터 멤버를 가리키는 포인터, 레퍼런스에 대해 const, volatile, _unaligned 속성을 제외할 수 있습니다. . 포인터와 참조의 경우 결과는 원래 객체를 참조합니다. 데이터 멤버에 대한 포인터의 경우 결과는 데이터 멤버에 대한 원래 (캐스트되지 않은) 포인터와 동일한 멤버를 참조합니다. 레퍼런스된 오브젝트에 따라 쓰기 연산은 정의되지 않은 행동을 일으킬 수 있습니다. const_cast 연산자를 const 변수에 직접적으로 사용할 수 있고 const_cast 연산자는 null포인터 값을 목적지의 nullptr 값으로 변경할 수 있습니다.

 

#include <iostream>

using namespace std;
class CCTest {
public:
   void setNumber( int );
   void printNumber() const;
private:
   int number;
};

void CCTest::setNumber( int num ) { number = num; }

void CCTest::printNumber() const {
   cout << "\nBefore: " << number;
   const_cast< CCTest * >( this )->number--;
   cout << "\nAfter: " << number;
}

int main() {
   CCTest X;
   X.setNumber( 8 );
   X.printNumber();
}

 



reinterpret_cast 연산자

 

어떤 포인터 타입을 다른 포인터타입으로 변경할 수 있습니다. 또한 어떤 정수 타입을 포인터타입으로 변경할 수 있고 그 반대도 가능합니다.

 

잘못된 reinterpret_cast 사용은 안전하지 않습니다. 원하는 변환이 본질적으로 낮은 레벨에서 작동한다면 다른 연산자를 사용해야 합니다. 예를 들어 char* 를 int*로 변환하거나 one class*를 unrelated_class*로 변환하는 작업은 안전하지 않습니다. const, volatile, _unaligned 속성을 제거할 수는 없습니다. null포인터 값을 목적지타입의 null포인터 값으로 바꿀 수 있습니다.

 

한가지 사용방법은 해시함수에 사용할 수 있습니다. 다른 타입의 키와 value값을 연결시켜 줄 수 있기 때문입니다.

 

#include <iostream>
using namespace std;

// Returns a hash code based on an address
unsigned short Hash( void *p ) {
   unsigned int val = reinterpret_cast<unsigned int>( p );
   return ( unsigned short )( val ^ (val >> 16));
}

using namespace std;
int main() {
   int a[20];
   for ( int i = 0; i < 20; i++ )
      cout << Hash( a + i ) << endl;
}

Output:
64641
64645
64889
64893
64881
64885
64873
64877
64865
64869
64857
64861
64849
64853
64841
64845
64833
64837
64825
64829

 


volitile

 


volitile로 선언된 변수는 외부적인 요인으로 그 값이 언제든지 바뀔 수 있음을 뜻한다. 따라서 컴파일러는 volitile 선언된 변수에 대해서는 최적화를 수행하지 않는다. volitile 변수를 참조할 경우 레지스터에 로드된 값을 사용하지 않고 매번 메모리를 참조한다. 

 

*(unsigned int *)0x8C0F = 0x8001

*(unsigned int *)0x8C0F = 0x8002;

*(unsigned int *)0x8C0F = 0x8003;

*(unsigned int *)0x8C0F = 0x8004;

*(unsigned int *)0x8C0F = 0x8005;

 

이 코드를 보면 5번의 메모리 쓰기가 모두 같은 주소인 0x8C0F에 이루어짐을 알 수 있다. 따라서 이 코드를 수행하고 나면 마지막으로 쓴 값인 0x8005만 해당 주소에 남아있을 것이다. 영리한 컴파일러라면 속도를 향상시키기 위해서 최종적으로 불필요한 메모리 쓰기를 제거하고 마지막 쓰기만 수행할 것이다. 일반적인 코드라면 이런 최적화를 통해 수행 속도 면에서 이득을 보게 된다.

 

하지만 이 코드가 MMIO(Memmory-mapped I/O)처럼 메모리 주소에 연결된 하드웨어 레지스터에 값을 쓰는 프로그램이라면 이야기가 달라진다. 각각의 쓰기가 하드웨어에 특정 명령을 전달하는 것이므로, 주소가 같다는 이유만으로 중복되는 쓰기 명령을 없애버리면 하드웨어가 오동작하게 될 것이다. 이런 경우 유용하게 사용할 수 있는 키워드가 volatile이다. 변수를 volatile 타입으로 지정하면 앞서 설명한 최적화를 수행하지 않고 모든 메모리 쓰기를 지정한 대로 수행한다.

 

가시성

 

volatile 키워드는 앞서 살펴본 하드웨어 제어를 포함하여 크게 3가지 경우에 흔히 사용된다.

 

(1) MMIO(Memory-mapped I/O)

(2) 인터럽트 서비스 루틴(Interrupt Service Routine)의 사용

(3) 멀티 쓰레드 환경

 

세 가지 모두 공통점은 현재 프로그램의 수행 흐름과 상관없이 외부 요인이 변수 값을 변경할 수 있다는 점이다. 인터럽트 서비스 루틴이나 멀티 쓰레드 프로그램의 경우 일반적으로 스택에 할당하는 지역 변수는 공유하지 않으므로, 서로 공유되는 전역 변수의 경우에만 필요에 따라 volatile을 사용하면 된다.

 

인터럽트의 경우와 마찬가지로 멀티 쓰레드 프로그램도 수행 도중에 다른 쓰레드가 전역 변수 값을 임의로 변경할 수 있다. 하지만 컴파일러가 코드를 생성할 때는 다른 쓰레드의 존재 여부를 모르므로 변수 값이 변경되지 않았다면 매번 새롭게 메모리에서 값을 읽어오지 않는다. 따라서 여러 쓰레드가 공유하는 전역 변수라면 volatile로 선언해주거나 명시적으로 락(lock)을 잡아야 한다.

 

이처럼 레지스터를 재사용하지 않고 반드시 메모리를 참조할 경우 가시성(visibility) 이 보장된다고 말한다. 멀티쓰레드 프로그램이라면 한 쓰레드가 메모리에 쓴 내용이 다른 쓰레드에 보인다는 것을 의미한다.

 

재배치

 

visual c++의 다음과 같은 기능을 추가로 갖고 있다. 

 

(1) volatile write: volatile 변수에 쓰기를 수행할 경우, 프로그램 바이너리 상 해당 쓰기보다 앞선 메모리 접근은 모두 먼저 처리되어야 한다.

(2) volatile read: volatile 변수에 읽기를 수행할 경우, 프로그램 바이너리 상 해당 읽기보다 나중에 오는 메모리 접근은 모두 이후에 처리되어야 한다.

 

컴파일러는 파이프라이닝의 효율을 위해 어느정도 범위에서 명령어를 재배치하는데 이를 막을 수 있다.

 

 

 

'C++' 카테고리의 다른 글

C++ 상수 멤버 함수 operator new 재정의  (0) 2020.03.11
C++ 멤버함수  (0) 2020.03.09
스레드 프로그래밍  (0) 2019.11.06
C++ 정리2  (0) 2019.10.30
C++ 정리  (0) 2019.10.29
Comments