언어/Effective C++

[Effective C++] #27_캐스팅

Hardii2 2021. 12. 27. 11:57

[Effective C++] #27_캐스팅은 절약하고 또 절약하자!

 

Scott Meyers의 "Effective C++" 를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 5장 '구현', 항목 27 "캐스팅은 절약, 또 절약! 잊지 말자"에 해당하는 내용입니다.

 

 


 

Casting?

우리 말로 "형변환"으로 잘알려진 Casting은, 자료형간의 형변환 혹은 포인터 간의 형변환을 위해 사용됩니다.

 

C언어의 Cast
(T) A	// A를 Typename으로 캐스트 합니다.
T (A)	// 함수 방식의 캐스트
class A {
	public:
    	explicit A (int size);
        ...
};

void doSoemthing(const A& a);
doSomething(A(15));		// 2번 방식의 캐스트로, 정수 "15"로 부터 클래스 A를 생성

 

C++의 캐스트 종류 네 가지
  • const_cast<T> (A) : 객체의 상수성 제거 용도
  • dynamic_cast<T>) (A) : 안전한 다운 캐스팅, 런타임 비용이 높다!
  • reinterpret_cast<T> (A) : 포인터를 Int로 바꾸는 등의 하부 수준의 캐스팅, 구현환경에 의존적! 
  • static_cast<T> (A) : 암시적 변환, 비상수 객체를 상수 객체로, int를 double로 변환할때 사용.

 

1. Cast 사용으로 인해 발생할 수 있는 문제 
class Base {...};

class Derived : public Base {...};

Derived d;

Base *pb = &d;		// *Derived 에서 *Base로 암시적 변환

위 코드는 파생 클래스 객체에 대한 기본 클래스 포인터를 초기화하는 코드입니다. 하지만, 두 포인터의 값이 항상 같지 않을 때도 존재합니다. 따라서, 포인터의 변위를 Derived* 포인터에 적용하여 실제 Base* 포인터 값을 구하는 동작이 런타임에 이루이집니다. 객체 하나가 가질 수 있는 주소가 오직 한개가 아니라 그 이상이 될 수 있음을 보여주는 사례입니다. C++에서는 다중 상속이 사용되면 이런 현상이 벌어지며, 단일 상속의 경우도 존재하죠. 

 

2. Cast 사용으로 인해 발생할 수 있는 문제 
class Window {
	public:
    	virtual void onResize() {...};
        ...
};

class SpecialWindow : public Window {
	public:
    	virtual void onResize() {
        	static_cast<Window>(*this).onResize();	// 파생 클래스의 onResize 결과를 Window로 캐스팅
            ...
        }
        ...
};

위 코드는 *this를 Window로 캐스팅합니다. 따라서, 호출되는 onResize 함수는 Window 클래스의 것이죠. 하지만 여기서 문제가 발생합니다. 이 코드에서, 캐스팅이 발생함과 동시에 *this의 기본클래스 부분에 대한 사본이 임시 생성되어, 지금 사용되는 onResize 함수는 이 임시 객체에서 호출된 것입니다. 따라서, 현재 객체에 대해 Window::onResize를 호출하지 않습니다! 현재 객체, 즉 SpecialWindow의 동작을 수행하기도 전에 기본 클래스 부분의 임시 사본에 대고 Window::onResize를 호출한 것입니다. 더 큰 문제는 Window::onResize 함수가 객체를 수정하도록 만들어져 있다면, 현재 객체는 그 수정이 반영되지 않을 것입니다. 아래 코드는 위 코드를 수정한 내용입니다. 

 

Class SpeicalWindow : public Window {
	public:
    	virtual void onResize(){
        	Window::onResize();		// *this에서 Window::onResize() 직접 호출
            ...
        }
        ...
};

책에서 제시하는 예제는 우리가 "캐스트 연산자가 입맛 당기는 상황이라면, 좋지 않은 징조다" 라고 설명합니다.

dynamic_cast의 단점
비용이 크고 느리다

어떤 구현환경의 경우, strcmp 연산이 dynamic_cast를 기반으로 만들어져 있습니다. 이 구현 환경 클래스 이름을 비교하기위해 strcmp가 최대 네 번까지 불릴수 있습니다. 상속 깊이가 더 깊어지거나, 혹은 다중 상속이 될 경우 비용은 더 커질 것입니다.

 

dynamic_cast의 첫 번째 대안 
// dynamic_cast를 사용한 예제

class Window {...};

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

typedef std::vector<std::tr1::shared_ptr<SpecialWindow> VPSW;

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


// dynamic_cast를 사용하지 않고 수정한 방법

typedef std::vector<std::tr1::shared_ptr<SpecialWindow> VPSW;

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

첫 번째 방법은 파생 클래스 객체에 대한 포인터를 컨테이너에 담아둡니다. 각 객체를 기본 클래스 인터페이스르 통해 조작할 필요가 없어지죠. 

 

dynamic_cast의 두 번째 대안 
class Window {
	public:
    	virtual void blink();		// 가상 함수로 blink 정의
        ...
};

class SpecialWindow {
	public:
    	virtual void blink(){...};	// blink() 함수 오버라이딩
        ...
};

typedef std::vector<std::tr1::shared_ptr<Window> VPW;
VPW winPtrs;
...
for (VPW::interator iter = winPtrs.begin();
	iter != winPtrs.end();
    ++iter)
{
	(*iter)->blink();
}

두 번재 방법은 Winow로 부터 파생된 클래스들이 기본 클래스의 인터페이스를 통해 조작 할 수 있도록, 가상 함수 집합으로 정리해서 기본클래스에 넣어 두는 방법입니다.

 

다른 방법이 존재한다면, 캐스팅은 피하십시오. 피할 수 없다면 최대한 함수 안에 숨길 수 있도록 하며, C++ 스타일의 캐스트를 선호하십시오.