- 캐스팅은 되도록 하지 마라. 특히 수행 성능에 민감한 코드에서 dynamic_cast는 더욱 하지 마라.
- 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있는지 확인해라. 가능하다면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않는 대신 함수를 호출하여 처리할 수 있다.
- 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호해라. 더욱 읽기 좋은 코드가 된다.
캐스팅은 되도록 하지 마라
C++에서는 어떤 일이 있어도 타입 에러가 생기지 않도록 보장해야 한다.
타입 에러가 발생하는 원인 중 가장 큰 것이 바로 캐스트(cast)이다.
일단 캐스팅 문법을 정리하면 다음과 같다.
- (T) 표현식: 표현식 부분을 T타입으로 캐스팅
- T(표현식): 표현식 부분을 T타입으로 캐스팅
이 두 개는 C에서 사용하는 구형 스타일의 캐스트라고 할 수 있다.
- const_cast<T>(표현식): 객체의 상수성을 없애는 용도로 사용, C++ 스타일 캐스트 중 유일하다.
- dynamic_cast<T>(표현식): 안전한 다운캐스팅을 할 때 사용, 주어진 객체가 어떤 클래스 상속 계통에 속한 타입인지 확인, 런타임 비용이 높은 캐스트 연산자이다.
- reinterpret_cast<T>(표현식): 포인터를 int로 바꾸는 등의 하부 수준 캐스팅을 한다. 적용 결과는 구현환경에 의존적이다
- static_cast<T>(표현식): 암시적 변환을 강제로 진행할 때 사용한다. 흔히 타입변환을 거꾸로 수행하는 용도로도 쓰인다.
구형 스타일의 캐스트는 여전히 적법하다. 하지만, C++ 스타일의 캐스트를 쓰는 것이 바람직하다.
코드도 읽기 쉬울 뿐 아니라 캐스트를 사용한 목적을 확실히 할 수 있어 컴파일러가 에러를 진단하기 쉬워진다.
캐스팅은 단순히 타입을 변경하는 것에 그치지 않는다.
일단 타입 변환이 있으면 이로 인해 런타임에 실행되는 코드가 만들어지는 경우가 적지 않다.
int x, y;
...
double d = static_cast<double>(x) / y;
int 타입의 x를 double 타입으로 캐스팅한 부분에서 코드가 만들어진다.
대부분의 컴퓨터 아키텍처에서 int의 표현구조와 double의 표현구조가 다르기 때문이다.
다음은 또 다른 예시이다.
class Base { ... };
class Derived: public Base { ... };
Derived d;
Base *pb = &d; // Derived* -> Base* 암시적 변환
파생 클래스 객체에 대한 기본 클래스 포인터를 만드는 코드이다.
그런데 두 포인터의 값이 같지 않을 때도 가끔 있다.
포인터의 변위를 Derived* 포인터에 적용하여 실제의 Base* 포인터 값을 구하는 동작이 바로 런타임에 이루어진다.
객체 하나가 가질 수 있는 주소가 한 개가 아닐 수 있다는 얘기이다.
다중 상속이 사용되면 이런 현상이 항상 생기지만, 단일 상속인데도 이렇게 되기도 한다.
이러한 점을 보면, C++에서는 데이터의 메모리 배치를 함부로 예상해서 사용하면 안 된다.
객체의 메모리 배치구조를 결정하는 방법과 객체의 주소를 계산하는 방법은 컴파일러마다 다르다.
즉, 사용환경이 달라지면 코드가 유효하지 않을 수 있다는 뜻이다.
캐스팅의 또 다른 문제점은 문제가 없어 보이지만 실제로는 틀린 경우가 많다는 것이다.
가상 함수를 파생 클래스에서 재정의해서 구현할 때 기본 클래스의 버전을 호출하는 문장을 가장 먼저 넣어야 하는 경우가 있다.
class Window {
public:
virtual void onResize() { ... }
...
};
calss SepcialWindow: public Window {
public:
virtual void onResize() {
static_cast<Window>(*this).onResize(); // 동작 X
...
}
...
};
기본 클래스인 Window 가 있고 파생 클래스인 SepcialWindow 가 있다.
onResize라는 함수를 가상 함수로 정의하고 있고, SpecialWindow에서 Window의 onResize를 호출해야 하는 상황이다.
하지만, 위의 코드는 동작하지 않는다.
SepcialWindow에서 호출한 onResize는 Window::onResize가 된다. 하지만, 함수 호출이 이루어지는 객체는 현재 객체가 아니다.
캐스팅이 일어나면서 *this의 기본 클래스 부분에 대한 사본이 임시적으로 만들어지게 되며 이에 해당하는 onResize가 호출된다.
즉, 임시의 Window 객체 사본에 대해 Resize를 진행한 후 현재 객체인 SpecialWindow의 남은 부분을 처리하는 상황이다.
이 문제를 해결하려면 캐스팅을 빼야 한다.
현재 객체의 onResize를 기본 클래스 버전으로 호출하면 되는 것이다.
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize();
...
}
...
};
dynamic_cast
캐스팅을 사용해야 하는 상황이 생긴다면 뭔가 이상해질 수 있다는 것을 암시한다.
특히 dynamic_cast를 써야 하는 상황이면 더욱 그렇다.
dynamic_cast는 매우 느리게 구현되어 있다. 예를 들어, 어떤 환경에서는 클래스 이름에 대한 문자열 비교 연산자에 기반을 두어 구현되었다. (strcmp)
이러한 환경에서, 상속 깊이가 깊어지거나 다중 상속이 사용된다면 비용은 더욱 커질 것이다.
그럼에도, dynamic_cast를 사용해야 하는 경우가 꽤 있다.
파생 클래스 객체임이 분명하고 이에 대해 파생 클래스의 함수를 호출하고 싶은데 객체의 핸들이 기본 클래스의 포인터밖에 없는 경우가 있다.
class Window { ... };
class SpecialWindow: public Window {
public:
void blink();
...
};
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtr.begin(); iter != windPtrs.end(); ++iter)
{
if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
{
psw->blink();
}
}
이 문제를 해결하는 방법은 두 가지이다.
- 파생 클래스 객체에 대한 포인터를 컨테이너에 담아서 기본 클래스 인터페이스를 통해 조작할 여지를 없애라.
class Window { ... };
class SpecialWindow: public Window {
public:
void blink();
...
};
typedef std::vector<std::tr1::shared_ptr<SepcialWindow>> VPSW;
VPSW winPtrs;
...
for(VPSW::iterator iter = winPtr.begin(); iter != windPtrs.end(); ++iter)
{
(*iter)->blink();
}
이렇게 하면 Window에서 파생된 모든 클래스를 같은 컨테이너로 관리할 수 없게 된다.
- 가상 함수를 아무것도 하지 않게 기본 클래스에 넣어두어라.
class Window {
public:
virtual void blink() {} // 아무것도 하지 않음
...
};
class SpecialWindow: public Window {
public:
virtual void blink() { ... } // 특정한 동작 수행
...
};
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end()); ++iter)
{
(*iter)->blink();
}
두 가지 방법 모두 모든 상황에 적용하는 것은 불가능하지만, 꽤 효과적인 해결법이다.
dynamic_cast에서 꼭 피해야 하는 설계가 있다.
바로 폭포식 dynamic_cast이다.
class Window { ... };
...
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
if(SpecialWindow1 *psw1 = dynamic_cast<SpecialWindow1*>(iter->get())) { ... }
else if(SpecialWindow2 *psw2 = dynamic_cast<SpecialWindow2*>(iter->get())) { ... }
else if(SpecialWindow3 *psw3 = dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
...
}
dynamic_cast는 속도가 매우 느린 연산인데 범벅이 되어버려 성능이 좋을 수 없다.
정리
잘 작성된 C++ 코드는 캐스팅을 거의 쓰지 않는다. 하지만 캐스팅을 아예 안쓸 수는 없다.
int를 double로 바꾸는 경우는 말이 안 되지 않는다.
그럼에도 최대한 사용하지 않는 것이 좋다.
만약 꼭 써야 하는 상황이면 캐스팅을 해야 하는 코드를 함수에 몰아 놓고, 외부에 노출하지 않도록 막아두는 식으로 처리하는 것이 좋다.