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를 반환해준다.
'Advanced C++' 카테고리의 다른 글
[Modern C++] (5-2) 오른값 참조, 이동 의미론, 완벽전달 (0) | 2021.04.14 |
---|---|
[Modern C++] (5-1) 오른값 참조, 이동 의미론, 완벽전달 (0) | 2021.04.12 |
[C++] 형 변환 type casting (0) | 2021.04.01 |
[Modern C++] (3-2) 현대적 C++에 적응하기 (0) | 2021.03.26 |
[Modern C++] (3-1) 현대적 C++에 적응하기 (0) | 2021.03.25 |