C++ vtable

mohadang·2022년 9월 16일
0
post-thumbnail

C++ 다형성 예제

#include <iostream>

class Base {
public:
  virtual void function1() = 0;
  virtual void function2() = 0;

private:
  int member1;
  int member2;
};

class Cat : public Base {
public:
  void function1() {
    std::cout << "Cat function1" << std::endl;
  }
  void function2() {
    std::cout << "Cat function2" << std::endl;
  }
};

class Dog : public Base {
public:
  void function1() {
    std::cout << "Dog function1" << std::endl;
  }
  void function2() {
    std::cout << "Dog function2" << std::endl;
  }
};

void call_function1(Base* impl) {
  impl->function1();
}

int main() {
  Cat cat;
  call_function1(&cat);//Cat function1
  Dog dog;
  call_function1(&dog);//Dog function1

  return 0;
}
  • call_function1에서 다형성의 호출 예를 볼 수 있다. 이런 다형성 코드를 C++에서는 어떻게 구현 하는 것일까? 그것은 바로 vtable이다

  • C++에서 virtual 함수를 사용하면 다형성을 지원하기위해 아래와 같이 vptr과 vtable을 추가한다.
  • vptr은 vtable을 가리키는 포인터이며 virtual 함수를 가지는 객체라면 vptr을 소유한다. 객체는 vptr을 참조하여 vtable에 접근한다.
  • vtable에는 실제 구현 코드에 접근하기 위한 함수 주소들이 배열 형태로 열거되어 있다

vtable 존재 확인

  • 일반 클래스
#include <iostream>

using namespace std;

class Base {
  int baseIntMember;
  void doSomethig() {
    cout << "Base::doSomethig()" << endl;
  }
};

int main() {
  Base base;
  cout << sizeof(base) << endl; // 4
  return 0;
}

virtual 함수가 없는 객체의 크기는 멤버 변수 baseIntMember 포함하여 4이다
  • virtual 클래스
#include <iostream>

using namespace std;

class Base {
public:
  int baseIntMember;
  virtual void doSomethig() {
    cout << "Base::doSomethig()" << endl;
  }
};

int main() {
  Base b;
  cout << sizeof(b) << endl; // 16

  b.baseIntMember = 10;

  void* base_ptr = reinterpret_cast<void*>(&b);
  cout << *(long*)base_ptr << endl;//vptr
  base_ptr = base_ptr + sizeof(long*);
  cout << *(int*)base_ptr << endl;//멤버 변수 baseIntMember

  return 0;
}

Base의 크기가 16으로 커졌다 vptr이 추가 되었기 때문에 커진 것이다.
b 객체의 메모리에 접근하면 첫번째로 vptr이 존재하고 
그 다음 부터는 멤버 변수들이 있다는 것을 확인 할 수 있다.
  • 같은 객체끼리는 같은 vptr 주소를 가지고 있다 이는 vtable을 공유 한다는 뜻이다
#include <iostream>

using namespace std;

class Base {
  virtual void doSomethig() { }
};
class Derived1 : public Base {};
class Derived2 : public Base {};

void ShowVptr(Base* b) {
  long* base_ptr = reinterpret_cast<long*>(b);
  cout <<"vptr : "<< *base_ptr << endl;
}

int main() {
  Base b;
  Derived1 d1;
  Derived2 d2;

  ShowVptr(&b);//vptr : 94182084803896
  ShowVptr(&d1);//vptr : 94182084803872
  ShowVptr(&d2);//vptr : 94182084803848

  Derived1 d1_1;
  ShowVptr(&d1_1);//vptr : 94182084803872

  return 0;
}

Derived1의 객체인 d1과 d1_1의 vptr 값이 같다

vtable에 접근하여 호출

  • vtable에는 멤버 함수 주소들이 있다 이를 어떻게 호출 할까 ? 또한 멤버 함수에서 멤버 데이터는 어떤 식으로 접근 하는걸까 ?
#include <iostream>

using namespace std;

class Base {
public:
  virtual void doSomethig() 
  { std::cout << "just base" << std::endl; }
};
class Derived1 : public Base {
public:  
  int data_;
  Derived1(int data) : data_(data) {}
  virtual void doSomethig() override 
  { std::cout << "Derived1 : " << data_ << std::endl; }
};
class Derived2 : public Base {
public:  
  int data_;
  Derived2(int data) : data_(data) {}
  virtual void doSomethig() override 
  { std::cout << "Derived2 : " << data_ << std::endl; }
};

void call_doSomethig(Base* b) {
  long* base_ptr = reinterpret_cast<long*>(b);
  long* vptr = (long*)*base_ptr;//vptr 접근
  
  //doSomethig 함수를 호출하기 위한 함수 포인터
  void (*fp)(Base*);
  //vtable에서 doSomethig 함수 주소를 가져옴
  fp = (void(*)(Base*))*vptr; 
  //doSomethig 호출
  fp(b);
}

int main() {
  Base b;
  Derived1 d1(10);
  Derived2 d2(20);

  call_doSomethig(&b);//just base
  call_doSomethig(&d1);//Derived1 : 10
  call_doSomethig(&d2);//Derived2 : 20

  Derived1 d1_1(30);
  call_doSomethig(&d1_1);//Derived1 : 30

  return 0;
}

여기서 주의깊게 볼 점은 doSomethig은 인자를 받지 않는데 함수 포인터로 
호출할때 Base* 인자를 받도록 하였다.
이는 C++에서는 멤버 함수를 호출 할때 this call 방식으로 호출하기 떄문이다.
this call 방식은 멤버 함수를 호출 할때 객체 자식(this)를 넘긴다.
this를 넘겨받은 함수에서는 this를 통해서 멤버 변수에 접근이 가능하다.
따라서 doSomethig 함수 내부에서는 this->data_ 멤버에 접근하여 값 출력이 
가능해진다.
profile
mohadang

0개의 댓글