Virtual function calling mechanism
cl开发者_Python百科ass 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?
The Holy Standard does not require a vptr or a vptr table. However in practice that’s the only way this is implemented.
So here’s psudo-code for what happens:
a_base_compatible_vtable_ptr = b->__vtable_ptr__
a_func_ptr = a_base_compatible_vtable_ptr[INDEX_FOR_fn]
a_func_ptr( b )
A main insight is that for an object of the der
class the vtable pointer in the object will point to the der
class’ vtable, which is compatible with the base
class’ vtable, but contains pointers pointing to the der
class’ function implementations.
Thus, the der
implementation of the function is called.
In practice the this
pointer argument passing in point (3) is typically optimized, special, by passing the this
pointer in a dedicated processor register instead of on the machine stack.
For more in depth discussion see the literature on C++ memory model, e.g. Stanly Lippman’s book Inside the C++ Object Model.
Cheers & hth.,
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 der
object contains a base
subobject that has exactly the same memory layout as any other base
object.
In particular your base
object layout will simply contain a pointer to the vtable (there are no fields), and the base
subobject of der
will also contain that pointer, only the value stored in the pointer differs and it will refer to the der
version of the base
vtable (to make it a bit more interesting, consider that both base
and der
did contain members):
// Base object // base vtable (ignoring type info)
+-------------+ +-----------+
| base::vptr |------> | &base::fn |
+-------------+ +-----------+
| base fields |
+-------------+
// Derived object // der vtable
+-------------+ +-----------+
| base::vptr |------> | &der::fn |
+-------------+ +-----------+
| base fields |
+-------------+ <----- [ base subobject ends here ]
| der fields |
+-------------+
If you look at the two drawings, you can recognize the base
subobject in the der
object, when you do base *bp = &d;
what you are doing is obtaining a pointer to the base
subobject inside der
. In this case, the memory location of the base
subobject is exactly the same as that of the base
subobject, but it need not be so. What is important is that the pointer will refer to the base
subobject, and that the memory pointed to has the memory layout of a base
, but with the difference that the pointers stored in the object will refer to the der
versions of the vtable.
When the compiler sees the code bp->fn()
, it will consider it to be a base
object, and it knows where the vptr is in a base
object, and it also knows that fn
is the first entry in the vtable, so it only needs to generate code for bp->vptr[ 0 ]()
. If bp
refers to a base
object then bp->vptr
will refer to the base
vtable and bp->vptr[0]
will be base::fn
. If the pointer on the other hand refers to a der
object, then bp->vptr
will refer to the der
vtable, and bp->vptr[0]
will refer to der::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 the base
(sub)object, in particular the value stored in vptr
, which gets updated in construction.
By clearly focusing on the fact that the base
subobject must be present and compatible with a base
object you can consider more complex scenarios, as multiple inheritance:
struct data {
int x;
};
class other : public data, public base {
int y;
public:
virtual void fn() {}
};
+-------------+
| data::x |
+-------------+ <----- [ base subobject starts here ]
| base::vptr |
+-------------+
| base fields |
+-------------+ <----- [ base subobject ends here ]
| other::y |
+-------------+
int main() {
other o;
base *bp = o;
}
This is a more interesting case, where there is another base, at this point the call base * bp = o;
creates a pointer to the base
subobject and can be verified to point to a different location than the o
object (try printing out the values of &o
and bp
). From the calling site, that does not really matter because bp
has static type base*
, and the compiler can always dereference that pointer to locate base::vptr
, use that to locate fn
in the vtable and end up calling other::fn
.
There is a bit more magic going on in this example though, as the other
and base
subobjects are not aligned, before calling the actual function other::fn
, the this
pointer has to be adjusted. The compiler resolves by not storing a pointer to other::fn
in the other
vtable, but rather a pointer to a virtual thunk (small piece of code that fixes the value of this
and forwards the call to other::fn
)
In a typical implementation there is only one vptr
per object. If the object is of type der
that pointer will point to the der
vtable, while if it is of type base
it points to the base
vtable. This pointer is set upon construction. Something similar to this:
class base {
public:
base() {
vptr = &vtable_base;
}
virtual void fn(){}
protected:
vtable* vptr;
};
class der: public base {
public:
der() {
vptr = &vtable_der;
}
void fn(){}
};
A call to b->fn()
does something similar to:
vtable* vptr = b->vptr;
void (*fn_ptr)() = vtpr[fn_index];
fn_ptr(b);
i found the best answer over Here..
Vptr is a property of the object, not of the pointer. Therefore the static type of b
(base
) does not matter. What does matter is its dynamic type (der
). The object pointed to by b
has its vptr pointing to der
's virtual method table (vtbl).
When you call b->fn()
, it's der
's virtual method table that gets consulted to figure out which method to call.
The vptr is not "of the class", but of the "instantiated object".
When d
is constructed, first the space for it is allocated (and contains the vptr as well).
Then base
is constructed (and the vptr is made pointing to base vtable) and then der
is constructed around base
and the vptr is updated to point to the der
vtable.
Both the base
and der
vtables have entries for the fn()
functions, and since the vptr refer t othe der one, when you call b->fn(), in fact what happens is a call to vptr(p)[fn_index]()
is called.
But since vptr(p) == vptr(&d), in this case, a call to der::fn
will results.
精彩评论