KoreanFoodie's Study
Effective C++ | 항목 27 : 캐스팅은 절약, 또 절약! 잊지 말자 본문
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 27 : 캐스팅은 절약, 또 절약! 잊지 말자
핵심 :
1. 다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서 dynamic_cast 는 몇 번이고 다시 생각하자. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해 보자.
2. 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 해 보자. 이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출할 수 있게 된다.
3. 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하자. 발견하기도 쉽고, 설계자가 어떤 역할을 의도했는지가 더 자세히 드러난다.
구형 스타일의 캐스트는 "(T) expr" , "T (expr)" 처럼 괄호를 쓴다. C++ 에서는 4 가지의 캐스트 방식이 있다 :
- const_cast : 객체의 상수성(constness)를 없애는 용도로 사용됨
- dynamic_cast : '안전한 다운캐스팅(safe downcasting)' 을 할 때 사용함 런타임 비용이 높음
- reinterpret_cast : 포인터를 int 로 바꾸는 등의 하부 수준 캐스팅을 위해 만들어진 연산자로, 적용 결과가 구현환경에 의존적임. 이런 캐스트는 하부 수준 코드 외에는 거의 없어야 함
- static_cast : 암시적 변환(비상수 객체를 상수 객체로, 혹은 int를 double 로 등)을 강제로 진행할 때 사용. 타입 변환을 거꾸로 수행하는 용도로도 쓰임(void* 를 일반 타입의 포인터로).
구형 스타일의 캐스트를 쓰는 경우는 다음과 같은 경우 말고는 찾아보기 힘들다.
class Widget
{
public:
explicit Widget(int size);
};
void doSomeWork(cosnt Widget& w);
// 함수 방식 캐스트를 사용, int 로부터 Widget 생성
doSomeWork(Widget(15));
// C++ 방식 캐스트를 사용, int 로부터 Widget 생성
doSomeWork(static_cast<Widget>(15));
다음 예시를 보자.
class Base { ... };
class Derived: public Base { ... };
Derived d;
// Derived* => Base* 의 암시적 변환이 이루어진다. 런타임에!
Base *pb = &d;
위에서, 객체 d 가 가질 수 있는 주소는 2개 이상이 될 수 있다.
캐스팅과 관련된, '보기엔 맞는 것 같지만 실제로는 틀린' 코드를 보자.
class Window
{
public:
// 기본 클래스의 onResize 구현
virtual void onResize() { ... }
...
};
class SpecialWindow: public Window
{
public:
virtual void onResize()
{
// 동작이 안 됨
static_cast<Window>(*this).onResize();
// SpecialWindow 에서만 필요한 작업을 여기서 수행
...
}
...
};
SpecialWindow 클래스에서 onResize 함수 내의 캐스팅은 제대로 이루어지지 않는다. 왜냐하면 캐스팅이 일어나면서 *this 의 기본 클래스 부분에 대한 사본이 임시적으로 만들어지게 되어 있는데, 지금의 onResize 는 바로 이 임시 객체에서 호출된 것이다! 따라서 위 코드는 현재의 객체에 대해 Window::onResize 를 호출하지 않는다.
코드를 수정하려면 캐스팅을 빼고 다음과 같이 고치면 된다.
class SpecialWindow: public Window
{
public:
virtual void onResize()
{
// *this 에서 Window::onResize 를 호출
Window::onResize();
// SpecialWindow 에서만 필요한 작업을 여기서 수행
...
}
...
};
마지막으로, dynamic_cast 를 보자.
dynamic_cast 는 비용이 매우 큰 캐스팅으로, 필요한 경우가 있지만 최대한 사용을 피하는 식으로 설계 및 구현을 하는 게 좋다. 다음 예시를 보자.
class Window { ... };
class SpecialWindow: public Window
{
public:
void blink();
...
};
typedef std::vector<std::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();
}
dynamic_cast 사용을 피하기 위해, 코드를 다음과 같이 수정해볼 수 있다.
typedef std::vector<std::shared_ptr<SpecialWindow>> VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter)->blink();
}
물론 이는 SpecialWindow 말고도 Window 의 다른 파생 클래스가 있을 경우 각기 다른 파생 클래스 타입의 벡터를 만들어 주어야 한다는 단점이 존재한다. 아니면 이런 방법도 있다.
class Window
{
public:
// '아무 동작 안하는' 가상 함수
// 별로 좋은 아이디어는 아니다 (항목 34 참조)
virtual void blink() {}
...
};
class SpecialWindow: public Window
{
public:
// 실제 구현
virtual void blink() { ... }
...
};
typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter)->blink();
}
정말 피해야 하는 설계가 하나 있는데, 바로 '폭포식(cascading) dynamic_cast' 라고 불리는 구조이다. 예시를 보자.
class Window { ... };
typedef std::vector<std::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())) { ... }
...
}
위의 설계는 심지어 새로운 파생 클래스가 생길 때 마다 하나를 더 추가해야 하는, 절대 피해야 하는 설계이다!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 29 : 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! (0) | 2022.10.25 |
---|---|
Effective C++ | 항목 28 : 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자 (0) | 2022.10.25 |
Effective C++ | 항목 26 : 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2022.10.25 |
Effective C++ | 항목 25 : 예외를 던지지 않는 swap 에 대한 지원도 생각해 보자 (0) | 2022.10.25 |
Effective C++ | 항목 24 : 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2022.10.25 |