[Effective C++] #27_캐스팅은 절약하고 또 절약하자!
Scott Meyers의 "Effective C++" 를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 5장 '구현', 항목 27 "캐스팅은 절약, 또 절약! 잊지 말자"에 해당하는 내용입니다.
Casting?
우리 말로 "형변환"으로 잘알려진 Casting은, 자료형간의 형변환 혹은 포인터 간의 형변환을 위해 사용됩니다.
C언어의 Cast
(T) A // A를 Typename으로 캐스트 합니다.
T (A) // 함수 방식의 캐스트
class A {
public:
explicit A (int size);
...
};
void doSoemthing(const A& a);
doSomething(A(15)); // 2번 방식의 캐스트로, 정수 "15"로 부터 클래스 A를 생성
C++의 캐스트 종류 네 가지
- const_cast<T> (A) : 객체의 상수성 제거 용도
- dynamic_cast<T>) (A) : 안전한 다운 캐스팅, 런타임 비용이 높다!
- reinterpret_cast<T> (A) : 포인터를 Int로 바꾸는 등의 하부 수준의 캐스팅, 구현환경에 의존적!
- static_cast<T> (A) : 암시적 변환, 비상수 객체를 상수 객체로, int를 double로 변환할때 사용.
1. Cast 사용으로 인해 발생할 수 있는 문제
class Base {...};
class Derived : public Base {...};
Derived d;
Base *pb = &d; // *Derived 에서 *Base로 암시적 변환
위 코드는 파생 클래스 객체에 대한 기본 클래스 포인터를 초기화하는 코드입니다. 하지만, 두 포인터의 값이 항상 같지 않을 때도 존재합니다. 따라서, 포인터의 변위를 Derived* 포인터에 적용하여 실제 Base* 포인터 값을 구하는 동작이 런타임에 이루이집니다. 객체 하나가 가질 수 있는 주소가 오직 한개가 아니라 그 이상이 될 수 있음을 보여주는 사례입니다. C++에서는 다중 상속이 사용되면 이런 현상이 벌어지며, 단일 상속의 경우도 존재하죠.
2. Cast 사용으로 인해 발생할 수 있는 문제
class Window {
public:
virtual void onResize() {...};
...
};
class SpecialWindow : public Window {
public:
virtual void onResize() {
static_cast<Window>(*this).onResize(); // 파생 클래스의 onResize 결과를 Window로 캐스팅
...
}
...
};
위 코드는 *this를 Window로 캐스팅합니다. 따라서, 호출되는 onResize 함수는 Window 클래스의 것이죠. 하지만 여기서 문제가 발생합니다. 이 코드에서, 캐스팅이 발생함과 동시에 *this의 기본클래스 부분에 대한 사본이 임시 생성되어, 지금 사용되는 onResize 함수는 이 임시 객체에서 호출된 것입니다. 따라서, 현재 객체에 대해 Window::onResize를 호출하지 않습니다! 현재 객체, 즉 SpecialWindow의 동작을 수행하기도 전에 기본 클래스 부분의 임시 사본에 대고 Window::onResize를 호출한 것입니다. 더 큰 문제는 Window::onResize 함수가 객체를 수정하도록 만들어져 있다면, 현재 객체는 그 수정이 반영되지 않을 것입니다. 아래 코드는 위 코드를 수정한 내용입니다.
Class SpeicalWindow : public Window {
public:
virtual void onResize(){
Window::onResize(); // *this에서 Window::onResize() 직접 호출
...
}
...
};
책에서 제시하는 예제는 우리가 "캐스트 연산자가 입맛 당기는 상황이라면, 좋지 않은 징조다" 라고 설명합니다.
dynamic_cast의 단점
비용이 크고 느리다
어떤 구현환경의 경우, strcmp 연산이 dynamic_cast를 기반으로 만들어져 있습니다. 이 구현 환경 클래스 이름을 비교하기위해 strcmp가 최대 네 번까지 불릴수 있습니다. 상속 깊이가 더 깊어지거나, 혹은 다중 상속이 될 경우 비용은 더 커질 것입니다.
dynamic_cast의 첫 번째 대안
// dynamic_cast를 사용한 예제
class Window {...};
class SpecialWindow : public Window {
public:
void blink();
...
};
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> VPSW;
VPSW winPtrs;
...
for(VPSW::iterator iter = winPtrs.begin();
iter != windPtrs.end();
++iter)
{
if(SpecialWindow *psw == dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink;
}
// dynamic_cast를 사용하지 않고 수정한 방법
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> VPSW;
VPSW winPtrs;
...
for(VPSW::iterator iter = winPtrs.begin();
iter != winPtrs.end();
++iter)
{
(*iter) ->blink ();
}
첫 번째 방법은 파생 클래스 객체에 대한 포인터를 컨테이너에 담아둡니다. 각 객체를 기본 클래스 인터페이스르 통해 조작할 필요가 없어지죠.
dynamic_cast의 두 번째 대안
class Window {
public:
virtual void blink(); // 가상 함수로 blink 정의
...
};
class SpecialWindow {
public:
virtual void blink(){...}; // blink() 함수 오버라이딩
...
};
typedef std::vector<std::tr1::shared_ptr<Window> VPW;
VPW winPtrs;
...
for (VPW::interator iter = winPtrs.begin();
iter != winPtrs.end();
++iter)
{
(*iter)->blink();
}
두 번재 방법은 Winow로 부터 파생된 클래스들이 기본 클래스의 인터페이스를 통해 조작 할 수 있도록, 가상 함수 집합으로 정리해서 기본클래스에 넣어 두는 방법입니다.
다른 방법이 존재한다면, 캐스팅은 피하십시오. 피할 수 없다면 최대한 함수 안에 숨길 수 있도록 하며, C++ 스타일의 캐스트를 선호하십시오.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #30_인라인 함수 (0) | 2021.12.30 |
---|---|
[Effective C++] #29_예외 안전성 확보 (0) | 2021.12.28 |
[Effective C++] #28_내부에서 사용하는 객체에 대한 핸들 반환 (0) | 2021.12.28 |
[Effective C++] #26_변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2021.12.27 |
[Effective C++] #25_예외를 던지지 않는 swap에대한 지원 (0) | 2021.12.25 |