27: 캐스팅은 절약, 또 절약! 잊지 말자
일단 캐스팅은 C++ 스타일의 캐스팅을 쓰는 것이 바람직하다. 우선, 코드를 읽을 때 알아보기 쉽고, 소스 코드의 어디에서 C++의 타입 시스템이 망가졌는지를 찾아보는 작업이 편해진다. 둘째, 캐스트를 사용한 목적을 좀 더 좁혀서 지정하기 때문에 컴파일러 쪽에서 사용 에러를 진단할 수 있다.
캐스팅이 있으면 이로 런타임에 실행되는 코드가 만들어지는 경우가 정말 적지 않다.
int x, y;
double d = static_cast<double>(x) / y; // x를 y로 나눈다. 부동소수점 나눗셈 사용
위의 코드를 보면 int 타입의 x를 double 타입으로 캐스팅한 부분에서 코드가 만들어진다. 그것도 거의 항상. 왜냐하면 대부분의 컴퓨터 아키텍처에서 int의 표현구조와 double의 표현구조가 아예 다르기 때문이다. 다음의 예제도 그렇다.
class Base { ... };
class Derived: public Base { ... };
Derived d;
Base* pb = &d; // Derived*에서 Base*의 암시적 변환이 이루어진다.
보다시피 파생 클래스 객체에 대한 기본 클래스 포인터를 만드는(초기화하는) 지극히 흔하디 흔한 코드이다. 그러나 두 포인터의 값이 같지 않을 때도 가끔 있다. 이런 경우가 되면, 포인터의 변위(offset)를 Derived* 포인터에 적용하여 실제의Base* 포인터 값을 구하는 동작이 바로 런타임(runtime)에 이루어진다. C++를 쓸 때는 데이터가 어떤 식으로 메모리에 박혀 있을 거라는 섣부른 가정을 피해야 한다.
class Window {
public:
virtual void OnResize() {}
};
class SepcialWindow : public Window {
public:
virtual void onResize() {
static_cast<Window>(*this).OnResize();
}
};
가상 함수를 파생 클래스에서 재정의해서 구현할 때 기본 클래스의 버전을 호출하는 문장을 가장 먼저 넣어달라는 요구사항을 만족하려고 위처럼 구현하였다. 하란 대로 구현했지만 이는 틀린 코드이다.
위에서 onResize 함수 호출이 이루어지는 객체는 현재의 객체가 아니다. 이 코드에서는 캐스팅이 일어나면서 *this의 기본 클래스 부분에 대한 사본이 임시적으로 만들어지게 되어 있는데, 지금의 onResize는 이 임시 객채에서 호출된 것이다. 다시 말해, 기본 클래스 부분의 사본에 대고 Window::onResize를 호출하는 것이다.
이 문제를 해결하려면 일단 캐스팅을 빼야 한다. 그냥 현재 객체에 대고 onResize의 기본 클래스 버전을 호출하도록 만들면 되는 것이다.
class SepcialWindow : public Window {
public:
virtual void onResize() {
Window::onResize();
}
};
dynamic_cast는 정말 느리게 구현되어 있다. 수행 성능에 사활이 걸린 코드라면 특히 dynamic_cast에 주의를 놓지 말아야 한다. 하지만 그 객체를 조작할 수 있는 수단으로 기본 클래스의 포인터(혹은 참조자)밖에 없을 경우는 적지 않게 생긴다.
class Window {};
class SpecialWindow : public Window {
public:
void blink();
};
typedef vector<tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
// 그다지 바람직스럽지 않은 코드: dynamic_cast를 쓰고 있다.
if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get()))
{
psw->blink();
}
}
이 문제를 해결하기 첫 번째 방법은, 파생 클래스 객체에 대한 포인터를 컨테이너에 담아둠으로써 각 객체를 기본 클래스 인터페이스를 통해 조작할 필요를 아예 없애 버리는 것이다.
typedef vector<tr1::shared_ptr<SpecialWindow>> VPW;
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter)->blink();
}
다른 방법은 원하는 조작을 가상 함수 집합으로 정리해서 기본 클래스에 넣어두면 된다. 여기서는 아무것도 안 하는 기본 blink를 구현해서 가상 함수로 제공하는 것이다.
class Window {
public:
virtual void blink() {}
};
class SpecialWindow : public Window {
public:
virtual void blink();
};
typedef vector<tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter)->blink();
}
상당히 많은 상황에서 dynamic_cast를 쓰는 방법 대신에 꽤 잘 쓸 수 있다.
정말 피해야 하는 설계는 바로 '폭포식(cascading) dynamic_cast'라고 불리는 구조이다.
캐스팅을 해야 하는 코드를 내부 함수 속에 몰아 놓고, 그 안에서 일어나는 '천한' 일들은 이 함수를 호출하는 외부에서 알 수 없도록 인터페이스로 막아두는 식으로 해결하면 된다.
이것만은 잊지 말자!
- 다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서 dynamic_cast는 몇 번이고 다시 생각해라. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해 보자.
- 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 해 보자. 이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출할 수 있게 된다.
- 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하자. 발견하기도 쉽고, 설계자가 어떤 역할을 의도했는지가 더 자세하게 드러난다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 29: 예외 안정성이 확보되는 그날 위해 싸우고 또 싸우자! (0) | 2023.10.28 |
---|---|
[Effective C++] 28: 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자 (0) | 2023.10.26 |
[Effective C++] 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2023.10.23 |
[Effective C++] 25: 예외를 던지는 swap에 대한 지원도 생각해 보자 (1) | 2023.10.22 |
[Effective C++] 24: 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (1) | 2023.10.21 |