가상 메소드 테이블가상 메소드 테이블(영어: virtual method table, virtual function table, virtual call table, 디스패치 테이블, vtable, 또는 vftable)은 동적 디스패치(또는 런타임 메소드 바인딩)를 지원하기 위해 프로그래밍 언어에서 사용되는 메커니즘이다. 클래스가 가상 함수(또는 가상 메소드)을 정의할 때마다, 대부분의 컴파일러들은 클래스에 숨겨진 멤버 변수를 추가하는데, 이것은 (가상) 함수들에 대한 포인터들의 배열들(가상 메소드 테이블(VMT 또는 Vtable)라고 불리는)을 가리킨다. 이 포인터들은 실행 기간 도중에 정확한 함수를 가리키게 되는데, 왜냐하면 컴파일 타임에는 베이스 함수가 호출될 것인지 또는 베이스 클래스를 상속한 클래스에 의해서 구현될 지 알려져 있지 않기 때문이다. 프로그램이 상속 계층 구조의 여러 클래스들을 포함한다고 가정해보자. superclass인 프로그램이 Cat 포인터의 이러한 동적 디스패치를 구현하는 여러 다른 방법들이 있지만, vtable (virtual table) 솔루션은 특히 C++과 관련된 언어들에서 흔하다. 비주얼 베이직 또는 델파이처럼 구현 시 객체들의 프로그래매틱 인터페이스를 분리하는 언어들은 또한 vtable 접근을 사용하는 경향이 있다. 왜냐하면 이것은 단순히 다른 메소드 포인터들의 집합을 사용함으로써 객체들에게 다른 구현을 사용할 수 있게 하기 때문이다. 구현객체의 디스패치 테이블은 객체의 동적으로 바인딩된 메소드들의 주소들을 포함할 수 있다. 메소드 호출들은 객체의 디스패치 테이블에서 메소드의 주소를 꺼냄으로써 수행된다. 이 디스패치 테이블은 같은 클래스에 속한 모든 객체들에서 같으며, 그러므로 보통 그들 끼리는 공유된다. 타입 호환이 되는 클래스에 속한 객체들은 같은 레이아웃의 디스패치 테이블을 가질 것이다. 주어진 메소드의 주소는 모든 타입 호환이 되는 클래스의 같은 오프셋에 나타난다. 그래서 주어진 디스패치 테이블 오프셋에서 메소드의 주소를 꺼내오는 것은 객체의 실제 클래스와 상응하는 메소드를 갖게 되는 것이다.[1] C++ 표준은 꼭 구현되어야 할 동적 디스패치를 어떻게 할 것인지를 정확히 위임하지 않는다. 그러나 컴파일러들은 보통 같은 기본 모델에서 마이너 변수를 사용한다. 일반적으로, 컴파일러는 각 클래스에 분리된 vtable을 생성한다. 객체가 생성되면, 이 vtable에 대한 포인터 (virtual table pointer, vpointer 또는 VPTR)는 이 객체의 숨겨진 멤버로써 더해진다. 컴파일러는 또한 이 객체의 vpointer들을 vtable의 상응하는 주소로 초기화하기 위해 각 클래스의 생성자 안에 숨겨진 코드를 생성한다. 많은 컴파일러들은 vpointer를 객체의 마지막 멤버에 위치시킨다. 다른 컴파일러들은 객체의 첫 번째 멤버에 위치시키기도 한다. portable 소스 코드는 둘 중 어느 쪽으로도 작동한다.[2] 예를 들면, g++은 이전에 vpointer를 객체의 마지막에 위치시켰다.[3] 예시아래의 클래스 선언은 C++ 문법으로 선언되었다. class B1 {
public:
void f0() {}
virtual void f1() {}
int int_in_b1;
};
class B2 {
public:
virtual void f2() {}
int int_in_b2;
};
아래의 클래스에서 상속하기 위해 사용된다. class D : public B1, public B2 {
public:
void d() {}
void f2() {} // override B2::f2()
int int_in_d;
};
아래는 C++ 코드이다. B2 *b2 = new B2();
D *d = new D();
GCC의 g++ 3.4.6은 객체 b2: +0: pointer to virtual method table of B2 +4: value of int_in_b2 virtual method table of B2: +0: B2::f2() 그리고 아래는 객체 d: +0: pointer to virtual method table of D (for B1) +4: value of int_in_b1 +8: pointer to virtual method table of D (for B2) +12: value of int_in_b2 +16: value of int_in_d Total size: 20 Bytes. virtual method table of D (for B1): +0: B1::f1() // B1::f1() is not overridden virtual method table of D (for B2): +0: D::f2() // B2::f2() is overridden by D::f2() 그들의 선언에서 클래스 D에서 메소드 다중 상속과 thunks
g++ 컴파일러는 클래스 D *d = new D();
B1 *b1 = static_cast<B1*>(d);
B2 *b2 = static_cast<B2*>(d);
이 코드의 실행 이후 호출
단일 상속의 경우 (또는 오직 단일 상속만 있는 언어의 경우), 만약 vpointer가 항상 (*((*d)[0]))(d)
가상 메소드 테이블 (*(*(d[+0]/*pointer to virtual method table of D (for B1)*/)[0]))(d) /* Call d->f1() */
(*(*(d[+8]/*pointer to virtual method table of D (for B2)*/)[0]))(d+8) /* Call d->f2() */
d->f1() 호출은 (*B1::f0)(d)
효율성가상 호출은 단순하게 컴파일된 포인터로 점프하는 일반 호출과 비교해서, 최소한 추가적인 색인화된 역참조와, 가끔은 "fixup" 추가를 필요로 한다. 그러므로 가상 함수를 호출하는 것은 본질적으로 일반 호출 함수보다 느리다. 게다가 JIT 컴파일이 사용되지 않은 환경에서, 가상 함수 호출은 보통 인라인화될 수 없다. 이런 오버헤드를 피하기 위해서, 컴파일러는 보통 vtable 사용을 피한다. 그러므로 위의 그러나 숙지해야 할 점은 가상 호출은 조건부 표현이라는 것이다. 만약 가상 호출이 상속 구조를 제거하면서 제거된다면, 그때는 코드 내에서 진단되기 위하여 어떤 함수를 호출해야 하는지를 결정하는 것이 요구된다. 이것은 코드의 각 위치에서 코드 진단을 요구한다. 대안과의 비교vtable은 일반적으로 좋은 성능과 동적 디스패치 구현과의 트레이드오프이다. 그러나 대안들이 존재하는데, 더 높은 성능과 낮은 비용을 가진 바이너리 트리 디스패치가 그것이다.[4] 같이 보기주해
각주
|
Portal di Ensiklopedia Dunia