자문자답 공부

Virtual 키워드

Roka_is_back 2024. 2. 21.

공부를 했던 것인데도 막상 말하려 하면 말이 턱 막히는데, 상대가 그것에 대해 말하면 기억이 나서 아 맞다 맞아 하는 슬픔을 극복하고자 복습 겸 주기적으로 다시 보며 상기하기 위해 적는다.. ㅎㅎ..

 

Virtual 이란?

virtual은 class 간의 부모 - 자식 관계에서 함수를 오버라이딩 할 수도 있게 해주는 키워드이다.

virtual func 가 1개라도 존재한다면 이 객체 1개당 1개의 Virtual Table Ptr 이 생긴다.

만약 다중 상속 시에는 추가로 Virtual Table Ptr이 잡힌다고 한다.(https://guru.tistory.com/125?category=1038889)

또, 전역에서 공유 가능해야 하므로 Data 영역에 위치한다고 한다.

Virtual Table?

가상 테이블은 함수 포인터 배열이라고 생각하면 된다.

현재 나를 표현하는 객체 클래스가 가르켜야 하는 함수의 주소들을 저장하고 있다.

그렇다면 왜 virtual Table이 필요한 것일까?

vTable은 동적 바인딩 , 런타임에 필요하기 때문에 중요하다.

가상 함수 테이블

 

RTTI 정보가 있음을 알 수 있다.

정적 바인딩? 동적 바인딩?

정적 바인딩이란?

코드상으로 명시적으로 정해진 타입으로 컴파일때 변수의 타입이 결정된다.

 

장점

컴파일시 타입이 결정되기 때문에 속도가 빠름.

타입 에러 문제를 조기 발견할 수 있음.

단점

결정된 후 변경이 불가능.

 

동적 바인딩이란?

런타임 도중에 이루어지는 바인딩을 의미한다.(ex. 업 캐스팅)

 

장점

실행도중 필요한 객체의 함수를 호출함으로써 유연성을 지님.

단점

변수의 예상치 못한 타입으로 안정성 저하.

https://rokaisback.tistory.com/24

 

[OS] Memory - 종류 / 주소 바인딩 / 용어

 

rokaisback.tistory.com

컴파일,런타임

소멸자 Virtual

만약 소멸자에서 해제해줘야 하는 자원이 있으며 자신이 부모가 될 가능성이 있다면 소멸자에 virtual을 붙여야 한다.

만약 안 붙이면 어떻게 될까 실험해보면 b 객체를 A Class 로 업캐스팅을 했더니 A 소멸자만 호출된 것을 볼 수 있다.

 

Virtual을 안 붙인 예시.

아래는 Virtual을 붙여 정상적으로 작동하는 예시 이다.

업 캐스팅

상속 관계로 묶여있는 클래스 객체들이 있을 때, 보통 자식을 부모 타입으로 바꾼뒤 vector 같은 자료구조에 한꺼번에 관리하고는 한다.

이때도 가상  테이블은 중요한 역할을 하는데, 아래 이미지를 보자.

분명 다른 class 임에도 각자 본인의 함수를 호출할 수 있는건 가상 함수 테이블의 존재 덕분이다.

 

다운 캐스팅

원래 객체로 돌아가며 데이터에 접근이 가능하다.

만약 본인 타입과 다른 타입으로 시도를 하면 nullptr을 반환하며, 성공시 주소를 반환한다.

그럼  virtual table 이 어떻게 데이터 타입을 알려주는 걸까?

앞에서 언급한 RTTI info 를 통해서 해결하는 것이다.

 

RTTI (RunTime Type Info)

dynamic_cast를 쓸때 rtti 정보를 가지고 비교 연산을 한다고 예전에 어떤 글에서 봤다.

실행 시간타입 정보를 검사해서 제대로 된 형 변환만을 허용하기 때문에,

안전성은 좋지만 성능이 좋지 않다.

다들 너무 무겁다 라고 했다.

 

RTTI와 Dynamic_cast 등의 원리

예전에 봤던 정보를 찾다가 더 좋은 블로그를 발견했다.

솔직히 인터넷으로 찾은 정보들을 내가 짜집기 하면서 공부한거라 맞는 정보인지 불안할 때가 있는데,

이 블로그 덕에 안심했다 ㅎㅎ

https://haewoneee.tistory.com/69

 

C++ 프로그래밍 : 런타임 타입 정보(Runtime Type Information)

C++ 프로그래밍 런타임 타입 정보(Runtime Type Information) 클래스 사이 타입 변환에 대해 두 번에 걸쳐 자세한 소개를 했다. 간단히 떠올려보면 강제 타입 변환을 비롯해 static_cast, reinterpret_cast에 관련

haewoneee.tistory.com

 

함수 호출 원리

함수는 Code 영역의 주소 아닌가? 최종적으로 다 같은 주소에 접근해야 하는 거 아닌가?

어떻게 vtbl에서는 각 객체마다 다른 주소를 보유하지? 라는 궁금증이 생겼다.

 

추측하기로는 멤버 함수는 객체가 필요하기 때문에 this를 필요로 하고 그때문에 뭔가 다르게 동작하지 않을까? 생각중인데 지금 어떻게 동작하는지 찾아보는 중이다.

오 찾았다!

 

 

 

일단 이 블로그를 통해 내 생각이 맞았다는 것을 알 수 있다.

그럼 왜 vtbl에 객체마다 다르게 저장되는 주소는 뭘까?

 

https://univ-developer.tistory.com/entry/c13?category=1011252

 

class에 대한 얘기(2)

안녕하세요 대학생 개발자입니다. 이번글에서는 저번글에 이어서 클래스에 대한 정리를 좀더 해보려고합니다. 저번글에서는 기본 생성자와 소멸자에 대한 얘기를 해봤습니다. 그럼 다음으로

univ-developer.tistory.com

 

일단 이 블로그를 통해 내 생각이 맞았다는 것을 알 수 있다.

그럼 왜 vtbl에 객체마다 다르게 저장되는 주소는 뭘까?

 

아래의 블로그를 보면 이런 답변이 있다.

링커는 일반적으로 함수의 상대 주소를 확인하고 이를 첫번째 단계로 vtable에 주입한다.

해당 함수에 대한 액세스는 this 를 통해 처리된다. 라고 하는 것 같은데..

두번째 링크를 보니까 이해가 될 것 같기도 하고..

x+8 상대주소로 _Dog.break()가 정의된 곳에 접근하면 이제 break(this) 호출하면서 코드 영역으로 가는건가? 

근데 _Dog.break() 정의가 x+8 주소에 있을 거라는 보장이 어딨는 거지?

offset 8도 vtbl에서 몇번째 떨어졌는지로 계산되는건데 main에서 언제 _Dog.break()를 호출할줄 알고?

얼추 이해한 것 같다.

아닐수도 있지만 대충 아래의 이미지 처럼 동작하며 주소를 찾아간다는 의미이지 않을까 싶다.
근데 지금은 이해 못해도 다음에 보면 또 더 잘 이해할 수 있지 않을까 싶다.(언젠가..!)

https://stackoverflow.com/questions/8682998/addresses-of-virtual-functions-in-vtable 

 

Addresses of virtual functions in vtable

Lets say there is a class named Person which contains a virtual function named age(). As per language semantics, vtable is per class and not per object. It is VPTR which is per object and points to

stackoverflow.com

https://medium.com/@show981111/compiler-memory-allocation-alignment-of-objects-and-virtual-functions-c219488962ce

 

[Compiler] Memory Allocation/Alignment of Objects and Virtual Functions

How variables are stored in the memory, functions are called in the low level, and the compiler deals with polymorphism.

medium.com

vtbl을 사용하지 않는 최적화?

vtbl을 사용하면 cpu 500 cycle 문제가 3번 발생하는데 vtbl을 사용하지 않고 사용자 구현으로 처리하면 2번으로 줄어든다고 하는 것 같은데 아직 잘 이해가 안돼서 그냥 링크만 남겨 놓는다.

https://aras-p.info/blog/2011/02/01/the-virtual-and-no-virtual/

 

The Virtual and No-Virtual · Aras' website

You are writing some system where different implementations have to be used for different platforms. To keep things real, let’s say it’s a rendering system which we’ll call “GfxDevice” (based on a true story!). For example, on Windows there could

aras-p.info

함수 호출 스택 메모리

함수 호출시 스택메모리에 정보가 쌓이는데,
돌아갈 곳의 정보-> 매개변수 정보 ->지역 변수 순으로 스택에 올라간다.

(자세한 내용은 예전에 다른 블로그에 정리했다.https://ramingstudy.tistory.com/62)

 

Class Memcpy

class 를 memcpy 하지 말라는 이야기를 들은적이 있다.

대충 vtbl 때문에 문제가 생길 수 있다 라고는 들었지만 무슨 문제가 어떻게 생기는 걸까?
자세한 원리가 궁금하여 실험해 봤다.

vtbl은 class 당 1개 생성

원래는 객체당 vtbl이 1개씩 할당되며, 이 객체가 사라지면 vtbl도 같이 사라진다고 생각을 했다.

하지만 그것은 틀렸고 방금 위에서 노트로 올린 이미지의 내용과 결이 같을 것 같다.

 

이유 1: 여러번 다시 실행해도 vtbl이 잡히는 위치는 정적이다. 

이유 2: myB가 소멸했음에도 불구하고 vtbl은 유지된다.
이 이유와 앞서 블로그들을 보면서 공부해본 결과 vtbl은 data 영역 또는 code 영역에 위치해 있고 class 당 1개 생긴다.

 

Memcpy 실험 1

흠 보면 A는 B의 vtbl 을 값 복사했다.

노트에서 적은 대로 계산을 한다고 치면 *(tbl 주소 + offset) 으로 함수 정의를 찾아갈 텐데..

원하는대로 동작하지 않는다.

일단 변수는 잘 바뀌는데 함수가 계속 본인 class 껄로 함수 호출을 한다.

공부한 대로라면 tbl에 rtti 정보가 있기 때문에 그걸 토대로 함수 주소를 찾아가는 걸로 아는데

분명 tbl 정보가 잘 바뀌었음에도 함수는 본인 함수를 수행한다.

왜일까..

 

 

Memcpy 실험 2

음 일단 내가 여태 일반 변수로 시험하고 있었는데 포인터로 변경했다.

잘 터진다.

실험 1이  안된 이유일반 변수로 했기 때문에 컴파일 타임에 모든 것이 정해진게 아닐까 생각한다.

그래서 vtbl을 참조 하지 않고 자신의 클래스 대로 행동한게 아닐까?

애초에 vtbl과 rtti 등의 정보들은 런타임에 업캐스팅, 다운캐스팅 등을 하면서 사용하려고 만들어 진 것이니 내가 잘못된 실험을 했다.

 

아무튼 pointer 로 바꾸고 나서는 myA->Print()를 수행하려고 하면 함수 규칙이 달라서 수행 못한다고 터진다.

이게 의도한대로 터진것인지 확신할순 없지만 error 설명 보면 원하는 대로 터진게 맞다고 생각한다.

 

만약 함수 위치를 동일하게 해주면 오류 없이 수행할 수 있지 않을까?

클래스 함수 위치를 동일하게 맞춰서 다시 수행해보면 오류 없이 B2를 출력한다.

결론

Class 는 memcpy 하지 말자!

댓글