일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 영화 리뷰
- 게임
- 상속
- c++
- 스마트 포인터
- Smart Pointer
- 영화
- 다형성
- resource management class
- exception
- 티스토리챌린지
- 루아
- Effective c++
- 참조자
- UE4
- Vector
- lua
- effective stl
- 반복자
- operator new
- more effective c++
- 암시적 변환
- 메타테이블
- 오블완
- 비교 함수 객체
- reference
- implicit conversion
- 언리얼
- 예외
- virtual function
Archives
- Today
- Total
스토리텔링 개발자
[Effective C++] 27. 캐스팅 본문
728x90
항목 27 : 캐스팅은 절약, 또 절약! 잊지 말자
C++의 동작 규칙
- 어떤 일이 있어도 타입 에러가 생기지 않도록 보장한다.
- 즉, 이론적으로는 컴파일만 깔끔하게 끝나면 그 이후엔 어떤 객체에 대해서도 불안전한 연산이나 말도 안되는 연산을 수행하려 들지 않는다.
- 허나 공교롭게도 이 타입 시스템을 우회하는 방법이 있으니, 그것이 캐스트(cast)이다.
C++에서 지원하는 캐스팅 문법
- C 스타일 캐스트 (구형 스타일 캐스트)
- (T)n
- n을 T 타입으로 캐스팅
- T(n)
- n을 T 타입으로 캐스팅
- (T)n
- C++ 스타일 캐스트 (신형 스타일 캐스트)
- const_cast(n)
- 객체의 상수성을 없애는 용도로 사용된다.
- 상수성을 없애는 기능을 가진 캐스트는 이것 뿐이다.
- dynamic_cast(n)
- 안전한 다운 캐스팅을 할 때 사용하는 연산자.
- 구형 스타일 캐스트 문법으로는 절대 불가능하다.
- 허나, 신경쓰일 정도로 런타임 비용이 높다는 단점이 있다.
- reinterpret_cast(n)
- 포인터를 int로 바꾸는 등 하부 수준 캐스팅을 위한 연산자.
- 결과가 구현 환경에 의존적이다.(즉, 이식성이 없다.)
- 그러므로 하부 수준 코드 이외엔 거의 없어야 한다.(항목 50 참조)
- static_cast(n)
- 암시적 변환을 강제로 진행할 때 사용한다.
- 비상수 객체를 상수 객체로 변환
- int를 double로 변환
- 타입 변환을 거꾸로 수행할 때 사용한다.
- void*를 일반 타입 포인터로 변환
- 기본 클래스의 포인터를 파생 클래스의 포인터로 변환
- 암시적 변환을 강제로 진행할 때 사용한다.
- const_cast(n)
c++ 스타일 캐스트를 쓰는 게 바람직한 이유
- 코드를 읽을 때 알아보기 쉽다.
- 각 캐스트의 사용 목적 범위가 더 좁기 때문에 컴파일러 쪽에서 사용 에러를 진단할 수 있다.
- 상수성을 없애려고 캐스팅 했는데 const_cast를 사용하지 않았으면 컴파일 에러가 발생한다.
타입 변환으로 인해 생성되는 런타임 실행 코드
int x, y;
...
double d = static_cast<double>(x) / y; // int -> double 캐스팅에서 코드가 생성된다.
- 캐스팅 부분에서 코드가 생성된다.
- 대부분의 컴퓨터 아키텍처에서 int의 표현구조와 double의 표현구조가 아예 다르기 때문이다.
캐스팅 이슈
- 객체 하나가 가질 수 있는 주소가 오직 한 개가 아니라 그 이상이 될 수 있다.
-
class Base { ... }; class Derived : public Base { ... }; Derived d; Base *pb = &d; // Derived* -> Base*의 암시적 변환이 발생.
- *pb와 &d의 값이 같지 않을 때가 가끔 발생한다!
- 이는 C++에서만 가능한 상황이다.
- 다중 상속이 사용되면 항상 이런 현상이 발생한다.
- 즉, 데이터가 어떤 식으로 메모리에 박혀 있을 거라는 섣부른 가정을 피해야 한다.
- 포인터 변위를 가정한 트릭은 위험하다.
- 어떤 객체의 주소를 char* 포인터로 바꿔서 포인터 산술 연산을 적용한다는 식의 사용법은 미정의 동작.
-
- 보기엔 맞는 것 같지만 실제론 틀린 코드
-
class Window { public: virtual void onReisze() { ... } ... }; class SpecialWindow : public Window { public: virtual void onResize() { static_cast<Window>(*this).onResize(); // 동작이 안 된다. .... } };
- 이 경우 예상처럼 동작하지 않는다.
- 캐스팅 시 *this의 기본 클래스 부분에 대한 사본이 생기기 때문이다.
- 그 사본의 onResize를 호출하므로 미정의 동작을 발생시킨다.
- 결국 현재의 객체에 대해 Window::onResize()를 호출하지 않고 지나간다.
- 반드시 아래와 같이 사용해야 한다.
class SpecialWindow : public Window { public: virtual void onResize() { Window::onResize(); .... } };
-
dynamic_cast
- 상당수의 구현환경에서 정말 느리게 구현되어 있다.
- 허나 dynamic_cast를 쓰고 싶어지는 경우가 분명 생긴다.
- 파생 클래스 객체임이 분명한 객체에게서 파생 클래스 함수를 호출하고 싶다.
- 하지만 그 객체 조작 수단이 기본 클래스의 포인터(혹은 참조자)뿐이라면.
- dynamic_cast를 피하는 방법
- 파생 클래스 객체에 대한 포인터를 컨테이너에 담아서 각 객체를 기본 클래스 인터페이스를 통해 조작할 필요를 없앤다.
-
class Window { ... }; class SpecialWindow : public Window { public: void blink(); ... }; // 다음의 경우를 고쳐보자. typedef vector< shared_ptr<Window> > VPW; VPW winPtrs; ... for (VPW::iteractor iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter) { if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get())) psw->blink(); } // dynamic_cast를 제거한 코드 typedef vector< shared_ptr<SpecialWindow> > VPSW; VPSW winPtrs; ... for (VPW::iteractor iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter) { (*iter)->blink(); }
- 이 경우 하위 클래스 별로 타입 안전성을 갖춘 컨테이너가 여러 개 필요하다는 문제점이 있다.
-
- 원하는 조작을 가상 함수 집합으로 정리해서 기본 클래스에 넣어둔다.
- 즉, 아무것도 안하는 기본 가상 함수를 기본 클래스에서 제공한다.
- 가상 함수 기본 구현시 주의 (항목 34 참조)
-
class Window { virtual void blink() {} // 아무 동작 안하기. // 허나 가상 함수 기본 구현은 조심해야 한다.(항목 34 참조) }; class SpecialWindow : public Window { public: void blink() { ... } // 구현 }; // 아래는 이제 모든 하위 클래스에 대응하여 동작한다. typedef vector< shared_ptr<Window> > VPW; VPW winPtrs; ... for (VPW::iteractor iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter) { (*iter)->blink(); }
폭포식 dynamic_cast는 반드시 피할 것!
typedef vector<shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter)
{
// 폭포식 dynamic_cast
if (SpecialWindow1 *psw1 = dynamic_cast<SpecialWindow1*>(iter->get()))
{
...
}
else if (SpecialWindow2 *psw2 = dynamic_cast<SpecialWindow2*>(iter->get()))
{
...
}
...
}
- 크기만 하고 아름답지 않으며 속도도 둔하고 망가지기 쉽다.
- 파생 클래스가 추가될 때마다 조건 분기문을 추가해야 한다.
결론
- 잘 작성된 C++ 코드는 캐스팅을 거의 쓰지 않는다.
- 써야 할 때는 최대한 격리시키자.
- 캐스팅을 해야 하는 코드를 내부 함수 속에 몰아 놓고, 그 안의 일은 공개하지 않는다.
참조
728x90
'개발 > Effective C++' 카테고리의 다른 글
[Effective C++] 29. 예외 안전성 (0) | 2024.06.25 |
---|---|
[Effective C++] 28. 클래스 내부 private 객체에 대한 핸들 리턴 문제 (0) | 2024.06.24 |
[Effective C++] 26. 필요한 시점 직전에 변수를 정의할 것 (0) | 2024.06.20 |
[Effective C++] 25. 예외를 던지지 않는 swap 함수 (0) | 2024.06.19 |
[Effective C++] 24. 비멤버 함수 구현으로 암시적 변환 지원 (0) | 2024.06.18 |
Comments