From a language perspective, the answer is: it doesn't. Nowhere in the C++ standard does it say how virtual functions are to be implemented. The compiler is free to make sure the correct function is called however it sees fit.
So, what would be gained by replacing the vptr (not the vtable) with an id and dropping the vtable? (replacing the vtable with an id doesn't really help anything whatsoever, once you have resolved vptr, you already know the run-time type)
How does the runtime know which function to actually call?
Consider:
template <int I>
struct A {
virtual void foo() {}
virtual void bar() {}
virtual ~A() {}
};
template <int I>
struct B : A<I> {
virtual void foo() {}
};
Suppose your compiler gives A<0>
the ... lets call it vid ... 0 and A<1>
the vid 1. Note that A<0>
and A<1>
are completely unrelated classes at this point. What happens if you say a0.foo()
where a0
is an A<0>
? At runtime a non-virtual function would just result in a statically dispatched call
. But for a virtual function, the address of the function-to-call must be determined at runtime.
If all you had was vid 0 you'd still have to encode which function you want. This would result in a forest of if-else branches, to figure out the correct function pointer.
if (vid == 0) {
if (fid == 0) {
call A<0>::foo();
} else if (fid == 1) {
call A<0>::bar();
} /* ... */
} else if (vid == 1) {
if (fid == 0) {
call A<1>::foo();
} else if (fid == 1) {
call A<1>::bar();
} /* ... */
} /* ... */
This would get out of hand. Hence, the table. Add an offset that identifies the foo()
function to the base of A<0>
's vtable and you have the address of the actual function to call. If you have a B<0>
object on your hands instead, add the offset to that class' table's base pointer.
In theory compilers could emit if-else code for this but it turns out a pointer addition is faster and the resulting code smaller.