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

Jangmanbo·2024년 4월 2일
0

Effective C++

목록 보기
27/33

캐스팅 문법

구형 스타일의 캐스트

  • (T) 표현식
    C 스타일 캐스트
  • T (표현식)
    함수 방식 캐스트

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

  • const_cast<T>(표현식)

    • 객체의 상수성을 없애는 용도로 사용
  • dynamic_cast<T>(표현식)

    • 안전한 다운캐스팅 연산자
    • 주어진 객체가 어떤 클래스 상속 계통에 속한 특정 타입인지 아닌지를 결정하는 용도로 사용
    • 런타임 비용 높음
  • reinterpret_cast<T>(표현식)

    • 하부 수준 캐스팅 연산자 (ex. 포인터 -> int)
    • 이식성 X
    • 하부 수준 코드 외에는 거의 사용하지 않아야 함
  • static_cast<T>(표현식)

    • 암시적 변환 강제로 진행할 때 사용 (ex. 비상수 -> 상수, int -> double)
    • 혹은 타입 변환 거꾸로 수행 (ex. void* -> 일반 포인터, 기본 클래스 -> 파생 클래스)
    • 단 상수 -> 비상수 캐스팅은 불가 (const_cast로만 가능

되도록이면 C++ 스타일의 캐스트를 쓰자!

  • 코드를 알아보기 쉬워, 어디서 C++ 타입 시스템이 망가졌는지 찾기 편함
  • 캐스트를 사용한 목적을 더 좁혀서 지정하기 때문에, 컴파일러에서 에러 진단 가능
    • ex) 상수성을 없애려는 목적일 때 const_cast를 사용하지 않으면 컴파일되지 않음

구형 스타일의 캐스트를 사용하는 경우

class Widget {
	explicit Widget(int size);
    ...
};

void doSomeWork(const Widget& w);

doSomeWork(Widget(15));	// 함수 방식 캐스트
doSomeWork(static_cast<Widget>(15));	// C++ 방식 캐스트

이렇게 객체를 인자로 받는 함수에 객체를 넘기기 위해 명시호출 생성자를 호출할 때 뿐이다.
그러나 이런 경우에도 가능하면 신형 스타일의 캐스트를 사용하는 것이 좋다.


캐스팅으로 일어나는 일

캐스팅으로 인해 타입변환이 있으면 런타임에 코드가 실행되는 경우가 있다.

int > double

int x, y;
...
double d = static_cast<double>(x)/y;	// int -> double 명시적 변환

intdouble로 캐스팅될 때 intdouble의 표현구조가 다르므로 특정 코드가 실행된다.

파생 클래스 객체에 대한 기본 클래스 포인터 생성

class Base { ... };

class Derived: public Base { ... };

Derived d;

Base *pb = &d;	// Derived* -> Base* 암시적 변환

두 포인터의 값이 같지 않다면(Derived 객체를 가리키는 주소가 한 개가 아니라면),
포인터의 offset을 Derived* 포인터에 적용하여 실제 Base* 포인터 값을 구하는 동작이 런타임에 이루어진다.


캐스팅을 함부로 쓰지 말자

class Window
{
public:
	virtual void onResize() { ... }
    ...
};

class SpecialWindow : public Window
{
public:
	virtual void onResize()
    {
    	static_cast<Window>(*this).onResize();	// *this의 사본을 만들어 Window의 OnResize를 호출
        // Window::OnResize(); 이게 실제로 의도한 동작
        ...										// SpecialWindow에서만 필요한 작업 수행
    }
    ...
};

여기서 사용자가 실제로 의도한 동작을 하기 위해서는 static_cast<Window>(*this).onResize(); 같이 캐스팅을 하지 말고,
Window::OnResize();를 호출하기만 하면 된다.


dynamic_cast

예를 들어 깊이가 4인 단일 상속 계통에 속한 객체에 dynamic_cast를 적용하면,
클래스 이름 비교를 위해 최대 4번 strcmp가 호출된다.

그럼에도 이 연산자를 사용하고 싶을 때가 있다.
기본 클래스 포인터만 가지고 있는데, 파생 클래스의 함수를 호출하고 싶을 때이다.


이런 코드가 있다면..

class Window { ... };

class SpecialWindow : public Window
{
public:
	void blink();
    ...
};

typedef
vector<shared_ptr<Window>> VPW;

VPW winPtrs;

...
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
	if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
    {
    	psw->blink();
    }
}

1. 파생 클래스 객체에 대한 포인터를 컨테이너에 담기

class Window { ... };

class SpecialWindow : public Window
{
public:
	void blink();
    ...
};

typedef
vector<shared_ptr<SpecialWindow>> VPSW;

VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
	(*iter)->blink();
}

애초에 파생 클래스인 SpecialWindow의 벡터를 선언함으로써 기본 클래스인 Window로 조작할 필요를 아예 없애는 것이다.
그러나 이 방법은 모든 Window의 파생 클래스에 대한 포인터를 같은 컨테이너에 저장할 수 없다.

2. 기본 클래스에 가상 함수 선언, 각 클래스에서 구현 (👍)

class Window 
{
public:
	virtual void blink() {}			// 아무 동작 안함!
};

class SpecialWindow : public Window
{
public:
	virtual void blink();	// SpecialWindow의 특정 동작 수행
    ...
};

typedef
vector<shared_ptr<Window>> VPW;	// 모든 Window 파생 타입 객체를 담을 수 있음

VPW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begi(); iter != winPtrs.end(); ++iter)
{
	(*iter)->blink();	// dynamic_cast 없이도 각 파생 클래스의 blick 함수 호출
}

이렇게 두 가지 방법으로 dynamic_cast를 사용하는 것을 회피할 수 있다.

폭포식 dynamic_cast

class Window { ... };

...	// 파생 클래스 정의

typedef
vector<shared_ptr<Window>> VPW;

VPW winPtrs;

...

for (VPSW::iterator iter = winPtrs.begi(); 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()))
    {
    	...
    }
    ...
}

이런 설계는 코드가 망가지기 매우 쉽다.
클래스가 추가되거나 빠질 때마다 위의 for문을 수정해야 하기 때문이다.



정리

  • 캐스팅은 가능하면 피하자. 특히 dynamic_cast는 더욱 신중히 생각해보자
  • 캐스팅이 필요하다면, 함수 안에 숨기자
  • 구형 스타일의 캐스트보다 C++ 스타일의 캐스트를 사용하자.

0개의 댓글