Programming Language/C++

[C++] 상속에서의 형변환 - RTTI와 dynamic_cast

lumana 2024. 6. 28. 16:33

상속에서의 형변환 - RTTI와 dynamic_cast

  • static_cast: 정적인 시간(컴파일 타임)에 캐스팅이 일어남

  • dynamic_cast : 동적인 시간(런타임)에 캐스팅이 일어남

복습! static cast

class Base {
   public:
    int x;
};

class Derived : public Base {
   public:
    int y;
};

int main() {
    Base *b = new Derived;
    Derived *d = static_cast<Derived*>(b);
}
  • b가 가리키는 인스턴스의 타입을 알고 있다면, static_cast를 이용하여 형변환을 해줄 수 있었다

RTTI(Run Time Type Information/Identification)

  • b가 실제로 어떤 타입을 가리키는지 컴파일 타임에 알기 어려운 경우가 많다

    • 런타임에 의존하는 경우가 발생
  • 런타임에 포인터가 가리키는 객체의 타입을 알 수 있게끔 해주는 기능을 RTTI라고 한다

  • 사실 C++에서 기본적으로 포인터가 가리키는 객체의 타입을 런타임에 확인할 수 있는 방법은 없다

    • 그런데 C++ 내부에서 RTTI가 필요할 수 밖에 없는 경우가 딱 한 가지 존재한다

      • 가상함수를 사용했을 경우이다
class Base {
   public:
    void f() {}
    int x;
};

class Derived : public Base {
   public:
    void f() {}
    int y;
};

int main() {
    Base *b = new Derived;
    Derived *d = static_cast<Derived*>(b);
    b->f();
}
  • 이 경우에는 f()가 가상 함수로 선언되어있지 않기 때문에 정적 바인딩이 일어난다
class Base { // 다형 클래스
   public:
    virtual void f() {}
        int x;
};

class Derived : public Base {
   public:
    void f() {}
    int y;
};

int main() {
    Base *b = new Derived;
    Derived *d = static_cast<Derived*>(b);
    b->f();
}
  • 만약 virtual로 f()를 선언하면 컴파일러는 b->f()를 건들지 않는다

    • 이게 가능하려면 런타임에 b가 가리키는 객체의 타입이 무엇인지를 알아올 수 있는 방법이 있어야 동적 바인딩이 가능하다

    • RTTI가 필요하다.

  • 가상 함수가 하나라도 있는 클래스를 다형 클래스라고 한다

  • 일단 virtual을 빼서 다형 클래스가 아닌 클래스로 만들어보자

#include <iostream>
using namespace std;
class Base {
   public:
    void f() {}
        int x;
};

class Derived : public Base {
   public:
    void f() {}
    int y;
};

int main() {
        cout << sizeof(Base) << endl;
        cout << sizeof(Derived) << endl;
}
  • Base의 크기는 4바이트, Derived의 크기는 8바이트로 예상할 수 있다.

  • 여기서 virtual을 추가하면

#include <iostream>
using namespace std;
class Base {
   public:
    void f() {}
        int x;
};

class Derived : public Base {
   public:
    void f() {}
    int y;
};

int main() {
        cout << sizeof(Base) << endl;
        cout << sizeof(Derived) << endl;
}
  • 크기가 8, 12로 출력된다. 4바이트가 더 늘어났다.

    • 이 4바이트는 기본적으로 어딘가를 가리키는 포인터다. 이 포인터에 클래스에 대한 정보가 담겨있다.

      • Derived가 생성되면, Derived 타입의 정보를 담고 있는 메모리 공간을 4바이트 포인터가 가리키게 된다.

      • Base가 생성되면, Base 타입의 정보를 담고 있는 메모리 공간을 4바이트 포인터가 가리키게 된다.

  • Base *b = new Derived; 라면 어떻게 동작할까?

    • 포인터 b는 Base* 이므로 8바이트 공간을 가리키는 포인터이다.

    • 여기서 RTTI를 실현하기 위해, 4바이트 포인터가 가리키는 곳을 따라가면 Derived라는 타입의 정보를 담는 공간이 나온다.

    • 그래서, 실제로 가리키는 객체가 Base처럼 생겼지만 실제로는 Derived 타입이라는 것을 확인한다.

    • 이것이 기본적인 RTTI의 원리이다.

Dynamic cast

  • dynamic_cast는 런타임에 타입에 대한 정보를 가지고 있어야 하기 때문에, RTTI가 지원되는 경우에만 dynamic cast를 사용해서 다운 캐스팅을 할 수가 있는 것이다.

  • 따라서 Base로부터 Derived로 dynamic cast를 하고 싶다면, Base 클래스는 반드시 가상 함수를 포함하는 다형 클래스여야 한다

int main() {
        cout << sizeof(Base) << endl;
        cout << sizeof(Derived) << endl;
    int *a = (int*)new Base;
    cout << a[0] << " " << a[1] << endl;
    int *b = (int*)new Derived;
    cout << b[0] << " " << b[1] << " " << b[2] << endl;
    int *c = (int*)new Derived;
    cout << c[0] << " " << c[1] << " " << c[2] << endl;
        delete a;
        delete b;
        delete c;
}
  • (int*)new Base는 8바이트 메모리 공간을 가리키는 포인터가 첫 4바이트를 보라는 의미

    • a[0]는 첫 4바이트 공간, a[1]은 두 번째 4바이트 공간
  • 실행결과

8
12
2661172 10
2661184 10 20
2661184 10 20

  • 첫 4바이트 공간에는 2661172(Base 클래스 정보), 2661184(Derived 클래스 정보)가 들어가 있다.

Example) Shaple Class

#include <iostream>
#include <math.h>

using namespace std;

class Shape {
public:
    virtual double GetArea() const = 0;
    virtual void Resize(double k) = 0;
};

class Circle : public Shape {
public:
    Circle(double r) : r(r) {}
    double GetArea() const {
        return r * r * 3.14;
    }
    void Resize(double k) {
        r *= k;
    }
private:
    double r;
};

class Rectangle : public Shape {
public:
    Rectangle(double a, double b) : a(a), b(b) {}
    double GetArea() const {
        return a * b;
    }
    void Resize(double k) {
        a *= k;
        b *= k;
    }
    double GetDiag() const {
        return sqrt(a * a + b * b);
    }
private:
    double a, b;
};

int main() {
    Shape *shapes[] = { new Circle(1), new Rectangle(1, 2) }; // 포인터 배열

    for (int i = 0; i < 2; i++) {
        // 도형의 넓이
        // 만약 직사각형일 경우, 대각선 길이 출력
        cout << "도형의 넓이:" << shapes[i]->GetArea() << endl;
        Rectangle *r = dynamic_cast<Rectangle*>(shapes[i]);
    }
    for (int i = 0; i < 2; i++) {
        delete shapes[i];
    }
}
  • 만약 shapes를 Rectangle로 static_cast를 했다면 shape를 Rectangle로 가정해서 캐스팅하기 때문에 컴파일 과정에서는 오류가 발생하지 않았을 것이다.

    • 하지만 r이 실제로 가리키는 객체가 Rectangle인지 Circle인지는 런타임에만 알 수 있으므로 dynamic_cast를 사용하자

    • dynamic cast를 사용하면 가리키는 객체가 Rectangle일때만 캐스팅에 성공한다

      • 형변환에 실패하면 NULL이 r에 들어가게 된다
    • 그래서 if문에서 NULL에 대한 처리를 추가해줘야 한다.

int main() {
    Shape *shapes[] = { new Circle(1), new Rectangle(1, 2) };
    for (Shape *s : shapes) {
        // 도형의 넓이
        // 만약 직사각형일 경우, 대각선 길이 출력
        cout << "도형의 넓이:" << s->GetArea() << endl;
        Rectangle *r = dynamic_cast<Rectangle*>(s);
        if (r! = NULL) {
            cout << "대각선의 길이:" << s->GetDiag() << endl;
        }
    }
    for (Shape *s : shapes) {
        delete s;
    }
}
  • Dynamic cast는 RTTI 작업을 거쳐야 하기 때문에 성능 측면에서 좋지는 않다

Dynamic cast를 사용하지 않고 Shape Class 구현

#include <iostream>
#include <string>
#include <math.h>

using namespace std;

class Shape {
public:
    virtual ~Shape() {}
    virtual double GetArea() const = 0;
    virtual void Resize(double k) = 0;
    virtual string GetInfo() const {
        return "도형의 넓이 : " + to_string(GetArea())
    }
};

class Circle : public Shape {
public:
    Circle(double r) : r(r) {}
    double GetArea() const {
        return r * r * 3.14;
    }
    void Resize(double k) {
        r *= k;
    }
private:
    double r;
};

class Rectangle : public Shape {
public:
    Rectangle(double a, double b) : a(a), b(b) {}
    double GetArea() const {
        return a * b;
    }
    void Resize(double k) {
        a *= k;
        b *= k;
    }
    double GetDiag() const {
        return sqrt(a * a + b * b);
    }
    string GetInfo() const {
        return Shape::GetInfo() + "\n대각선의 길이: " + to_string(GetDiag());
    }
private:
    double a, b;
};

int main() {
    Shape *shapes[] = { new Circle(1), new Rectangle(1, 2) };
    for (Shape *s : shapes) {
        // 도형의 넓이
        // 만약 직사각형일 경우, 대각선 길이 출력
        cout << "도형의 넓이:" << s->GetArea() << endl;
        Rectangle *r = dynamic_cast<Rectangle*>(s);
        if (r! = NULL) {
            cout << "대각선의 길이:" << s->GetDiag() << endl;
        }
    }
    for (Shape *s : shapes) {
        delete s;
    }
}
  • GetInfo 함수를 가상 함수로 만들어서 각 인스턴스의 타입에 맞는 정보를 출력하도록 구현하였다.

참조) 두들낙서 C/C++ 강좌