[C++] 항목 27: 캐스팅은 절약, 또 절약! 잊지 말자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 5. 구현
👉🏻 항목 27: 캐스팅은 절약, 또 절약! 잊지 말자
✅ 캐스팅이란?
C++에서 캐스팅(casting)이란 타입 시스템을 바꾸는 기능을 말한다.
이는 매우 조심히 다뤄야 하는 강력한 기능이다.
캐스팅은 두 가지 스타일로 나뉜다:
-
구형 스타일의 캐스트 (C 스타일)
(T) 표현식 T(표현식)
-
신형 스타일의 캐스트 (C++ 스타일)
const_cast<T>(표현식) dynamic_cast<T>(표현식) reinterpret_cast<T>(표현식) static_cast<T>(표현식)
✅ 신형 스타일 캐스트의 종류
각 캐스트는 고유한 목적과 특징을 가진다:
1. const_cast
- 객체의 상수성을 없애기 위해 사용
- 상수 객체를 비상수 객체로 캐스팅하는 유일한 방법
2. dynamic_cast
- 안전한 다운캐스팅(safe downcasting)을 위해 사용
- 주어진 객체가 어떤 클래스 상속 계통에 속한 특정 타입인지 결정할 때 사용
- 런타임 비용이 높다 (매우 느림)
3. reinterpret_cast
- 포인터를 int로 바꾸는 등의 하부 수준 캐스팅을 위해 사용
- 구현 환경에 의존적이므로 이식성이 없다
4. static_cast
- 암시적 변환을 강제 수행할 때 사용
- int → double, 비상수 객체 → 상수 객체
- 기본 클래스의 포인터 → 파생 클래스의 포인터
- void* → T*
✅ 신형 스타일을 사용해야 하는 이유
class Widget {
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
int main() {
doSomeWork(Widget(15)); // 구형 방식
doSomeWork(static_cast<Widget>(15)); // 신형 방식
int x, y;
double d = static_cast<double>(x)/y; // 신형 방식
}
신형 스타일의 장점:
- 코드를 읽을 때 알아보기 쉽다
- 캐스트 사용 목적을 더 좁혀 지정하기에, 사용 에러를 진단하기 쉽다
✅ 캐스팅 사용 시 함정들
1. 포인터 주소 문제
class Base { ... };
class Derived : public Base { ... };
int main() {
Derived d;
Base* pb = &d; // Derived* -> Base* 암시적 변환
}
문제: Derived와 Base의 포인터 값이 같지 않을 때도 있다.
즉, 객체 하나가 가질 수 있는 주소가 여러 개일 수 있다.
2. 기본 클래스 함수 호출 시 실수
class Window {
public:
virtual void onResize() { ... }
...
};
class SpecialWindow : public Window {
public:
virtual void onResize() {
// ❌ 잘못된 방법: 임시 객체가 생성됨
static_cast<Window>(*this).onResize();
...
}
...
};
문제: 캐스팅이 일어나며 *this의 기본 클래스 부분에 대한 사본이 임시 생성된다.
함수 호출이 이루어지는 객체가 현재의 객체가 아니다!
해결:
class SpecialWindow : public Window {
public:
virtual void onResize() {
Window::onResize(); // ✅ 올바른 방법
...
}
...
};
✅ dynamic_cast의 문제점과 해결책
문제 상황
class Window { ... };
class SpecialWindow : public Window {
public:
void blink() { ... };
...
};
typedef std::vector<shared_ptr<Window>> VPW;
int main() {
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
iter != winPtrs.end();
++iter) {
if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink();
}
}
문제점:
- 클래스 이름에 대한 문자열 비교 연산 기반으로 구현되어 있어 정말 느리다
- 상속 깊이가 깊거나, 다중 상속의 경우 비용이 매우 크다
해결 방법 1: 타입별 컨테이너 사용
typedef std::vector<shared_ptr<SpecialWindow>> VPSW;
int main() {
VPSW winPtrs;
...
for(VPSW::iterator iter = winPtrs.begin();
iter != winPtrs.end();
++iter) {
(*iter)->blink();
}
}
한계: Window에서 파생되는 모든 객체를 담을 수 없다.
해결 방법 2: 가상 함수 활용
class Window {
public:
virtual void blink() {} // 아무 동작도 하지 않는다
...
};
class SpecialWindow : public Window {
public:
virtual void blink() { ... }
...
};
typedef vector<shared_ptr<Window>> VPW;
int main() {
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
iter != winPtrs.end();
++iter)
(*iter)->blink();
}
핵심: 원하는 조작을 가상 함수 집합으로 정리하여, 기본 클래스에 넣어두면 된다.
✅ 피해야 할 위험한 설계
폭포식(cascading) dynamic_cast
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())
) { ... }
...
}
문제점:
- 속도가 매우 느리다
- Window 클래스 계통이 바뀌면, 해당 코드도 함께 수정되어야 한다 → 망가지기 쉽다
🧐 정리
- 다른 방법이 가능하다면 캐스팅은 피하자.
특히 속도가 중요한 코드에서 dynamic_cast는 깊게 고민하자. - 캐스팅이 꼭 필요하다면, 함수 안에 숨길 수 있도록 하자.
최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고, 해당 함수를 호출할 수 있게 된다. - 구형 스타일의 캐스트보단 C++ 스타일의 캐스트를 선호하자.
발견하기 쉽고, 어떤 역할을 의도했는지 파악하기 쉽다.
댓글남기기