스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!
💡 다른 방법이 가능하다면 캐스팅은 피하자!
특히, 수행 성능에 민감한 코드에서 dynamic_cast는 몇 번이고 다시 생각하자.
설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해보자.
💡 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 해보자.
이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출할거다.
💡 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하도록 하자.
발견하기 쉽고, 설계자의 의도가 자세히 드러난다.
(T) 표현식
T (표현식)
객체의 상수성을 없애는 용도로 사용한다.
휘발성(volatileness)을 제거하는 용도로도 사용한다.
안전한 다운캐스팅을 할 때 사용한다
어떤 객체가 어떤 클래스 상속계통에 속한 특정 타입인지 결정하는 작업에 쓰인다.
구형 스타일 캐스트로는 흉내낼 수 없다.
런타임 비용이 높은 연산자
하부 수준 캐스팅(포인터 -> int 등)을 위해 만들어짐
적용 결과는 구현환경에 의존적이다 (이식성이 없다)
하부 수준 코드 외에는 없어야 한다.
암시적 변환(비상수 객체 -> 상수 객체 / int -> double)을 강제로 진행할 때 사용
void* -> 일반 타입 포인터 / 기본 클래스의 포인터 -> 파생 클래스의 포인터
등에서 쓰인다.
코드를 읽을 때 알아보기 쉽고,
소스코드의 어느 부분에서 C++의 타입 스타일이 망가졌는지 찾기도 쉽고,
캐스트를 사용하는 목적을 좁혀서 지정하기 때문에 컴파일러에서 사용 에러를 진단할 수 있다.
class Widget {
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
doSomework(Widget(15));
doSomework(static_cast<Widget>(15));
이 상황을 보면,
객체를 인자로 받는 함수에 객체를 넘기기 위해서
명시호출 생성자를 호출해야 하는 상황이었당.
C스타일 캐스트 문법으로 Wiget(15)로 그냥 냅다 캐스팅해버렸는데,
이게 익숙할지는 몰라도
그냥 C++ 스타일로 하자.
캐스팅 : 어떤 타입을 다른 타입으로 처리하라고 컴파일러에게 알려준다?
ㄴㄴ
일단 타입 변환이 있으면 런타임에 실행되는 코드가 만들어지는 경우도 많다.
class Base { ... };
class Derived : public Base { ... };
Derived d;
Base* pb = &d;
이런 상황에서 마지막 줄에서
Derived*
에서 Base*
의 암시적 변환이 이루어진다.
파생 클래스 객체(Derived 객체) 에 대한 기본 클래스 포인터(Base*)를 만드는 코드이다.
근데,
두 포인터의 값이 같지 않은 경우도 있다.
포인터의 변위를 Derived*
에 적용해서 Base*
포인터 값을 구하는 동작이 런타임에 이루어진다.
객체 하나가 가질 수 있는 주소가 한 개 이상이 될 수 있다.
(Derived 타입의 객체가Base*
로 가리킬 때의 주소, Derived*
로 가리킬 때의 주소)
: C++의 다중상속에서 이런 일이 생길 수 있음
but 단일 상속에서도 이렇게 될 수 있다.
데이터가 어떤 방식으로 메모리에 박혀 있을지 섣부르게 가정하지 말아야 한다.
(어떤 객체 주소를 char* 포인터로 바꿔서 포인터 산술 연산을 적용하는 코드는 미정의 동작을 야기한다)
class Window {
public:
virtual void OnResize() { ... }
...
};
class SpecialWindow : public Window {
public:
virtual void OnResize() {
static_cast<Window>(*this).OnResize();
...
}
....
};
이런 상황이 있다.
언리얼처럼 가상함수는 기본 클래스 버전을 실행하고 실행해야 하는 상황이라고 치자.
캐스팅이 일어나면서 *this의 기본 클래스에 대한 사본이 만들어졌다
그래서 OnResize()는 그 사본에 대해 호출된다.
그니까
현재 객체에 대해 수행하기도 전에 기본 클래스의 사본에 대고 Window::OnResize()
를 호출한거다
만약에 Window::OnResize가 객체를 수정하는 코드였다면
현재 객체는 수정되지 않을거다
사본에 대해서 호출된거니까 걍 엉뚱한 사본만 바뀔거다.
그러면?
기본 클래스에서 한 수정은 반영이 안되고, 파생 클래스에서 한 수정만 반영되겠지
일단 캐스팅을 뺀다.
그리고
현재 객체에 대해 OnResize
의 기본 클래스 버전을 호출하도록 하면 된다.
class SpecialWindow : public Window {
public:
virtual void OnResize() {
Window::OnResize();
...
}
....
};
이렇게.
일상생활에서 캐스트 연산자에 대해 경계해야만 한다.
특히, 수행 성능이 매우 중요한 코드에서는 dynamic_cast를 매우매우 주의해야 한다.
파생 클래스 객체에 대해 파생 클래스의 함수를 호출하고 싶은데,
그 객체를 조작할 수 있는 수단으로 기본 클래스의 포인터나, 참조자밖에 없는 경우
dynamic_cast가 보고 싶을 거다.
이 상황에서 대처 방법은 크게 두 가지다.
이렇게 하면,
각 객체를 기본 클래스 인터페이스를 통해 조작할 필요가 없어진다.
class Window { ... };
class SpecialWindow : public Window {
public:
void blink();
...
};
typedef
std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for (VPW::iterator iter = WinPtrs.begin() ; iter != winPtrs.end() ; ++iter) {
if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get())) {
psw->blink();
}
}
이렇게 하지 말고
typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter)
((iter)->blink();
이렇게 해보장.
위에꺼는
Window 객체에 대한 포인터를 담아놓았기 때문에
SpecialWindow 객체에 접근하려면 dynamic_cast를 이용해서 다운캐스팅 해야 했는데,
아래꺼(추천)는
SpecialWindow
객체에 대한 포인터를 컨테이너에 저장한거다.
그래서 캐스팅할 필요는 없지만,
다른 타입의 포인터를 담으려면 타입 안정성을 갖춘 컨테이너가 여러 개 필요할 것이다.
위 상황에서
blink()
를 기본 클래스에서 구현해서 가상함수로 제공하면 된당.
class Window {
public:
virtual void blink() {}
...
};
class SpecialWindow : public Window {
public:
virtual void blink() { ... }
...
};
typedef std::vector<std::tr1::shared_ptr<Window> VPW;
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter)
(*iter)->blink();
class Window { ... };
...
typedef std::vector<std::tr1::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())) { ... }
...
}
이런식으로 해버리면,
크기만 하고 느리고 망가지기 쉬운 멍충이 코드가 되어버린다.
이런거는
가상함수 호출을 기반으로 한 코드로 바꿔줘야 한당.
웬만하면 캐스팅은 별로 안하는게 좋긴 하지만,,
int -> double로 바꾸는 등은 쓸 수도 있긴하다.
꼭 써야 한다면
캐스팅해야 하는 코드를 내부 함수 속에 몰아넣어서
함수를 호출하는 외부에서 알 수 없도록 하자.
😊
진짜,, 제일 힘들었던 부분
여러 번 읽어봐야겠다.