class base {
public:
virtual void fn(){}
};
class der: public base {
public:
void fn(){}
};
der d;
base *b = &d;
b->fn();
When the compiler encounters the statement b->fn(), the following information is available to the compiler:
- b is a pointer to the class base,
- base class is having a virtual function as well as a vptr.
My question is: how does the vptr of class der come into picture at run time?
When reasoning about this, it helps me to maintain a clear image of the memory layout of the classes, and in particular to the fact that the
derobject contains abasesubobject that has exactly the same memory layout as any otherbaseobject.In particular your
baseobject layout will simply contain a pointer to the vtable (there are no fields), and thebasesubobject ofderwill also contain that pointer, only the value stored in the pointer differs and it will refer to thederversion of thebasevtable (to make it a bit more interesting, consider that bothbaseandderdid contain members):If you look at the two drawings, you can recognize the
basesubobject in thederobject, when you dobase *bp = &d;what you are doing is obtaining a pointer to thebasesubobject insideder. In this case, the memory location of thebasesubobject is exactly the same as that of thebasesubobject, but it need not be so. What is important is that the pointer will refer to thebasesubobject, and that the memory pointed to has the memory layout of abase, but with the difference that the pointers stored in the object will refer to thederversions of the vtable.When the compiler sees the code
bp->fn(), it will consider it to be abaseobject, and it knows where the vptr is in abaseobject, and it also knows thatfnis the first entry in the vtable, so it only needs to generate code forbp->vptr[ 0 ](). Ifbprefers to abaseobject thenbp->vptrwill refer to thebasevtable andbp->vptr[0]will bebase::fn. If the pointer on the other hand refers to aderobject, thenbp->vptrwill refer to thedervtable, andbp->vptr[0]will refer toder::fn.Note that at compile time the generated code for both cases is exactly the same:
bp->vptr[0](), and that it gets dispatched to different functions based on the data stored in thebase(sub)object, in particular the value stored invptr, which gets updated in construction.By clearly focusing on the fact that the
basesubobject must be present and compatible with abaseobject you can consider more complex scenarios, as multiple inheritance:This is a more interesting case, where there is another base, at this point the call
base * bp = o;creates a pointer to thebasesubobject and can be verified to point to a different location than theoobject (try printing out the values of&oandbp). From the calling site, that does not really matter becausebphas static typebase*, and the compiler can always dereference that pointer to locatebase::vptr, use that to locatefnin the vtable and end up callingother::fn.There is a bit more magic going on in this example though, as the
otherandbasesubobjects are not aligned, before calling the actual functionother::fn, thethispointer has to be adjusted. The compiler resolves by not storing a pointer toother::fnin theothervtable, but rather a pointer to a virtual thunk (small piece of code that fixes the value ofthisand forwards the call toother::fn)