Pointers to member functions are very strange animals
Pointers to member functions are very strange animals.
Warning: The discussion that follows is specific to
the way pointers to member functions are implemented by the
Microsoft Visual C++ compiler. Other compilers may do things
differently.
Well, okay, if you only use single inheritance, then
pointers to member functions are just a pointer to the
start of the function, since all the base classes
share the same "this" pointer:
class Simple { int s; void SimpleMethod(); };
class Simple2 : public Simple
{ int s2; void Simple2Method(); };
class Simple3 : public Simple2
{ int s3; Simple3Method(); };
Since they all use the same "this" pointer (p), a pointer to
a member function of Base can be used as if it were a pointer
to a member function of Derived2 without any adjustment
necessary.
The size of a pointer-to-member-function of a class that
uses only single inheritance is just the size of a pointer.
But if you have multiple base classes, then things get interesting.
class Base1 { int b1; void Base1Method(); };
class Base2 { int b2; void Base2Method(); };
class Derived : public Base1, Base2
{ int d; void DerivedMethod(); };
| p |
|
Base1::b1
|
| q |
|
Base2::b2
Derived::d
|
There are now two possible "this" pointers. The first (p) is used
by both Derived and Base1, but the second (q) is used by Base2.
A pointer to a member function of Base1 can be used as a pointer
to a member function of Derived, since they both use the same "this"
pointer. But a pointer to a member function of Base2 cannot be
used as-is as a pointer to a member function of Derived, since the
"this" pointer needs to be adjusted.
There are many ways of solving this.
Here's how the Visual Studio compiler decides to handle it:
A pointer to a member function of a multiply-inherited class
is really a structure.
|
Address of function
|
|
Adjustor
|
The size of a pointer-to-member-function of a class that
uses multiple inheritance is the size of a pointer plus the
size of a size_t.
Compare this to the case of a class that uses only single inheritance.
The size of a pointer-to-member-function can change depending on
the class!
Aside: Sadly, this means that Rich Hickey's wonderful technique of
Callbacks
in C++ Using Template Functors cannot be used as-is.
You have to fix the place where he writes the comment
// Note: this code depends on all ptr-to-mem-funcs being same size
Okay, back to our story.
To call through a pointer to a member function, the "this"
pointer is adjusted by the Adjustor, and then the function
provided is called. A call through
a function pointer might be compiled like this:
void (Derived::*pfn)();
Derived d;
(d.*pfn)();
lea ecx, d ; ecx = "this"
add ecx, pfn[4] ; add adjustor
call pfn[0] ; call
When would an adjustor be nonzero? Consider the case above.
The function Derived::Base2Method() is really Base2::Base2Method()
and therefore expects to receive "q" as its "this" pointer.
In order to convert a "p" to a "q", the adjustor must have the
value sizeof(Base1), so that when the first line of Base2::Base2Method()
executes, it receives the expected "q" as its "this" pointer.
"But why not just use a thunk instead of manually adding the adjustor?"
In other words, why not just use a simple pointer to a thunk that
goes like this:
Derived::Base2Method thunk:
add ecx, sizeof(Base1) ; convert "p" to "q"
jmp Base2::Base2Method ; continue
and use that thunk as the function pointer?
The reason: Function pointer casts.
Consider the following code:
void (Base2::*pfnBase2)();
void (Derived::*pfnDerived)();
pfnDerived = pfnBase2;
mov ecx, pfnBase2 ; ecx = address
mov pfnDerived[0], ecx
mov pfnDerived[4], sizeof(Base1) ; adjustor!
We start with a pointer to a member function of Base2,
which is a class that uses only single inheritance, so it
consists of just a pointer to the code.
To assign it to a pointer to a member function
of Derived, which uses multiple inheritance, we can re-use
the function address, but we now need an adjustor so that
the pointer "p" can properly be converted to a "q".
Notice that the code doesn't know what function pfnBase2
points to, so it can't just replace it with the matching thunk.
It would have to generate a thunk at runtime and somehow
use its psychic powers to decide when the memory can safely
be freed. (This is C++. No garbage collector here.)
Notice also that when pfnBase2
got cast to a pointer to member function of Derived, its size changed,
since it went from a pointer to a function in a class that uses only single
inheritance to a pointer to a function in a class
that uses multiple inheritance.
Casting a function pointer can change its size!
I bet that you didn't know that before reading this entry.
There's still an awful lot more to this topic,
but I'm going to stop here before everybody's head explodes.
Exercise:
Consider the class
class Base3 { int b3; void Base3Method(); };
class Derived2 : public Base3, public Derived { };
How would the following code be compiled?
void (Derived::*pfnDerived)();
void (Derived2::*pfnDerived2();
pfnDerived2 = pfnDerived;
Answer to appear tomorrow.