본문 바로가기
서적 정리/Effective C++

27.캐스팅은 절약, 또 절약! 잊지 말자

by 민돌이2 2021. 12. 17.

C 스타일의 캐스팅 방식은 두 가지가 있다.

(type) 표현식 //(int)a
type(표현식) //int(a)

 

C++ 스타일의 캐스팅 방식은 네 가지가 있다.

const_cast<type>(표현식) //const_cast<int>(a)
dynamic_cast<type>(표현식) //dynamic_cast<int>(a)
reinterpret_cast<type>(표현식) //reinterpret_cast<int>(a)
static_cast<type>(표현식) //static_cast<int>(a)

const_cast :

객체의 상수성(const)을 붙이거나 없애는 용도로 사용한다. 사실상 상수성을 붙이는건 선언만 하기 때문에 없애는 용도로만 사용한다.

 

dynamic_cast : 

안전한 다운캐스팅(safe downcasting)을 할 때 사용한다. 

주어진 객체가 어떤 클래스 상속 계통에 속한 특정 타입인지 아닌지를 결정하는 작업에 쓰인다. 런타임 비용이 높다.

 

reinterpret_cast : 

포인터를 int로 바꾸는 등의 하부 수준 캐스팅에 쓰인다. 적용 결과는 구현환경에 의존적이다(이식성이 없다는 뜻).

 

static_cast<type> :

암시적 변환을 강제로 진행할 때 사용한다(비상수 객체를 상수 객체로(3장 참고), int를 double로 바꾸는 등).

상수 객체를 비상수로 캐스팅은 못한다(const_cast만 가능).

 

파생 클래스에서 기본 클래스로 캐스팅하여 객체를 수정할 때 생각하지 않은 에러가 발생할 수 있다.

class Window
{
public:
	virtual void OnResize(int n)
	{
		...
	}
};

class SpecialWindow : public Window
{
public:
	virtual void OnResize(int n)
	{
		static_cast<Window>(*this).OnResize(n);

		...

	}
};

Window의 파생 클래스인 SpecialWindow는 가상함수인 OnResize 함수를 오버라이딩하고 있다.

재정의한 SpecialWindow의 OnResize 함수는 기본 클래스인 Window를 캐스팅하여 Window의 OnResize 함수를 사용한다. 이 코드의 목표는 SpecialWindow 객체의 부모 클래스의 OnResize를 쓰고자 함이다. 하지만 현실은 *this의 기본 클래스 부분에 대한 사본을 사용하고 있다. 즉, *this의 원본이 아닌 사본을 사용한다는 의미이고 임시로 생성한 기본 클래스 사본의 OnResize을 호출하는 것이다.

 

무슨 말인지 잘 이해가 안가서 직접 보기위해서 코드를 추가했다.

class Window
{
public:
	virtual void OnResize(int n)
	{
		num = n;
	}

	void Print() { cout << num << endl; }

private:
	int num = 0;
};

class SpecialWindow : public Window
{
public:
	virtual void OnResize(int n)
	{
		static_cast<Window>(*this).OnResize(n);

	}
};

int main()
{
	SpecialWindow b;
	b.OnResize(3);
	b.Print();

	system("Pause");
	return 0;
}

Print 함수를 사용하면 Window 기본 클래스의 num을 출력하는데, b.OnResize(3); 을 함으로써 num에 3을 대입하였다.

하지만 *this를 기본 클래스로 캐스팅한 결과물은 원본이 아닌 사본이기 때문에 3이 출력되지 않고 0이 출력된다.

출력 결과

 

기본 클래스로 캐스팅하여 호출하는 방식이 아닌 현재 객체에 대고 기본 클래스 버전의 OnResize 함수를 호출하게 해야한다.

class SpecialWindow : public Window
{
public:
	virtual void OnResize(int n)
	{
		Window::OnResize(n);
	}
};

int main()
{
	SpecialWindow b;
	b.OnResize(3);
	b.Print();

	system("Pause");
	return 0;
}

출력 결과

 

dynamic_cast는 굉장히 비용이 높은 캐스트다. 그럼에도 dynamic_cast를 사용하고 싶을 할 때가 있다. 파생 클래스 객체임이 분명할 때 파생 클래스의 함수를 호출하고 싶은데, 그 객체를 조작할 수단으로 기본 클래스의 포인터 혹은 참조자밖에 없는 경우다(기본 클래스에서 파생 클래스의 함수를 사용할 상황).

dynamic_cast를 피해 가는 일반적인 방법이 있다.

1.파생 클래스 객체에 대한 포인터 혹은 스마트 포인터를 컨테이너에 담는다.

객체를 기본 클래스 인터페이스에서 조작할 필요를 없애것이 목표다.

솔직히 알필요 없어보인다. 객체 자체를 기본 클래스가 아니라 함수가 있는 파생 클래스로 생성하란 소리라고 생각한다.

class SpecialWindow : public Window
{
public:
	...

	void Blink();
};

typedef vector<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();
}

기본 클래스인 Window에서 파생 클라스의 함수인 Blink를 사용하기 위해 dynamic_cast를 사용하고 있는데

typedef vector<shared_ptr<SpecialWindow>> VPSW;
VPSW sWinPtrs;

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

파생 클래스로 선언함으로써 dynamic_cast를 사용하지 않는다.

 

2.파생 클래스의 함수를 기본 클래스로 올린다.

class Window
{
public:
	...

	void Blink();
};

 

폭포식(cascading) dynamic_cast는 정말 피해야 하는 방식이다.

typedef vector<shared_ptr<Window>> VPW;
VPW winPtrs;

for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
	if (SpecialWindow1* psw = dynamic_cast<SpecialWindow1*>(iter->get())) //SpecialWindow1
		psw->Blink();
	else if (SpecialWindow2* psw = dynamic_cast<SpecialWindow2*>(iter->get())) //SpecialWindow2
		psw->Blink();
	else if (SpecialWindow3* psw = dynamic_cast<SpecialWindow3*>(iter->get())) //SpecialWindow3
		psw->Blink();
}

언리얼에서 IsA 함수를 쓸 때가 있었다. 충돌시 이 객체의 클래스를 판별하는데 자꾸 터져서 짜증나서 전부 지워버렸다. 

 

이것만은 잊지 말자

다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서 dynamic_cast는 고민하자.

설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도하자.

캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 하자. 최소한 사용자는 자신의 코드에서 캐스팅을 넣지 않고 이 함수를 호출하게 할 수 있다.

C 스타일의 캐스팅보다 C++ 스타일의 캐스팅을 선호하자. 발견하기도 쉽고, 어떤 설계자가 의도인지 알기 쉽다.

728x90

댓글