
dynamic_cast
부모 클래스 포인터만 가지고 있는 상태에서 자식 클래스에만 존재한 데이터에 접근하고 싶을 때 어떻게 해야할까?
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
virtual ~Base() = default;
};
class Derived : public Base
{
protected:
std::string m_name{};
public:
Derived(int value, std::string_view name)
: Base{ value }, m_name{ name }
{
}
const std::string& getName() const { return m_name; }
};
Base* getObject(bool returnDerived)
{
if (returnDerived)
{
return new Derived{ 1, "Apple" };
}
else
{
return new Base{ 2 };
}
}
int main()
{
Base* b{ getObject(true) };
delete b;
return 0;
}
이때 여기서 Base*타입인 b를 이용하여 Derived::getName()을 어떻게 호출할 수 있을까?
우선 첫번째 방법은 이전에 정리했던 virtual function을 사용하는 방법이다, 이는 Base*타입의 b가 실제로는 Derived클래스 타입 객체를 가리키고 있따면 Derived::getName()을 호출시킬 수 있는 방법이다
그러나 Derived에만 존재해야 하는 데이터라면 위 방식은 사용할 수 없다 (Base에서 getName()이 virtual로 선언되어 있어야 하기 때문)
이럴때 바로 dynamic_cast<>를 사용한다, dynamic_cast<>는 다운 캐스팅에 주로 사용한다 (부모 클래스 포인터를 자식 클래스 포인터로 변환)
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) };
d->getName(); //가능
delete b;
b는 실질적으로 getObject(true) 함수를 통해 동적 할당된 Derived 클래스 타입 객체를 가리키고 있기 때문에 dynamic_cast에 성공한 것이다, 만약 getObject(false)로 b가 Base 클래스 타입 객체를 가리키고 있었다면 dynamic_cast는 실패한다
Base* b{ getObject(false) };
Derived* d{ dynamic_cast<Derived*>(b) }; //실패
d->getName(); //nullptr참조로 크래시 발생 가능
따라서 dynamic_cast<>가 성공했는지 nullptr 체크 후 해당 포인터를 사용하는걸 권장한다
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) };
if (d)
{
d->getName();
}
dynamic_cast는 해당 타입으로 변환이 가능한지 확인하기 위해 런타임에 검사를 하기 때문에 약간의 오버헤드가 발생한다
만약 protected나 private 상속을 사용하면 dynamic_cast가 불가능하다 (외부에서 업 캐스팅이 불가능하기 때문에 다운 캐스팅도 불가능하다)
클래스에 가상 함수가 하나도 없다면 dynamic_cast가 불가능하다, dynamic_cast는 런타임에 객체의 실제 타입을 확인하기 위해 RTTI 매커니즘을 사용하는데 이 정보가 virtual table에 존재하기 때문이다
따라서 가상함수가 하나도 없는 클래스는 virtual table이 없고 RTTI정보도 없기 때문에 런타임에 객체의 실제 타입을 확인할 수 없고 dynamic_cast가 불가능하다 (다형성 컴파일 에러 발생)
마지막으로 virtual 기반 클래스 상속을 사용하지 않는 다중 상속 (다이아몬드 구조)일때 dynamic_cast가 실패한다
class Base
{
public:
virtual ~Base() {}
};
class Left : public Base {};
class Right : public Base {};
class Final : public Left, public Right {};
이렇게 virtual 기반 클래스 상속을 사용하지 않은 다중 상속일 경우 하나의 객체안에 같은 타입의 기반 클래스객체가 2개가 있기 때문에 상속 경로가 모호해져 dynamic_cast가 불가능 한 것이다
static_cast를 사용한 다운 캐스팅
static_cast를 통해서도 다운 캐스팅이 가능하다, 하지만 dynmaic_cast와 다르게 런타임에 해당 캐스팅이 유효한지 타입 검사를 진행하지 않기 때문에 위험하다 (속도는 더 빠름)
이는 Base를 Derived로 static_cast 하게 되면 Base*가 실제로 Derived클래스 객체를 가리키지 않아도 캐스팅이 성공한다는 의미이다 (위험)
다운 캐스팅이 확실하게 성공한다는 보장이 있다면 static_cast를 사용하는것도 괜찮은 방법이다
Base* b{ getObject(true) }; //getObject(true)로 Derived클래스 타입 객체를 동적할당하고 Base*타입의 b가 가리키게 함
Derived* d{ static_cast<Derived*>(b) }; //캐스팅 성공
std::cout << d->getName();
일반적으로 이러한 다운 캐스팅을 할 때는 dynamic_cast를 사용하는걸 권장한다
하지만 가상 함수를 사용하여 dynamic_cast를 굳이 하지 않아도 되게 클래스를 설계하는것이 더 좋다고 생각한다, 단 기반 클래스를 수정하여 가상함수를 추가할 수 없거나 혹은 파생 클래스에만 존재하는 무언가에 접근할때는 dynamic_cast를 사용할 수 밖에 없다
dynamic_cast 참조
위에서 dynamic_cast를 전부 포인터로 캐스팅 했지만 참조와 함께 사용도 가능하다
Derived apple{ 1, "Apple" };
Base& b{ apple };
Derived& d{ dynamic_cast<Derived&>(b) };
d.getName() //ok
C++에는 null ref 개념은 없기 때문에 참조를 이용한 dynamic_cast는 실패 시 null ref를 반환할 수 없다, 따라서 std::bad_cast 타입의 예외를 발생시킨다
operator<<를 사용하여 상속된 클래스 출력
class Base
{
public:
virtual void print() const { std::cout << "Base"; }
};
class Derived : public Base
{
public:
virtual void print() const override { std::cout << "Derived"; }
};
int main()
{
Derived d{};
Base& b{ d };
b.print();
return 0;
}
위 코드는 Derived가 출력되게 된다
그러나 이러한 상황에서 b is a Derived라고 출력하고 싶으면 어떻게 해야할까?
int main()
{
Derived d{};
Base& b{ d };
std::cout << "b is a";
b.print();
std::cout << '\n';
return 0;
}
이렇게 번거롭게 작성해야 한다, 이를 operator<<를 이용하여 한줄로 깔끔하게 작성해보자
우선 operator<<를 오버로딩 해보자
class Base
{
public:
virtual void print() const { std::cout << "Base"; }
friend std::ostream& operator<<(std::ostream& os, const Base& b)
{
os << "Base";
return os;
}
};
class Derived : public Base
{
public:
void print() const override { std::cout << "Derived"; }
friend std::ostream& operator<<(std::ostream& os, const Derived& d)
{
os << "Derived";
return os;
}
};
int main()
{
Base b{};
std::cout << b << std::endl;
Derived d{};
std::cout << d << std::endl;
return 0;
}
이렇게 잘 overloading했다면 Base, Derived가 출력되게 된다
그렇다면 다음 코드는 어떤 결과를 출력할까?
Derived d{};
Base& refb{ d };
std::cout << refb;
결과는 Base가 나온다, 분명 refb가 실질적으로 참조하는 타입은 Derived클래스 타입이지만 operator <<가 가상함수가 아니기 때문에 Base의 operator<<가 호출되기 때문이다
그럼 operator<< 오버로딩 함수를 virtual function으로 만들면 끝?? -> 그렇지 않다
우선 멤버 함수만 가상함수가 될 수 있다 (상속 관계에서 필요하기 때문에 클래스 내부 멤버 함수만 가상 함수가 가능해야 함)
따라서 friend로 선언한 비멤버 operator<< 오버로딩 함수는 가상 함수가 될 수 없다
또한 어떻게 가상함수로 만든다고 쳐도 Base와 Derived버전의 operator<<는 함수 시그니처가 달라서 override해서 사용이 불가능하다
이를 해결하기 위해서 operator<< 오버로딩 함수 내부에 가상 함수를 호출 하는 방법을 사용하면 된다
class Base
{
public:
friend std::ostream& operator<<(std::ostream& os, const Base& b)
{
os << b.print(); //Base의 가상함수 print()를 출력
return os;
}
//가상함수 선언
virtual std::string print() const { return "Base"; }
};
class Derived : public Base
{
public:
//기반 클래스 가상함수 override
virtual std::string print() const override { return "Derived"; }
};
int main()
{
Derived d{};
Base& refb{ d };
std::cout << refb;
return 0;
}
refb는 실질적으로 Derived클래스 타입 객체를 참조하고 operator<<의 인자는 const Base& 타입이기 때문에 매개변수도 실질적으로 Derived클래스 타입 객체를 참조하게 되어 Derived::print()가 호출되고 Derived가 출력되게 되는것이다
위 코드는 다음과 같이 변경하여 조금 더 유연하게 사용이 가능해진다
class Base
{
public:
friend std::ostream& operator<<(std::ostream& os, const Base& b)
{
return b.print(os);
}
virtual std::ostream& print(std::ostream& os) const
{
os << "Base";
return os;
}
};
class Derived : public Base
{
public:
virtual std::ostream& print(std::ostream& os) const override
{
os << "Derived";
return os;
}
};
int main()
{
Derived d{};
Base& refb{ d };
std::cout << refb << std::endl;
return 0;
}
이렇게 사용시 기존의 print()는 std::string으로 한정되어 있었는데 이제는 std::ostream&으로 처리되어 출력을 자유롭게 결정할 수 있다
또한 인자로 std::ostream& 객체를 전달받기 때문에 멤버 변수를 출력하는등 복잡하고 연쇄적인 출력도 가능하다
operator<<는 그냥 아무것도 출력하지 않고 실제로 출력에 관여하는건 print 가상함수가 되는것이다
유연하고 강력한 방법이기 때문에 상속 관계에서 operator<<로 출력을 구현할 때 권장하는 방법이다