Under the hood
It can be helpful to see the
assembly-language code generated by a virtual function
call,
so you can see that late-binding is indeed taking place. Here’s the output
from one compiler for the call
i.adjust(1);
inside the function f(Instrument&
i):
push 1
push si
mov bx, word ptr [si]
call word ptr [bx+4]
add sp, 4
The arguments of a C++ function call,
like a C function call, are pushed on the stack from right to left (this order
is required to support C’s variable argument lists), so the argument
1 is pushed on the stack first. At this point in the function, the
register si (part of the Intel X86 processor architecture) contains the
address of i. This is also pushed on the stack because it is the starting
address of the object of interest. Remember that the starting address
corresponds to the value of this, and this
is quietly pushed on the stack as an argument before every member function call,
so the member function knows which particular object it is working on. So
you’ll always see one more than the number of arguments pushed on the
stack before a member function call (except for static member functions,
which have no this).
Now the actual virtual function call must
be performed. First, the VPTR must be produced, so the
VTABLE can be found. For this compiler the VPTR is
inserted at the beginning of the object, so the contents of this
correspond to the VPTR. The line
mov bx, word ptr [si]
fetches the word that si (that is,
this) points to, which is the VPTR. It places the VPTR into the
register bx.
The VPTR contained in bx points to
the starting address of the VTABLE, but the function pointer to call isn’t
at location zero of the VTABLE, but instead at location two (because it’s
the third function in the list). For this memory model each function pointer is
two bytes long, so the compiler adds four to the VPTR to calculate where the
address of the proper function is. Note that this is a constant value,
established at compile time, so the only thing that matters is that the function
pointer at location number two is the one for adjust( ).
Fortunately, the compiler takes care of all the bookkeeping for you and ensures
that all the function pointers in all the VTABLEs of a particular class
hierarchy occur in the same order, regardless of the order that you may override
them in derived classes.
Once the address of the proper function
pointer in the VTABLE is calculated, that function is called. So the address is
fetched and called all at once in the statement
call word ptr [bx+4]
Finally, the stack pointer is moved back
up to clean off the arguments that were pushed before the call. In C and C++
assembly code you’ll often see the caller clean off the arguments but this
may vary depending on processors and compiler
implementations.