
항목 27 캐스팅은 최대한 절약하자
27.1 캐스팅 문법
■ 27.1.1 캐스트 스타일
- C 캐스트(구형 스타일 캐스트)
- (T) 표현식
- T (표현식)
- C++ 캐스트(신형 스타일 캐스트)
C++ 캐스트 | 설명 | 예시 |
const_cast<T>(표현식) | 객체의 상수성(constness) 혹은 휘발성(volatileness)을 없애는 용도로 사용한다. | const → non-const |
static_cast<T>(표현식) | 암시적 변환을 진행하거나 타입을 거꾸로 변환하는 용도로 쓰인다. | int → double void* → int* Parent* → Child* |
reinterpret_cast<T>(표현식) | 하부 수준 캐스팅을 위해 만들어진 연산자다. 구현 환경에 의존적이므로 이식성이 없다. | int → char* |
dynamic_cast<T>(표현식) | 안전하게 다운 캐스팅할 때 사용하는 연산자다. | Parent* → Child* |
■ 27.1.2 C++ 캐스트의 장점
- 타입 시스템이 망가졌을 때 코드를 알아보기 쉬워 찾아보는 작업이 편리하다.
- 사용 목적이 드러나기 때문에 캐스트를 잘못 사용했을 때 컴파일러가 오류를 파악할 수 있다.
27.2 캐스팅 동작 방식
- 다른 타입으로 변환될 때 실행 시간(run time)에 코드가 만들어진다.
// int 타입의 x가 double 타입으로 명시적으로 캐스팅되는 부분에서 코드가 만들어집니다.
int x, y;
double d = static_cast<double>(x) / y;
// Child* 타입의 c가 Parent* 타입으로 암시적으로 캐스팅되는 부분에서 코드가 만들어집니다.
class Parent { ... };
class Child : public Parent { ... };
Child c;
Base* p = &c;
- C++에서는 데이터가 메모리에 있을 것이라고 가정할 수 없다.
- 어떤 객체의 주소를 다른 타입의 포인터로 바꿔서 포인터 연산을 하는 코드는 정의되지 않은 동작을 보인다.
- 컴파일러마다 객체를 메모리에 할당하는 구조와 주소를 계산하는 방법이 다르다.
27.3 잘못된 캐스팅
■ 27.3.1 static_cast 남용
- static_cast<T>(*this)는 부모 클래스에 대한 임시 객체를 만든다.
- 임시 객체에 대해 onResize 함수를 호출하고 원본 객체에 대해 onResize 함수는 호출되지 않아 문제가 발생한다.
- 문제를 해결하려면 캐스팅을 사용하지 않고 부모 클래스의 onResize 함수를 올바르게 호출한다.
// 부모 클래스입니다.
class Window {
public:
virtual void onResize() { ... }
};
// 자식 클래스입니다.
class SpecialWindow : public Window {
public:
virtual void onResize() {
// 부모 클래스의 onResize 함수를 올바르게 호출합니다.
Window::onResize();
// 부모 클래스의 사본에 onResize 함수를 잘못 호출합니다.
static_cast<Window>(*this).onResize();
...
}
};
■ 27.3.2 dynamic_cast 남용
- dynamic_cast는 대부분 구현 환경에서 매우 느리게 동작한다.
- 폭포식(cascading) dynamic_cast 구조는 매우 느리고 망가지기 쉽다는 문제가 발생한다.
class Window { ... };
class SpecialWindow1 : public Window { ... };
class SpecialWindow2 : public Window { ... };
class SpecialWindow3 : public Window { ... };
// 부모 클래스를 가리키는 포인터를 배열에 저장합니다.
vector<shared_ptr<Window>> winPtrs;
// 부모 클래스 포인터를 일일이 다운캐스트해서 자식 클래스 객체에 접근합니다.
for (auto iter = winPtrs.begin(); iter != winPtr.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())) { ... }
}
- 첫 번째 해결 방법은 자식 클래스 객체에 대한 포인터를 컨테이너에 저장한다.
- 각 객체를 자식 클래스 인터페이스를 통해서 접근한다.
class Window { ... };
class SpecialWindow : public Window {
public:
void blink();
};
// 자식 클래스를 가리키는 포인터를 배열에 저장합니다.
vector<shared_ptr<SpecialWindow>> winPtrs;
for (auto iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
// 자식 클래스 포인터로 함수를 호출합니다.
(*iter)->blink();
}
- 두 번째 해결 방법은 가상 함수를 기본 클래스에 선언하고 자식 클래스에서 재정의(override)한다.
- 각 객체에서 재정의된 함수를 부모 클래스의 포인터로 호출한다.
class Window {
public:
virtual void blink() { }
};
class SpecialWindow : public Window {
public:
virtual void blink() { ... }
};
// 부모 클래스를 가리키는 포인터를 배열에 저장합니다.
vector<shared_ptr<Window>> winPtrs;
for (auto iter = WinPtrs.begin(); iter != WinPtrs.end(); ++iter) {
// 부모 클래스 포인터로 재정의된 함수를 호출합니다.
(*iter)->blink();
}
NOTE
① 가급적이면 캐스팅을 사용하지 않는다. 특히 성능이 중요한 코드에서 dynamic_cast는 사용하지 않는다.
② 캐스팅이 꼭 필요한 경우, 함수 안에 캐스팅을 숨겨 사용자가 직접 캐스팅을 사용하지 않게 한다.
③ C 스타일 캐스팅보다 C++ 스타일 캐스팅을 선호하자. 의도가 더 분명하고 발견하기도 쉽다.
'Object Oriented Programming(C++) > Effective C++' 카테고리의 다른 글
Effective C++ | 항목 33 상속된 이름을 숨기는 일은 피하자 (0) | 2023.05.20 |
---|---|
Effective C++ | 항목 32 public 상속 모형은 반드시 is-a를 따르도록 만들자 (0) | 2023.05.18 |
Effective C++ | 항목 26 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2023.03.03 |
Effective C++ | 항목 24 타입 변환이 모든 매개 변수에 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2022.02.15 |
Effective C++ | 항목 23 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자 (0) | 2022.02.15 |
댓글