dynamic_cast가 무엇인가?
아래의 예제는 두 객체의 상속 관계를 보여주고 있습니다.
CObj 클래스가 base 클래스이고, CCar 클래스는 CObj 클래스를 상속한 클래스입니다.
enum ObjType{
BASE,
CAR
};
// 부모 클래스
class CObj{
ObjType m_type;
public:
CObj( const ObjType& type = ObjType::BASE) : m_type(type){};
virtual ~CObj() = default;
};
// 자식 클래스
class CCar : public CObj{
int m_nPrice;
public:
CCar(int nPrice) : m_nPrice(nPrice), CObj(ObjType::CAR){};
int getPrice(){
return m_nPrice;
}
};
// 객체 생성
CObj* CreateObj( ObjType type){
if ( type == ObjType::CAR){
return new CCar(1000); // upcasting
}
else{
return new CObj();
}
}
void DoSomething( CObj* pObj){
// ...
}
int main(){
CObj* pObj = CreateObj( ObjType::CAR );
DoSomething( pObj);
delete pObj;
}
그리고, 일반적으로 위의 CreateObj 함수처럼, 생성한 객체를 부모 클래스 타입의 포인터로 참조합니다.
참고로, 이렇게 참조하는 것이 가능한 이유는, 상속받은 클래스의 타입을 부모 클래스 타입으로 암시적인 변환이 가능하기 때문입니다.
이러한 타입 변환을 up-casting이라고 부릅니다.
그런데, CreateObj 함수가 반환한 CObj 객체 포인터에서 참조하는, CCar 객체의 getPrice 함수에 접근하려면 어떻게 해야 될까요?
첫 번째 떠오르는 방법은 가상 함수를 추가하는 것입니다.
그러면, CObj 타입의 포인터로도 getPrice 함수에 접근할 수 있게 됩니다.
그런데, 단지 getPrice에 접근하려는 목적을 달성하기 위해 CObj에 getPrice 가상 함수를 추가해야 하고, CObj의 모든 자식들은 getPrice 함수를 상속받아야 합니다.
게다가, 만약 CPerson이라는 자식 클래스를 만든다면 getPrice 함수의 의미가 제대로 전달되지도 않습니다.
가상 함수에 관한 내용은 여기에서 볼 수 있습니다.
두 번째 방식으로 static_cast 연산자를 사용해서 명시적인 타입 변환을 하는 방법입니다.
이 방식의 문제점은 CreateObj 함수가 실시간에 생성하는 객체가 반드시 CCar 클래스 객체가 아닐 수도 있다는 것입니다.
CObj* pObj = CreateObj( ObjType::BASE ); // CCar가 아닌 CObj 객체 생성
CCar* pCar = static_cast<CCar*>(pObj);
int nPrice = pCar->getPrice(); // 위험한 접근
만약, 위와 같이 객체를 생성한다면 getPrice 함수의 존재를 모르는 CObj 객체를 getPrice 함수로 접근하는 것이 될 것입니다.
static_cast에 관한 글은 여기에서 볼 수 있습니다.
이럴 때 사용할 수 있는 방법이 바로 dynamic_cast 연산자입니다.
이 연산자는 실시간에 부모 타입의 참조를 자식 타입의 참조로 변환하는 기능을 제공합니다.
참고로, 이와 같은 타입 변환을 down-casting이라고 부릅니다.
dynamic_cast의 선언은 다음과 같습니다.
dynamic_cast< 자식 클래스 타입의 참조 >( 부모 객체의 참조 );
이 기능을 써서 getPrice 함수에 접근하려면 다음과 같이 할 수 있습니다.
int main(){
CObj* pObj = CreateObj( ObjType::CAR );
CCar* pCar = dynamic_cast<CCar*>(pObj); // down-casting
if ( pCar ){
int nPrice = pCar->getPrice();
cout << "Car Price is " << nPrice << endl;
}
DoSomething( pObj);
delete pObj;
}
▼출력
Car Price is 1000
만약, 위의 pObj이 CCar 타입의 객체를 참조하지 않으면, dynamic_cast는 nullptr을 반환합니다.
따라서, 변환된 참조 pCar를 사용하기 전에 반드시 제대로 변환되었는지를 확인해야 합니다.
그리고, 가상 함수를 선언하지 않거나 상속받지 않은 클래스에는 dynamic_cast를 사용할 수 없습니다.
가볍게 지나쳤을지도 모르지만, 위에서 선언한 클래스는 분명히 가상 함수를 갖고 있습니다.
// 부모 클래스
class CObj{
ObjType m_type;
public:
CObj( const ObjType& type = ObjType::BASE) : m_type(type){};
virtual ~CObj() = default; // dynamic_cast를 사용하려면 가상 함수가 필요함
};
참고로, 위의 default는 컴파일러가 제공하는 소멸자( destructor )를 그대로 사용한다는 것을 명시적으로 표현하는 지시자입니다.
그러므로, CObj 클래스는 소멸자를 따로 제공할 필요가 없습니다.
또한, dynamic_cast는 실시간에 변환하고자 하는 객체의 타입을 확인하기 위해서 Run-Time Type Information( RTTI )을 사용하는데, 이 정보는 꽤 큰 비용을 요구합니다.
그렇기 때문에, 이 정보를 제거하는 옵션을 제공하는 컴파일러도 있습니다.
물론, RTTI를 제거하고 컴파일하면 dynamic_cast는 사용할 수 없습니다.
그렇다면, 어차피 사용해야 할 가상 함수를 사용해서 문제를 해결하는 것이 나은 것이 아닐까라는 생각을 할 수 있습니다.
하지만, dynamic_cast를 사용하는 것이 더 나은 경우도 있습니다.
- C++ standard library의 클래스를 상속받는 경우처럼, 부모 클래스에 가상 함수를 추가할 수 없는 경우
- 특정 클래스 만을 위한 기능에 접근하기 위해, 모든 클래스에 가상 함수를 추가해야 하는 경우
- 아무런 기능이 없는 가상 함수를 부모 클래스에 추가해야 하는 경우
참고로, 아무런 기능이 없는 가상 함수의 경우, 부모 클래스에 순수 가상 함수를 작성하는 방법도 있습니다.
이때, 부모 클래스는 추상 클래스가 되어 객체로 만들 수 없게 되는 문제를 고려해야 합니다.
추상 클래스에 대한 내용은 여기서 볼 수 있습니다.
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] C-style 문자열과 const char* (0) | 2024.08.15 |
---|---|
[C++] nullptr 리터럴( literal )과 NULL의 차이점 (0) | 2024.08.07 |
[C++] 명시적인 C-style cast와 static_cast의 차이점 (0) | 2024.08.06 |
[C++] noexcept 키워드를 사용하는 이유 (0) | 2024.08.01 |
[C++] namespace가 필요한 이유 (0) | 2024.07.30 |