[Effective C++] 항목 27 : 캐스팅은 절약, 또 절약! 잊지 말자

수민이슈·2023년 4월 3일
0

Effective C++

목록 보기
27/30
post-thumbnail

스콧 마이어스의 Effective C++을 읽고 개인 공부 목적으로 요약 작성한 글입니다!

💡 다른 방법이 가능하다면 캐스팅은 피하자!
특히, 수행 성능에 민감한 코드에서 dynamic_cast는 몇 번이고 다시 생각하자.
설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해보자.
💡 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 해보자.
이렇게 하면 최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고 이 함수를 호출할거다.
💡 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하도록 하자.
발견하기 쉽고, 설계자의 의도가 자세히 드러난다.


🖊️ 캐스팅 문법

C 스타일 캐스트 (구형 스타일 캐스트)

(T) 표현식	
T (표현식)

C++ 스타일 캐스트 (신형 스타일 캐스트)

const_cast

객체의 상수성을 없애는 용도로 사용한다.
휘발성(volatileness)을 제거하는 용도로도 사용한다.

dynamic_cast

안전한 다운캐스팅을 할 때 사용한다
어떤 객체가 어떤 클래스 상속계통에 속한 특정 타입인지 결정하는 작업에 쓰인다.

구형 스타일 캐스트로는 흉내낼 수 없다.

런타임 비용이 높은 연산자

reinterpret_cast

하부 수준 캐스팅(포인터 -> int 등)을 위해 만들어짐
적용 결과는 구현환경에 의존적이다 (이식성이 없다)

하부 수준 코드 외에는 없어야 한다.

static_cast

암시적 변환(비상수 객체 -> 상수 객체 / int -> double)을 강제로 진행할 때 사용
void* -> 일반 타입 포인터 / 기본 클래스의 포인터 -> 파생 클래스의 포인터
등에서 쓰인다.

웬만하면 C++ 스타일을 쓰도록 하자

코드를 읽을 때 알아보기 쉽고,
소스코드의 어느 부분에서 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를 매우매우 주의해야 한다.

파생 클래스 객체에 대해 파생 클래스의 함수를 호출하고 싶은데,
그 객체를 조작할 수 있는 수단으로 기본 클래스의 포인터나, 참조자밖에 없는 경우
dynamic_cast가 보고 싶을 거다.

이 상황에서 대처 방법은 크게 두 가지다.

방법 1 : 파생 클래스 객체에 대한 포인터를 컨테이너에 담아둔다.

이렇게 하면,
각 객체를 기본 클래스 인터페이스를 통해 조작할 필요가 없어진다.

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객체에 대한 포인터를 컨테이너에 저장한거다.
그래서 캐스팅할 필요는 없지만,
다른 타입의 포인터를 담으려면 타입 안정성을 갖춘 컨테이너가 여러 개 필요할 것이다.

방법 3 : 원하는 조작을 가상함수 집합으로 기본 클래스에 넣어둔다.

위 상황에서
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();

폭포식 dynamic_cast는 피하자!

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로 바꾸는 등은 쓸 수도 있긴하다.
꼭 써야 한다면
캐스팅해야 하는 코드를 내부 함수 속에 몰아넣어서
함수를 호출하는 외부에서 알 수 없도록 하자.


😊

진짜,, 제일 힘들었던 부분
여러 번 읽어봐야겠다.

0개의 댓글