[Advanced C++] 67. dynamic_cast, 상속 관계에서 operator<<를 이용한 출력

dev.kelvin·2025년 7월 1일
1

Advanced C++

목록 보기
67/74
post-thumbnail

1. dynamic_cast

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 타입의 예외를 발생시킨다


2. operator<<를 사용하여 상속된 클래스 출력

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<<로 출력을 구현할 때 권장하는 방법이다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글