목록

2019년 6월 13일 목요일

[C++] 형변환(Type casting) dynamic_cast

편의상 클래스(또는 구조체)를 그냥 클래스로 표현합니다.


dynamic_cast는 보통 부모클래스와 자식클래스간 형변환에 사용되는 캐스트 연산자로
다른 캐스팅과는 다르게 실패 검증기능이 있어서,
형변환에 실패할 시 포인터로의 캐스팅은 null값을 리턴하고,
이 포스팅에선 참조로의 캐스팅 예시 소스코드를 보여드리진 않지만,
참조로의 형변환이 실패하면 std::bad_cast 예외를 냅니다.

그리고 dynamic_cast 를 사용할 때 피연산자는 다형 클래스 형식이여야 합니다.
이게 무슨 말이냐면, virtual 함수가 하나라도 있어야 된다는 뜻입니다.
그렇지 않으면 빌드시 컴파일 타임에 오류를 뱉게 됩니다.

virtual 함수가 없을 때는 자식클래스에서 부모클래스로의 변환일때만 가능합니다.



dynamic_cast의 원리는 클래스내에는 숨겨진 값이 있습니다.
가상 함수가 있는경우 vftable 을 가르키는 포인터 변수가 숨겨져 있고,
virtual 상속을 하는 경우는 vbtable 이란것도 있죠.
dynamic_cast는 vftable 포인터의 값을 통해 캐스팅의 실패 및 성공 여부를 알아낼 수 있습니다.

vftable은 가상함수 테이블로. 가상함수가 하나라도 있는 클래스에는 가상함수테이블 포인터가 반드시 존재합니다. 가상함수 테이블은 클래스마다 다르며, 상속구조에서 다른 가상함수 테이블 포인터 주소를 바꾸어 클래스마다 다른 함수가 불리도록 오버라이딩 하는것을 가능하게 만듭니다.


*RTTI(Real Time Type Information) 옵션을 끄면 dynamic_cast 를 사용할 수 없습니다.


1. 사용 예

1)  다형성을 위해 자손클래스 들을 모아 놓은 리스트에서 원래의 타입을 알아내려고 할 때

물론 원래의 타입을 알아내기 위해서라면 virtual 함수를 만들어 고유의 타입 ID를 리턴하도록 오버라이딩 하고
타입 아이디에 따라 타입을 알아내면 되긴 합니다.
하지만 상속할 클래스가 내가 만든 클래스가 아니라면? 라이브러리에 들어있는 거라 함수를 마음대로 추가할 수 없다면? 등등의 이유로 오버라이딩을 통한 해결이 어려울 때가 있지요.


  1. #include <iostream>
  2. class Animal { virtual void f() {} };
  3. class Cat : public Animal {};
  4. class Dog : public Animal {};
  5. void main()
  6. {
  7.         Animal* animals[3] = { new Cat, new Dog, new Dog };
  8.         if (dynamic_cast<Cat*>(animals[1]))
  9.         {
  10.                std::cout << "animals[1] Cat";
  11.         }
  12.         else if (dynamic_cast<Dog*>(animals[1]))
  13.         {
  14.                std::cout << "animals[1] Dog";
  15.         }
  16.         else if (dynamic_cast<Dog*>(animals[1]))
  17.         {
  18.                std::cout << "animals[1] 은 몰라";
  19.         }
  20. }


2) 죽음의 마름모 꼴에서 한 번에 다운 캐스팅 할 때
static_cast 같은 다른 캐스팅은 불가능한데 dynamic_cast는 가능해요. vftable의 값을 보면 알 수 있거든요.
심지어 C스타일 캐스트도 한 번에 다운 캐스팅 시도하면 컴파일 타임에 오류를 내뱉습니다.
reinterpret_cast는 가능하긴 하나, 어떤 자식 기준으로 변환할지 상관하지 않고 주소 그대로 강제적인 캐스팅을
해대기 때문에 문제가 발생하는 경우가 있습니다.

3) std::any 처럼 타입 상관없이 어떤 값을 저장하는 무언가를 만들 때
any가 정말 적당한 예중 하나인것 같아서 넣어보았습니다.
std::any_cast 는 사실상 dynamic_cast 사용하여 박싱 클래스로 부터
원래의 타입을 알아내고 원래의 타입에 대한 값을 돌려주는 역할을 합니다.



2. dynamic_cast 사용 시 런타임 형변환에 실패하는 경우



1)  자식클래스가 부모 클래스를 protected 또는 private 상속 받은 경우
  이 경우는 비주얼 스튜디오에서 컴파일 타임 에러를 내어주긴 하나
  피연산자가 추론형태인 경우 (템플릿 함수내에서 템플릿 인수 이거나 람다 함수에서 auto인자)는
  문제 없이 빌드가 되고 런타임에 실패값을 줍니다.
  1. class A { virtual void f() {} };
  2. class B : private A {};
  3. void main()
  4. {
  5.         B b;
  6.         auto Cast = [](auto * p) {return dynamic_cast<A*>(p); }; //캐스팅 실패. 0값 리턴 
  7.         auto result = Cast(&b);
  8. }



2) 피연산자 원래의 타입보다 자손 타입으로 캐스팅 할 때
   조상 타입으로 캐스팅 하는것은 가능합니다. (목표 조상까지 모두 public 상속 받아야 함)

  1. class A { virtual void f() {} };
  2. class B : public A {};
  3. void main()
  4. {
  5.         A a;
  6.         auto result = dynamic_cast<B*>(&a); //캐스팅 실패. 0값 리턴 
  7. }



3) 조상은 같지만 줄기가 다른 클래스로의 변환

  1. class A { virtual void f() {} };
  2. class B : public A {};
  3. class C : public A {};
  4. void main()
  5. {
  6.         B b;
  7.         auto result = dynamic_cast<C*>(&b); //캐스팅 실패. 0값 리턴 
  8. }


4) 전혀 다른 클래스로의 변환
     이건 컴파일 타임에 에러를 내어 줄 수 있을 법한데, 그냥 빌드 되네요.
  1. class A { virtual void f() {} };
  2. class B : public A {};
  3. class C {};
  4. void main()
  5. {
  6.         B b;
  7.         auto result = dynamic_cast<C*>(&b); //캐스팅 실패. 0값 리턴 
  8. }




5) 죽음의 마름모꼴 상속에서 겹치는 조상 클래스로의 변환
   이것도 그냥 쓰면 컴파일 타임에 모호하도고 에러뜨는데, 추론형태로 전달하면 빌드됩니다.
   실패 이유는 어떤조상의 클래스인지 모호해서 그렇습니다. 여기서는 B::A 인지 C::A인지 모호해서.


  1. class A { virtual void f() {} };
  2. class B : public A {};
  3. class C : public A {};
  4. class D : public B, public C {};
  5. void main()
  6. {
  7.         D d;
  8.         auto Cast = [](auto * p) {return dynamic_cast<A*>(p); }; // 모호. 캐스팅 실패. 0값 리턴 
  9.         auto result = Cast(&d);
  10.         result = Cast(static_cast<B*>(&d)); //대체 방법. 어떤 조상의 A클래스인지 알 수 있게 
  11.                                              //조상포인터로 먼저 캐스팅
  12. }

A에서 D로 조상 클래스에서 한 번에 캐스팅 하는 것은 가능합니다.
static_cast 같은 다른 캐스팅은 불가능한데 dynamic_cast는 가능해요. vftable의 값을 보면 알 수 있거든요.



댓글 없음:

댓글 쓰기

목록