Hold on.
Original code (slightly edited for brevity and to demonstrate the problem):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
struct Base {
virtual void foo() { x = 99; }
void bar() { x = 111; }
int x;
};
struct Derived: Base {
virtual void bar() { y = 42; }
int y;
};
void f( Base*& a ) {
}
int main() {
Derived* d = new Derived;
f( d );
}
|
Ok. There is a very subtle problem here, and it has to do with how the compiler deals with inheritance and how it
compiles member functions.
When D derives from B (to abbreviate Derived and Base), the memory for an instance of D is laid out such that
all of the memory used by the direct (non-inherited) members of D are together and all the members of B are
together. So, in my above example, The layout of an instance of B is as follows:
"this" ptr offset what is stored there
0x0000 vtable pointer for B
0x0004 int x
|
When the compiler compiles Base::foo, it knows the "this" pointer (passed implicitly as parameter), and it knows
that x is 4 bytes higher than the "this" pointer, so it writes 99 to *(this + 4). Likewise, when it compiles Base::bar,
it knows the "this" pointer (passed implicitly as parameter), and it knows that x is bytes higher than the "this"
pointer, so it writes 111 to *(this + 4).
The layout of an instance of D is as follows (this isn't quite correct, but it demonstrates the point below):
"this" ptr offset what is stored there
0x0000 vtable pointer for D
0x0004 int y
0x0008 int x
|
Now, here's the problem. Suppose you have a pointer-to-D, such as in the example above. If you call
Base::bar(), it wants to write 111 to *(this + 4). But, look at the layout. Offset 0x0004 is no longer
x, the variable that Base::bar wants to modify, but instead it is y!
How can this work? It works because the compiler is silently generating some "fixup" code for you.
1 2 3 4
|
int main() {
Derived* d = new Derived;
d->bar();
}
|
On the call to Base::bar, the compiler says: ok, you are calling bar() with a Derived*, but bar() is a member
of Base. Instead of passing in the exact pointer value of d, I'm going to kludge it a bit -- I'm going to ADD 4
to the pointer, and pass that. So I'm going to pass, essentially ( d + 4 ) as the "this" pointer to Base::bar.
Now when Base::bar takes the "this" pointer and does *( this + 4 ) = 111, it actually points to x! Voila.
Incidentally, the fact that the compiler has to generate this "fixup" code means that typecasts are not
necessarily free; indeed, they can and do generate code. But that is beside the point here.
Now, go one step further. In the declaration of f(), we take a pointer-to-Base by non-const reference,
meaning that the user intends to modify the pointer value and have the caller see the change. The
problem is that the compiler had to generate the fixup code in the first place, so in fact, you are
attempting to bind a non-const reference to a temporary variable (that variable being the kludged
"this" pointer), and the C++ standard does not allow you to do that.
And
that is why you get the compile error.
The reason why it works for this main:
1 2 3 4
|
int main() {
Base* b = new Derived; // Line A
f( b );
}
|
is because the "fixup" code is generated on Line A, and the result of the fixed up pointer is actually stored in
memory, in variable b. Thus f(b) can be called, because no fixup code needs to be generated there.