컨텐츠 바로가기

[C++] static_cast, dynamic_cast, const_cast, reinterpret_cast

http://sweeper.egloos.com/1907485

아래 내용은 MEC++ 2장을 토대로 개인 견해와 예제 코드를 곁들여 작성되었다.

--------------------------------------------------------------------------------

1. C 스타일 캐스트와 C++ 캐스트 간략 소개

캐스트(cast, 형변환)은 A 타입에서 B 타입으로 명시적으로 타입 변환을 하기 위해 사용된다.

우선, 전통적인 C 스타일 캐스팅은 가급적 지양하는 것이 좋다.
이것의 첫째 문제는 C 스타일의 캐스트는 어떤 타입을 다른 타입으로 아무 생각없이 바꾸어주는 괴물이나 마찬가지라는 것이다.

이런 방식의 캐스팅을 조금이라도 세심하게 상황에 맞게 조정해 주는 것이 필요하다.

예를 들어, const 객체에 대한 포인터를 non-const 객체에 대한 포인터로 바꾸어 주는 캐스트와 기본 클래스 객체에 대한 포인터를 파생 클래스 객체에 대한 포인터로 바꾸어 주는 캐스트는 그 동작 자체가 엄청나게 다르기 때문이다.

하지만, 전지전능하신 C 스타일 캐스팅은 이러한 세부사항들에 대해 전혀 신경을 쓰지 않는다. So cool~

또 하나의 마이너한 문제는 코드 가독성에 관련된 것인데...

C 스타일 캐스트가 (type-id)(e-pression)의 형식이라 프로그래머가 명시적으로 캐스트가 사용된 코드를 분간하기가 쉽지는 않다.
C/C++ 코드에서 괄호가 어디 한두개인가?

C++은 이러한 C 스타일 캐스트의 문제를 보완하기 위해 C++ 스타일의 캐스트 연산자 네 가지를 새로 도입했다.
  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
기존 C 스타일 캐스트의 문법이 (type-id)(e-pression) 이었다면, C++ 캐스터들은 캐스터<type-id>(e-pression)의 문법 형식을 따른다.

예를 들어서...
C 스타일에서 (int)(fWeight)으로 캐스팅 하던 것을, C++ 스타일에선 static_cast<int>(fWeight); 와 같이 사용한다는 것이다.

이렇게 naming이나 형식 등의 차이점으로 인해 명확하게 캐스팅이 사용되었음을 쉽게 확인할 수 있다.

이제 각 캐스터의 의미에 대해 하나씩 상세하게 알아보자.

설명의 편의를 위해 설명의 순서는 const->reinterpret->static->dynamic의 순으로 하겠다.


2. const_cast


const_cast는 표현식의 상수성(const)을 없애는 데 사용된다.
물론 const_cast로 표현식에 상수성을 부여할 수도 있지만, 이는 굳이 캐스터가 필요없기에 거의 사용되지 않는다.

그리고, 상수성 제거하는 것 이외의 용도로는 const_cast를 써봤자 먹히지 않는다.

아래 예제를 보자.

1) 상수성 부여

char chArray[] = "Hello";

// 아래 두 개는 똑같은 행위를 한다.
// 따라서, 굳이 뭐 캐스터를 붙여 쓸 필요가 없다.
const char* chPointer = chArray;
const char* chPointer = const_cast<const char*>(chArray);

// const char* 이므로 데이터 변경 불가
*chPointer = 'X';

2) 상수성 제거

class Widget {...};
class SpecialWidget : public Widget {...};

void Update(SpecialWidget *psw);

SpecialWidget sw;
const SpecialWidget& csw = sw;

// 에러, const SpecialWidget*를 SpecialWidget*를 받는 함수에 넘기려 함
Update(&csw);

// OK, const가 명확히 제거됨.
Update(const_cast<Widget*>(&csw));

// 이것도 OK이지만, C 스타일 캐스트. 피하자~
Update((Widget*)(&csw));


3. reinterpret_cast


reinterpret_cast는 어떠한 포인터 타입도 어떠한 포인터 타입으로든 변환이 가능하다.
즉, 아래와 같은 것들이 가능하다는 것이다.
  • 어떠한 정수 타입도 어떠한 포인터 타입으로 변환이 가능하고, 그 역(포인터 타입->정수 타입)도 가능하다.
  • int* 에서 char* 로, 또는 One_Class* 에서 Unrelated_Class* 로도 가능하다.
얼핏 봤을 때 상당히 자유롭고 강력한 캐스터 같지만, 특수한 케이스가 아니면 사용하지 않는 것을 권한다.

우선, 전통적인 캐스팅의 개념에서 벗어날 수 있는 포인터 변환 등이 reinterpret_cast를 씀으로써 강제 형변환되기 때문이다.
변환 관계에 놓인 두 개체의 관계가 명확하거나, 특정 목적을 달성하기 위할 때만 사용하는 것이 바람직하다.

게다가 이 연산자가 적용된 후의 변환 결과는 거의 항상 컴파일러에 따라 다르게 정의되어 있다.
따라서, 이 캐스팅 연산자가 쓰인 소스는 직접 다른 곳에 소스 이식이 불가능할 수 있다. (거의 그렇다)

다음은 MSDN의 예제 코드이다.

// expre_reinterpret_cast_Operator.cpp 
// compile with: /EHsc 

#include <iostream

// 포인터의 주소에 기반하여 해쉬 값을 반환함.
unsigned short Hash( void *p ) 
{
// reinterpret_cast로 void* -> int 형변환
// 물론 C 스타일 캐스팅 : val = (unsigned int)p; 로도 가능하긴 하다.
unsigned int val = reinterpret_cast<unsigned int>( p ); 
return ( unsigned short )( val ^ (val >> 16) ); 
}

int main() 
int a[20]; 

for ( int i = 0; i < 20; i++ ) 
std::cout << Hash( a + i ) << std::endl
}


4. static_cast


static_cast는 C 스타일 캐스터와 똑같은 의미와 형변환 능력을 가지고 있는, 가장 기본적인 캐스트 연산자이다.
C 스타일의 그것과 구실이 똑같다 보니 받는 제약도 똑같다.
 
예를 들어, struct 타입을 intdouble 타입으로 형변환 할 수 없고, float 타입을 포인터 타입으로 형변환 할 수 없다.
게다가, static_cast는 표현식이 원래 가지고 있는 상수성(constness)를 떼어버리지도 못한다.
(이를 위해 별도로 const_cast가 존재한다)

헌데 여기까지만 설명하면 static_cast라는 네이밍에 대해 의문이 발생한다.
C 스타일 캐스터와 그리고 같기만 하다면, c_cast 라던지 하지 굳이 왜 static_cast라는 이름을 붙였을까?

static_cast는 형변환에 대한 타입체크를 run-time에 하지않고, compile 타임에 정적으로 수행한다.
즉, 다음에 소개될 dynamic_cast(run-time 타입체크)와 그 타입체크 시점이 정반대이다.

특히, 상속관계에 있는 클래스 객체간 형변환에 대해 static_cast와 dynamic_cast의 차이점이나 용도에 대해선 따로 챕터를 떼어 자세히 작성하겠다.


5. dynamic_cast


dynamic_cast는 런타임에 (동적으로) 상속 계층 관계를 가로지르거나 다운캐스팅시 사용되는 캐스팅 연산자이다.
(물론, 업캐스팅시에도 사용할 수 없지만, 업캐스팅시엔 굳이 캐스터 연산자를 쓸 필요가 없으므로...)

말하자면, dynamic_cast는 기본 클래스 객체에 대한 포인터나 참조자의 타입을 파생 클래, 혹은 형제 클래스의 타입으로 변환해 준다는 것이다.

캐스팅의 실패는 NULL(포인터)이거나 예외(참조자)를 보고 판별할 수 있다.

즉, dynamic_cast는 런타임에 다형성을 이용하여 모호한 타입 캐스팅을 시도할 때 (다형성 위배),
엉뚱한 변환 결과가 넘어가지 않도록 하여, 런타임 오류가 방지하는 역할을 한다.

왜 다형성이라는 글자를 bold 로 표현했겠는가?
클래스가 단순히 상속 관계에 있다고 해서 이것이 다형성을 가진다고 얘기할 순 없다.
다형성을 가지려면 virtual 멤버 함수가 존재해야 한다.
(상속 관계에 있지만 virtual 멤버 함수가 하나도 없다면 다형성을 가진게 아니라 단형성이다)

dynamic_cast는 다형성을 띄지 않은 객체간 변환은 불가능하며, 시도시 컴파일 에러가 발생한다.

이처럼 C++ RTTI에 의존적이므로, 캐스팅 연산 비용은 꽤나 비싼 편이다.
연산 비용은 상속 체계의 복잡도와 깊이가 커지고 깊어질수록 더 증가한다.

다음은 static_cast와 dynamic_cast를 사용할 경우의 Disassembly 예제 내용이다.

class BaseClass {...};
class DerivedClass : public BaseClass {...};

BaseClass* pBC = new DerivedClass;

// 정적으로 형변환. 형변환 자체만 수행
DerivedClass* pSDC = static_cast<DerivedClass*>(pBC);
mov eax, dword ptr [ebp - 14h]
mov dword ptr [ebp - 20h], eax

// 런타임에 동적으로 형변환 및 RTTI 체크
DerivedClass* pSDC = dynamic_cast<DerivedClass*>(pBC);
push 0
push  offset DerivedClass 'RTTI Type Descriptor' (0C7A01Ch)
push  offset BaseClass 'RTTI Type Descriptor' (0C7A094h)
push  0
mov eax, dword ptr [ebp - 14h]
push eax
call @ILT+715(___RTDynamicCast) (0C712D0h)
add esp, 14h
mov dword ptr [ebp - 2Ch], eax

어떤가? dynamic_cast의 비용이 느껴지는가?


6. static_cast VS. dynamic_cast (for 상속관계 클래스 형변환)

<static_cast>
정적으로 형변환을 해도 아무런 문제가 없다는 것은 이미 그 녀석이 정확히 어떤 녀석인지 알고 있을 경우에 속할 것이고,

<dynamic_cast>
동적으로 형변환을 시도해 본다는 것은 이 녀석의 타입을 반드시 질의해 봐야 된다는 것을 의미한다.

위에서 dynamic_cast의 수행 비용에 대해 언급했었다.
즉, RTTI를 해야 하는 경우엔 dynamic_cast를 이용해 런타입의 해당 타입을 명확히 질의해야 하고, 그렇지 않은 경우엔 static_cast를 사용하여 변환 비용을 줄이는 것이 좋다.

아래 예제들을 살펴보자.

// 아래 클래스들은 다형성을 제대로 갖춘 상속 관계를 가진다고 가정한다.
// 즉, 코드에는 없지만(귀차니즘), 가상 함수들이 존재하는 것이다.

// 비행기에 여러 직군의 사람들이 탑승했다.
// 한 승객이 갑자기 급성 맹장염에 걸려 의사가 급하게 수술을 해야 한다.

class Passenger {...};
class Student : public Passenger
{
...
void Study();
};
class Teacher : public Passenger
{
...
void Teach();
};
class Doctor : public Passenger
{
...
void Treat();
void Operate();
};

int main()
{
typedef vector<Passenger *> PassengerVector;
PassengerVector passengerVect;

Passenger* pPS = new Student();
if ( pPS )
{
passengerVect.push_back( pPS );
// 비행기 타자마자 공부한다고 치고~
// pPS가 명확하게 어느 클래스의 인스턴스인지 알고 있다.
// 이 경우엔 굳이 비용이 들어가는 dynamic_cast가 아닌, static_cast를 쓰는게 낫다.
Student* pS = static_cast<Student *>( pPS );
pS->Study();
}

Passenger* pPT = new Teacher();
if ( pPT )
{
passengerVect.push_back( pPT );
}
// Doctor 역시 비슷하게 추가.

...

// 응급 환자 발생. passengerVect 중 의사가 있다면 수술을 시켜야 한다.
PassengerVect::iterator bIter(passengerVect .begin());
PassengerVect::iterator eIter(passengerVect .end());
for( ; bIter != eIter; ++bIter )
{
// Passenger 포인터로 저장된 녀석들 중 누가 의사인지 구분해야 한다.
// 런타임 다형성 체크에 의해 Doctor가 아닌 녀석들에 대한 형변환 결과는 NULL
Doctor* pD = dynamic_cast<Doctor *>(*bIter);
if ( pD )
{
pD->Operate();
}
}
}

만약, 위 코드의 전체 승객 중 의사를 찾아내는 과정에서 dynamic_cast가 아니라, static_cast를 사용하였다면 어떻게 될까?

static_cast는 동적 타입체크를 하지 않고, Student와 Teacher는 Person의 파생 클래스이므로 변환 연산 규정에도 위배되지 않으므로, 그냥 타입 변환이 일어난다.

하지만, 변환 결과는 애초 기대했던 바와 전혀 다르다.
실제 Student 클래스 타입이지만, Doctor 클래스 타입으로 타입 변환이 되면서...
  • Doctor 클래스 고유 멤버 함수에 대한 접근이 불가능해진다. 
  • 포인터가 가리키는 메모리 내용을 Doctor 클래스에 맞춰서 해석하기에 Student의 내용 중 일부가 Doctor 멤버 필드에 엉뚱하게 들어가거나, 슬라이스 문제가 발생할 수 있다.
다시 말해, 껍데기만 Doctor 클래스이지 내용은 전혀 Doctor의 것이 아니게 된다는 것이다.
이때 멤버 필드에 접근시 엉뚱한 값이 들어가 있거나, 런타임 오류가 발생할 수 있게 된다.

위 예제를 잘 보고 언제 static_cast와 dynamic_cast를 구분해서 쓰는 게 좋은지 잘 이해해야 한다.


덧글|덧글 쓰기|신고