My take (and I'm more than happy to be corrected):
Here, CustomImage
is a polymorphic class (with a vptr as the first "member" under the Windows ABI), but Image
is not. The order of the definitions means that the ImageFactory
functions know that CustomImage
is an Image
, but main()
does not.
So when the factory does:
Image* ImageFactory::LoadFromDisk(const char* imageName)
{
return new (std::nothrow) CustomImage(imageName);
}
the CustomImage*
pointer is converted to an Image*
and everything is fine. Because Image
is not polymorphic, the pointer is adjusted to point to the (POD) Image
instance inside the CustomImage
-- but crutially, this is after the vptr, because that always comes first in a polymorphic class in the MS ABI (I assume).
However, when we get to
ImageFactory::DebugPrintDimensions((ImageFactory::CustomImage*)image);
the compiler sees a C-style cast from one class it knows nothing about to another. All it does is take the address it has and pretend it's a CustomImage*
instead. This address in fact points to the Image
within the custom image; but because Image
is empty, and presumably the empty base class optimisation is in effect, it ends up pointing to the first member within CustomImage
, i.e. the unique_ptr
.
Now, ImageFactory::DebugPrintDimensions()
assumes it's been handed a pointer to a fully-complete CustomImage
, so that the address is equal to the address of the vptr
. But it hasn't -- it's been handed the address of the unique_ptr
, because at the point at which is was called, the compiler didn't know any better. So now it dereferences what it thinks is the vptr (really data we're in control of), looks for the offset of the virtual function and blindly exectutes that -- and now we're in trouble.
There are a couple of things that could have helped mitigate this. Firstly, since we're manipulating a derived class via a base class pointer, Image
should have a virtual destructor. This would have made Image
polymorphic, and in all probability we wouldn't have had a problem (and we wouldn't leak memory, either).
Secondly, because we're casting from base-to-derived, dynamic_cast
should have been used rather than the C style cast, which would have involved a run-time check and correct pointer adjustment.
Lastly, if the compiler had all the information to hand when compiling main()
, it might have been able to warn us (or performed the cast correctly, adjusting for the polymorphic nature of CustomImage
). So moving the class definitions above main()
is recommended too.