Advanced C++

[C++] 형 변환 (2) dynamic_cast / static_cast / reinterpret_cast / const_cast

로파이 2021. 4. 1. 15:12

C++ 스타일의 클래스 간 명시적 형변환을 알아본다.

dynamic_cast / static_cast / reinterpret_cast / const_cast

 

dynamic_cast<type-id>(expression)

  • dynamic_cast는 런타임에 expression의 타입을 확인하여 type-id로 형 변환이 가능한지 검사한다.
  • 참조나 포인터에 대해서만 사용가능하다.
  • RTTI, 런타임 형식 정보를 사용하기 때문에 다른 캐스팅보다 느리다.
  • 안전한 캐스팅을 제공하며 캐스팅이 실패하면 0 포인터, 즉 nullptr를 반환한다. 

참고 : docs.microsoft.com/ko-kr/cpp/cpp/dynamic-cast-operator?view=msvc-160

 

- 업 캐스팅

// 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
}

문법상 허용이 되는 업 캐스팅의 경우 암묵적 형 변환이 가능하다. 이는 명시적으로 dynamic_cast를 사용하여 안전하게 캐스팅을 시도 할 수 있다. 만약 실제 타입이 기반 클래스가 아니라 파생 클래스라면 캐스팅이 실패한다.

 

- 다운 캐스팅

// dynamic_cast_3.cpp
// compile with: /c /GR
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
}

C 스타일에서는 금지되지 않았던 포인터의 다운 캐스팅이 dynamic_cast를 사용하면 실패한다. 위의 예시에서 pb는 실제로 D 타입 인스턴스고 첫번째의 경우 실제 타입과 정확히 일치하기 때문에 캐스팅이 성공한다. 두번재에서는 B 타입 인스턴스를 D 타입으로 다운 캐스팅하려하기 때문에 실패하게 된다.

 

- 다중 상속

// dynamic_cast_4.cpp
// compile with: /c /GR
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;
   A* pa = dynamic_cast<A*>(pd);   // C4540, ambiguous cast fails at runtime
   B* pb = dynamic_cast<B*>(pd);   // first cast to B
   A* pa2 = dynamic_cast<A*>(pb);   // ok: unambiguous
}

업 캐스팅에서 기반 클래스가 모호한 경우 캐스팅에 실패한다. D 클래스 타입 포인터 pd를 A 클래스 포인터 타입 pa로 캐스팅하는 과정에서 D 클래스는 다중 상속 클래스로 B의 A인지 C의 A인지 모호하기 때문에 실패하게 된다.

 

static_cast<type-id>(expression)

  • static_cast는 컴파일 타임에 형변환을 검사한다.
  • 실제 가리키는 타입이 파생 클래스임을 알고 있을 경우 다운 캐스팅을 명시적으로 선언할 수 있다.
  • 문맥 상 형 변환이 허용되지 않으면 컴파일 에러를 발생시킨다.
  • 검사가 오류가 없더라도 형 변환이 안전한 것은 아니다.

참고: docs.microsoft.com/ko-kr/cpp/cpp/static-cast-operator?view=msvc-160

 

- 컴파일 타임에 검사해도 되는 경우

이전 포스트 예제에서 다운 캐스팅은 금지되지만 C 스타일 명시적 변환은 허용하고 있다. C++에서는 static_cast가 컴파일 타임에 이를 금지한다.

 

- 컴파일 타임에 형식을 알 수 없는 경우

static_cast가 안전하지 않은 이유는 문맥에서 해당 인스턴스의 타입에 대한 검사가 컴파일 타임에 알 수 없는 경우 발생한다.

void foo5(B* pb)
{
	C* pc1 = dynamic_cast<C*>(pb);
	C* pc2 = static_cast<C*>(pb);
}

위 예제에서 함수의 매개변수 pb의 타입은 동적으로 결정된다. 이는 static_cast가 컴파일 타임에 실제 타입을 알 수 없기 때문에 에러를 발생시키지는 않지만 안전하지 않은 C 스타일 캐스팅 결과와 동일한 결과를 산출한다.

 

- dynamic_cast vs static_cast

dynamic_cast와 static_cast 모두 업 캐스팅이 허용된다. 하지만 다운 캐스팅의 경우, dynamic_cast는 런 타임에 실제 타입 정보를 검사하여 다운 캐스팅이 허용되는 지 확인 후 nullptr 혹은 형 변환이 이루어진다. static_cast는 컴파일 타임에 형 변환이 이루어지며 위와 같이 컴파일 시간에 형식을 알 수 없는 경우 다운 캐스팅이 허용되지 않음에도 강제 다운 캐스팅 형 변환이 이루어진다.

 

dynamic_cast는 static_cast보다 안전하지만 RTTI를 이용하기 때문에 static_cast보다 매우 느리다. 만약, 사용자가 해당 인스턴스가 다운 캐스팅이 안전하다는 보장이 있고 자주 호출되는 루틴에서 사용되면 static_cast를 사용하는 것이 좋고, 실행 시간 제약이 크지 않으며 함수 루틴이 조건적으로 드물게 호출되는 문맥에서 정확함이 필요하다면 dynamic_cast를 사용하는 것이 좋다. 

 

- 명시적 변환을 선언할 때

리터럴형 변수간 캐스팅에서 명시적 선언으로 C 스타일을 대체할 수 있다. 하지만 narrow conversion등의 문제는 여전히 사용자의 책임에 있다.

typedef unsigned char BYTE;

void f() {
   char ch;
   int i = 65;
   float f = 2.5;
   double dbl;

   ch = static_cast<char>(i);   // int to char
   dbl = static_cast<double>(f);   // float to double
   i = static_cast<BYTE>(ch);
}

 

const_cast<type-id>(expression)

  • 표현식의 const성을 제거하거나 부여하는데 사용한다.
  • const 함수 내에서 this 포인터를 const를 제거한 클래스로 타입 변환을 하여 멤버 변수를 수정할 수 있다. 

참고: docs.microsoft.com/ko-kr/cpp/cpp/const-cast-operator?view=msvc-160

 

- const 함수 내에서 멤버 변수 수정

class Text
{
private:
	char m
public:
    void foo() const
    {
    	const_cast<Text*>(this)-> m = 'x';
    }
};

 

reinterpret_cast<type-id>(expression)

포인터가 다른 포인터 형식으로 변환하는 것을 허용한다.

정수 계열 데이터가 포인터 형식으로 변환될 수 있다.

 

C++ 타입 캐스팅 중 가장 유연하고 모든 형 변환을 허용한다.

실제 데이터 값을 수정하지 않고 비트를 그대로 유지하며 형식만 바꾸기 때문에 포인터에서 정수형 혹은 정수에서 포인터로 해석할 수 있게 한다. 

 

참고: docs.microsoft.com/ko-kr/cpp/cpp/reinterpret-cast-operator?view=msvc-160


- 포인터 주소를 정수로 재해석

msdn 예제에서 포인터를 정수로 해석하여 해쉬하는 예제를 제시한다.

// 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;
}

 

- 포인터 형식 단순 재해석

이전 포스트에서 가상 함수가 없는 클래스로 캐스팅 될 때, 메모리 해제가 안되는 문제가 있었다. reinterpret_cast는 클래스가 어떻게 되든 상관 없이 주소를 바꾸지 않고 형식만 그대로 바꾼다.

class First { };
class Second : public First
{
public:
	virtual void f() {};
};
class Third : public Second 
{
public:
	virtual void f() {};
};

void foo4()
{
	Third* ps = new Third;
	First* pf = dynamic_cast<First*>(ps);  // vtable 드랍, 주소 변경 o
	First* pf2 = static_cast<First*>(ps);  // vtable 드랍, 주소 변경 o
	First* pf3 = reinterpret_cast<First*>(ps); // vtable 드랍, 주소 변경 x
	delete pf3;
}

pf3는 ps와 같은 주소를 가지고 있기 때문에 delete가 문제없이 된다.

 

static_pointer_cast<T>

template <class T, class U>
  shared_ptr<T> static_pointer_cast (const shared_ptr<U>& sp) noexcept;

shared_ptr의 바탕 클래스를 다른 클래스로 캐스팅된 shared_ptr를 반환해준다.