아래는 인포북 – More Effective C++에 있는 내용입니다.
항목 2 : 가능한 C+ + 스타일의 캐스트를 즐겨 쓰자
이번 항목에서는 goto와 함께 프로그래밍계의 1급 기피대상인 캐스트(cast, 형변환) 란 것에 대해 생각해 보기로 합시다. 그렇게 쓰지 말라고 많은 사람들이 목놓아 외침에도 불구하고 캐스트와 goto는 많은 코드에서 버젓이 한 자리 하고 있습니다. 왜냐하면 프로그램을 작성하면서 사태가 걷잡을 수 없이 악화되면 어쩔 수 없이 이런 것들이 필요해지기 때문입니다. 캐스트는 바로 이런 것들에 속합니다.
하지만, 어쨌든 C 스타일의 캐스트는 있어야 할 것이 못 됩니다. 우선, 이것의 첫째 문제는 C 스타일의 캐스트는 어떤 타입을 다른 타입으로 아무 생각 없이 바꾸어주는 괴물이나 마찬가지라는것입니다. 이런 방식의 캐스팅을 조금이라도 세심하게 조정해 주는 것이 있었으면 좋을 텐데 말이죠. 예를 들어, 상수(const) 객체에 대한 포인터를 비상수 객체에 대한 포인터로 바꾸어 주는 캐스트(즉, 어떤 객체의 상수성(constness)만을 바꾸는 캐스트)와 기본 클래스 객체에 대한 포인터를 파생 클래스 객체에 대한 포인터로 바꾸어 주는 캐스트(즉, 객체의 타입을 완전히 바꾸는캐스트)는 엄청나게 다르거든요.
전통적인 C 스타일 캐스트는 이런 사항에 대해 아무런 신경도쓰지 않습니다(사실 놀랄 것도 없죠. C++가 아니라 C 언어에서 통하도록 설계되었으니까요).C 스타일 캐스트가 가진 또 하나의 문제는 눈으로 찾아내기가 힘들다는 점입니다. 문법적으로 보면 캐스트는 식별자를 괄호로 둘러싼 것일 뿐입니다. 사실 C++ 코드에서 괄호와 식별자가 어디 그리 드문 건가요? 이렇기 때문에 캐스트에 관련된 아주 기본적인 질문, 이를테면 이 프로그램엔 캐스트 연산자가 들어 있나요? 같은 질문에도 답하기가 껄끄럽습니다. 왜냐하면, 인간의 눈이란 미물은 캐스트 부분을 잘 찾지 못하는 경우가 많고, grep1) 같은 도구를 쓰더라도 구문 구조상 매우 흡사한 괄호와 식별자가 섞인 부분과 구분할 수 없기 때문입니다.
C++는 이러한 C 스타일 캐스트의 문제를 보완하기 위해 C++ 스타일의 캐스트 연산자 네 가지를 새로 도입했습니다. 이 네 가지의 연산자는 static_cast , const_cast , dynamic_cast, reinterpret_cast 인데, 이 연산자에 대해 여러분이 알아야 하는 딱 한 가지는, C++ 스타일의 타입 캐스팅을 하려면 다음과 같이 쓰지 않고
(타입) 표현식
보통 다음과 같이 쓴다는 점입니다.
static_cast<타입> (표현식)
예를 하나 듭시다. int 타입의 변수를 double인 것처럼 행세하게 해서 부동소수점 실수를 결과로 내는 표현식에 넣고 싶습니다. 이때 C 스타일 캐스트를 사용한다면 다음과 같이 해야 합니다.
int firstNumber, secondNumber ;
. . .
double result = ( (double ) firstNumber ) / secondNumber ;
하지만 새로운 스타일의 캐스트를 사용한다면 다음과 같은 코드가 나옵니다.
double result = static_cast<double> (firstNumber) / secondNumber ;
이제는 인간의 눈이나 프로그램이나 발견하기 쉬운 캐스트가 되었습니다.
static_cast 는 C 스타일 캐스트와 똑같은 의미와 형변환 능력을 가지고 있는, 기본적인 캐스트 연산자입니다. C 스타일의 그것과 구실이 똑같다 보니 받는 제약도 똑같습니다. 예를 들어,struct 를 int 타입으로 바꾼다든지 double을 포인터 타입으로 바꾸는 일은 이것으로 할 수 없습니다. 게다가, static_cast 는 표현식이 원래 가지고 있는 상수성(constness)을 떼어버리지도 못합니다. 이런 일을 하는 캐스트 연산자인 const_cast 가 따로 있는 것을 보면 짐작할 수있지요.
나머지 세 가지의 C++ 캐스트 연산자는 좀 더 구체적인 목적을 위해 만들어졌습니다. const_cast는 표현식의 상수성이나 휘발성(volatileness)을 없애는 데에 사용합니다. 이 연산자가 쓰여진 소스를 만나면, 아, 이 개발자는 const 나 volatile로 선언한 변수라든지 이런 타입의 값을 내는 표현식에서 이런 특성만 바꾸고 싶어하는구나 라고 생각하면 되겠습니다. 이러한 프로그래머의 의도는 컴파일러에 의해서 더욱 확실해집니다. 즉, 상수성이나 휘발성을 제거하는 것 이외의용도로 const_cast 를 쓰면 통하지 않습니다. 다음 예제를 봅시다.
class Widget { . . . } ;
class SpecialWidget : public Widget { . . . } ;
void update (SpecialWidget *psw) ;
SpecialWidget sw; / / sw는 비상수 객체입니다.
const SpecialWidget & csw = sw; / / 그러나 csw는 이객체를
/ / 상수 객체인것처럼참조합니다.
update (&csw) ; / / 에러입니다! const SpecialWidget *는
/ / SpecialWidget *를 받는 함수에 넘길
/ / 수 없습니다.
update (const_cast <SpecialWidget *> (&csw) ) ;
/ / 이상 무. &csw의 상수성이명확하게
/ / 제거되었습니다(그리고 csw – sw
/ / 도 마찬가지 – 의 정보는 이제 update
/ / 안에서 바뀔 수 있습니다) .
update ( (SpecialWidget * ) &csw) ;
/ / 앞에서와 똑같습니다. 하지만두 눈을 부릅
/ / 떠야찾을 수 있는 C 스타일캐스트입니다.
Widget *pw = new SpecialWidget ;
update (pw) ; / / 에러입니다! pw의 타입은Wi d g e t *이지만,
/ / update 는 SpecialWidget *를 받습니다.
update (const_cast <SpecialWidget *> (pw) ) ;
/ / 에러입니다! const_cast 는 상수성이나
/ / 휘발성에 영향을 줄 때에만유효하고,
/ / 상속 관계를 하향시키는 캐스팅2 ) 등엔
/ / 쓸 수 없습니다.
지금까지는 객체의 상수성을 떼어 주는 const_cast 에 대해 알아보았습니다.
구체적인 용도를 가진 C++ 캐스트 연산자 두 번째는 dynamic_cast 입니다. 이 연산자는 상속
계층 관계를 가로지르거나 하향시킨 클래스 타입으로 안전하게 캐스팅할 때 사용합니다. 말하자
면, dynamic_cast 는 기본 클래스의 객체에 대한 포인터나 참조자의 타입을 파생(derived) 클래
스, 혹은 형제(sibling) 클래스의 타입으로 변환해 준다는 것입니다3). 캐스팅의 실패는 널 포인터
(포인터를 캐스팅할 때)나 예외(참조자를 캐스팅할 때)를 보고 판별할 수 있습니다.
Widget *pw;
. . .
update (dynamic_cast <SpecialWidget *> (pw) ) ;
/ / 이상 무. update 에포인터pw를
/ / SpecialWidget 에 대한 포인터로서 넘깁니다.
/ / 물론 pw가그 객체를 가리켜야되고,
/ / 그렇지 않으면널 포인터가 넘어갑니다.
void updateViaRef(SpecialWidget& rsw);
updateViaRef(dynamic_cast <SpecialWidget&> (*pw) ) ;
/ / 이상 무. pw가 그 객체를 참조하고 있으면
/ / updateViaRef 에 *pw를 SpecialWidget
/ / 의 참조자 타입으로 넘깁니다. 실패하면
/ / 예외가 발생합니다.
dynamic_cast 에도 제약이 있습니다. 이 연산자는 상속 계층 구조를 오갈 때에만 사용해야 합니다. 게다가 가상 함수가 없는 타입에는 적용할 수 없고(자세한 내용은 항목 24를 참조하세요), 상수성 제거에도 쓸 수 없습니다.
int firstNumber, secondNumber;
. . .
double result = dynamic_cast<double>(firstNumber)/secondNumber;
/ / 에러입니다! 가상 함수가전혀 없습니다.
const SpecialWidget sw;
. . .
update( dynamic_cast<SpecialWidget*>(&sw) );
/ / 에러입니다! dynamic_cast 는 상수성을
/ / 제거하는데쓸 수 없습니다.
3) 이외에, dynamic_cast는 어떤 객체가 차지하고 있는 메모리의 시작부분을 찾는 데에도 씁니다. 이것에 관해서는 항목27에서 알아보기로 합시다.
대신에, 상속 관계에 있지 않은 타입을 캐스팅할 때에는 static_cast 를 쓰고, 상수성을 제거하려면 const_cast 를 쓰면 되겠지요.
네 가지 C++ 캐스트 연산자의 마지막은 reinterpret_cast 입니다. 이 연산자가 적용된 후의변환 결과는 거의 항상 컴파일러에 따라 다르게 정의되어 있습니다. 따라서, 이 연산자가 쓰인소스는 직접 이식이 불가능합니다.
reinterpret_cast 의 가장 흔한 용도는 함수 포인터 타입을 서로 바꾸는 것입니다. 예를 들어, 어떤 특정한 타입의 함수 포인터를 배열로 만들어 놓았다고 가정합시다.
typedef void (*FuncPt r) ( ) ; // FuncPtr 은 인자를 받지 않고
/ / void를 반환하는 함수에 대한
/ / 포인터입니다.
FuncPtr funcPtrArray[10]; // funcPtrArray는 10개의 FuncPtr로 만들어진 배열입니다.
이때 다음의 함수에 대한 포인터를 funcPtrArray에 넣어야 할 피치 못할 사정이 생겼습니다.
int doSomething();
간단할 것 같지만 캐스팅을 하지 않으면 절대로 안 됩니다. 왜냐하면 doSomething은 funcPtrArray에 넣기에는 타입이 맞지 않기 때문입니다. 이 배열에 들어가는 함수는 void를 반환하지만, doSomething은 i nt 를 반환하지 않습니까?
funcPtrArray[ 0 ] = &doSomething ; / / 에러입니다! 타입불일치이군요.
reinterpret_cast 를 쓰면 컴파일러에게 이 일을 강제로 시킬 수 있습니다.
funcPtrArray[ 0 ] = / / 이것은 컴파일됩니다.
reinterpret_cast <FuncPtr> (&doSomething) ;
함수 포인터의 캐스팅은 소스의 이식성을 떨어뜨리고(C++ 스펙에는 모든 함수 포인터를 똑같은방법으로 나타내야 한다는 보장이 전혀 없습니다), 어떤 경우에 이런 캐스팅은 잘못된 결과를 낳기도 하기 때문에(항목 31을 보십시오), 여러분 목에 칼이 들어오기 전에는 함수 포인터의 캐스팅은 어떻게든 피하세요.
컴파일러가 C++ 캐스트 연산자를 지원하지 않는 컴파일러를 쓰는 분은 우울하십니까? 우울해 하지 마세요. 그런 경우에는 static_cast , const_cast, reinterpret_cast 대신에 전통적인캐스팅을 써도 됩니다. 더욱이, 다음과 같이 매크로를 만들면 모양도 흉내낼 수 있습니다.
#define static_cast(TYPE, EXPR) ( (TYPE) (EXPR) )
#define const_cast(TYPE, EXPR) ( (TYPE) (EXPR) )
#define reinterpret_cast(TYPE, EXPR) ( (TYPE) (EXPR) )
최신 컴파일러가 무색할 정도로 코드의 모양이 흡사하게 만들어질 것입니다. 다음처럼 말입니다.
double result = static_cast(double, firstNumber) / secondNumber;
update( const_cast(SpecialWidget*, &sw) );
funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething);
물론, 이런 방법은 근접하게 구현한 것일 뿐이지, 진짜 연산자들처럼 안전한 캐스팅을 수행하는것은 아닙니다. 하지만 여러분의 구식 컴파일러가 최신의 캐스트 연산자를 지원하게 되면, 코드를 간단히 업그레이드할 수 있겠지요?
dynamic_cast 의 경우는 어떻게 쉽게 흉내낼 수 있는 방법이 없습니다. 하지만 많은 라이브러리들이 안전한 상속 기반의 캐스팅 장치를 함수나 매크로 등으로 제공하고 있습니다. 이런 함수들을 아무리 찾아봐도 없는데 다운 캐스팅을 꼭 해야 한다면 어쩔 수 없지만, C 스타일 캐스트로또 되돌아가야 합니다. 그렇지만 캐스팅 실패를 판별할 수 있는 기능은 처음부터 물 건너간 것입니다. 앞의 것들과 마찬가지로 dynami c_ca s t 도 매크로로 만듭니다.
#define dynamic_cast( TYPE, EXPR ) ( (TYPE) (EXPR) )
역시 기억해 둘 점은, 이 매크로는 흉내일 뿐이지 진정한 dynami c_ca st 가 아니라는 점이겠지요. 캐스팅이 실패했는지를 판별할 방법이 없습니다.
아, 저도 눈치 10단입니다. C++ 스타일의 캐스트 연산자는 보기에 썩 좋은 편도 아니고 키보드치기도 힘듭니다. 보기가 불편해서 못쓰겠다는 분들은 최신의 C++ 표준안에서도 C 스타일 캐스트가 여전히 허용된다는 점을 위안으로 삼으세요. 그러나 컴파일러와 다른 프로그래머에게 확실한 의미와 인식성을 선사한다는 점만 보아도, C++ 스타일 캐스트의 추한 외모는 그리 튀는 단점이라고 생각되지 않습니다. C++ 캐스트 연산자가 쓰인 프로그램은 읽어 내기에도 쉽고(사람이나도구나), 이것이 없으면 찾아낼 수 없는 캐스팅 에러를 컴파일러가 대신 찾아내도록 하기 때문에훨씬 좋습니다. 이 정도면 C 스타일 캐스트를 갖다 버려야 할 충분한 이유가 되겠지요? 하나 더있습니다. 캐스트 연산자를 보기에 깔끔하지 않고 게다가 타이핑까지 어렵도록 만든 것은 아주잘 한 일일지도 모른다는 거죠.