일반 상속과 가상 상속 오브젝트의 구조
다이아몬스 상속의 해결방법인 Virtual Inheritance 가상 상속의 구조에 대해 알아보자.
이전 게시글에서 언급했던 다이아몬드 상속의 구조이다.
간단하게 이전 게시글에서 마지막에 언급했던 다이아몬드 상속의 내용을 다시 정리해 보면,
위 (그림1)과 같은 구조를 갖는 Liger 오브젝트를 생성할 때 virtual 키워드 없이 오브젝트를 생성하면
Animal Constructor가 2번 중복되어 호출된다는 것이다.
virtual 키워드를 사용해서 Liger 오브젝트를 호출하면 Animal Contructor가 1번만 호출된다.
그렇다면 virtual 키워드를 붙여서 inheritance를 만들 때 일반적인 상속과 어떤 차이점이 있을까?
앞서 virtual function 챕터에서 다뤘으니,
Animal 클래스를 만들고 이를 상속받는 Lion class를 만들었을 때 Lion 클래스의 size는 24byte 임을 우리는 알고 있다.
그렇다면 이 때 Lion이 일반 상속이 아니라 virtual 상속을 받게 되면?
32 bytes가 나온다!
이 이유는, 오브젝트의 구성이 조금 달라지기 때문이다.
우선 Animal object의 모양이 (그림3)의 초록색 파트처럼 들어오고, -여기까진 똑같다-
이번에는 virtual 상속을 받았기 때문에 Lion파트가 조금 달라지는데,
(그림3)의 파란색 부분의 모양으로 사자 오브젝트가 만들어지면서 8bytes가 4개, 총 32 bytes의 크기를 갖게 되는 것이다.
오브젝트 안에 *VT 가 있기 때문에 각각의 클래스에 맞는 virtual table이 만들어지는데,
일반 상속의 경우 오브젝트가 어떤 방식으로 만들어지는지에 따라서
*VT 가 Lion VT 를 가리킬 수도, Animal VT를 가리킬 수도 있다고 설명했었다.
하지만 virtual Inheritance의 경우,
상속을 받은 Lion의 Virtual Table의 구조가 일반 상속과는 달라지고,
또한 object에서도 2개의 *VT를 가지고 있기 때문에 이 포인터들이 가리키는 곳도 달라지게 된다.
일반 상속과 가상 상속의 함수 실행 비교
일반적인 상속관계에서
새로 생성된 오브젝트의 포인터는 Animal 이므로 *VT와 animalData만 볼 수 있다.
하지만 speak() 를 호출할 때 Lion의 speak()를 호출할 수 있는데, 그 이유는
이 *VT가 사자의 Virtual Table을 가리키고 그 VT 안에는 speak()를 가리키는 포인터와 ~Lion() 를 가리키는 포인터가
저장이 되어 있기 때문이다.
여기서 사자의 speak()이 lionData를 사용하더라도 lionData의 위치는 (그림5)에서 볼 수 있듯
오브젝트의 시작점에서 2칸 떨어진 곳에 저장되어있다는 것을 알기 때문에 아무런 문제 없이 speak()를 실행할 수 있다.
하지만 Virtual Inheritance의 관계에서는,
(그림6) Animal*는 초록색 박스들(Animal object)에만 스코프가 있을 것이고,
*VT는 Lion VT를 가리키게 되는데, 이 때는 조금 다르게
Lion VT를 가리키고는 있지만 조금 떨어진 곳을 가리키고 있다.
그리고 이 안에는 Lion::speak() 함수와 ~Lion() 함수로 가는 포인터들이 저장되어 있다.
그렇다면 일반적으로, 우리는 간단하게 speak()를 콜하기 위해서는 *를 받고 *VT를 통해서 Lion::speak()를 호출하면 되겠다고 생각을 할 텐데 문제가 생긴다.
Lion::speak()이 lionData를 사용하게 되면 이 함수는 lionData가 어디에 있는지 알 방법이 없다!
어려운 내용이다. 사실 나도 아직 잘 이해가 안된다.
일반적인 상속관계에서 동물이든 사자든 오브젝트의 시작점은 (그림5)의 분홍색 점 부분으로 모두 같다고 한다.
그런데 virtual 상속의 경우 Lion object의 시작점은 (그림6) (1)번인데, Animal object의 시작점은 (2)번이다.
일반적인 상속인 (그림5)에서는 시작점이 같기 때문에 animalData에 접근하려면 한 칸 아래,
lionData에 접근하려면 두 칸 아래 이런 식으로 접근하면 된다.
그런데 virtual 상속의 경우 Animal의 시작점을 가지고 함수를 실행하려는데 lionData가 어디에 있는지 알 방법이 없다!
위쪽의 데이터인 LionData의 크기는 많아질 수도 적어질 수도 있고 심지어 LionData가 아닌 TigerData가 될 수도 있기 때문이다.
즉 Base class인 animal 에서는 virtual Table에 쓰여져 있는 사자의 speak 함수를 실행하려 해도
animal object에서 보기에는 도무지 함수를 실행하기 위핸 데이터가 어디에 있는지 알 수 없다는 것이다.
(그림7) 따라서 Lion VT는 위에 offset = -16(-2) 이런 추가 정보가 들어간다.
실제 사자 오브젝트의 시작점은 16바이트 전, 또는 2칸 전 부터 시작이라는 사실을 알려주는 것이다.
물론 이런 offset 정보가 VT에 있다고 해서 Lion speak 함수가 자동으로 offset을 가져다가 사용할 수는 없다.
offset을 계산해서 speak함수를 부르라고 하는 thunk 함수가 따로 존재한다.
이 thunk 함수가 offset을 사용해서 speak 함수 destructor 함수의 위치를 계산해서 그 함수들을 불러주는 것이다.
(그림8) 다만 이 때 일반적인 오브젝트가 가리키는 Virtual Table이 thunk함수를 사용해 가리키게 되면
offset을 또 빼주는 것이기 때문에 이상한 곳을 가리키게 되니 주의해야 한다!
이해를 돕기 위해 코드로 살펴보면,
이렇게 위 코드처럼 일반적인 Lion 포인터를 사용해서 speak() 함수를 부르게 되면 (그림8)에서 (1) 포인트에서 시작하는 것이므로 오브젝트는 이에 맞는 *VT를 따라가서 Lion::speak() 함수를 불러주게 된다.
그런데 위 코드처럼 base 클래스를 통해서 Lion 오브젝트를 만들고 speak() 함수를 부르게 되면 이때 베이스포인터는
(그림8)에서 (2) 포인트를 바라보게 되는 것이므로 이 때의 *VT는 그에 맞는 분홍색 화살표대로 따라간 곳을 가리키게 되고 이 때 offset정보를 활용해서 사자의 speak()함수를 불러주는 thunk 함수를 실행시키게 되는 것이다.
thunk Lion::speak() 함수는 offset 정보를 이용해서 16 전 혹은 2칸전으로 옮긴 후에 speak() 함수를 실행시켜 준다.
왜 이렇게 복잡하게 다루게 되는지?? 이해가 어려울 수 있다.
그 이유는, 상속받은 object가 사자일수도, 호랑이일 수도 있고, 또 다른 여러가지 동물이 될 수가 있고,
그렇기 때문에 각 animal의 data는 8byte 가 될 수도, 16 byte가 될 수도, 24byte가 될 수도 있기 때문이다.
dynamic하게 조정해 주어야 하기 때문에 이런 dynamic 정보가 VT 안에 offset으로 들어가게 되는 것이다.
실제로는 VT 안에 다른 정보가 더 들어 있는데 쉬운 이해를 위해서 그림들과 같이 구조 설명을 했다.
다이아몬드 상속에서 virtual Inheritance을 사용해주어야 하는 이유.
다시 다이아몬드 상속 관게로 돌아와서
일반적인 상속 관계에서 Liger 오브젝트를 만들게 되면
아래와 같이 그려진다.
이 안에 animalData가 2개 있어서 animal의 constructor가 2번 불러지게 되는 것이다.
virtual 상속을 사용한 Liger 오브젝트를 만들 때는 아래와 같이 그려진다.
animalData가 1개만 있기 때문에 동물 컨스트럭터가 1번만 불러지게 되는 것!
virtual inheritance를 가진 다이아몬드 오브젝트와 일반 상속을 가진 오브젝트를 비교해 보면 virtual을 사용해야 하는 이유를 잘 알 수 있다.
컨스트럭터가 1번 불러지는 것 뿐만 아니라 animalData가 중복되기 때문에 어떤 animalData를 사용해야 할지 특정하는 것이 매우 어려워지기 때문에 다이아몬드 구조에서 virtual 상속을 사용하는 것이 중요한 것이다.
'모던C++ > 상속관계 Inheritance' 카테고리의 다른 글
8. Dynamic Cast _C++ (0) | 2022.09.02 |
---|---|
6. 다중 상속 multiple inheritance _ C++ (0) | 2022.09.01 |
5. Pure Virtual Function 순수 가상 함수_ C++ (0) | 2022.09.01 |
4. Virtual Table, Virtual Function _ C++ (0) | 2022.08.31 |
3. 가상 함수 Virtual Function _ C++ / Dynamic Polymorphism (0) | 2022.08.23 |
2. 접근 권한 키워드 - public, private, protected, 파생 클래스 _ C++ (0) | 2022.08.23 |
1. Inheritance 상속이란? _ C++ (0) | 2022.08.23 |