I was experimenting with C++ and found the below code as very strange.
class Foo{
public:
virtual void say_virtual_hi(){
std::cout << "Virtual Hi";
}
void say_hi()
{
std::cout << "Hi";
}
};
int main(int argc, char** argv)
{
Foo* foo = 0;
foo->say_hi(); // works well
foo->say_virtual_hi(); // will crash the app
return 0;
}
I know that the virtual method call crashes because it requires a vtable lookup and can only work with valid objects.
I have the following questions
say_hi
work on a NULL pointer?foo
get allocated?Any thoughts?
The object foo
is a local variable with type Foo*
. That variable likely gets allocated on the stack for the main
function, just like any other local variable. But the value stored in foo
is a null pointer. It doesn't point anywhere. There is no instance of type Foo
represented anywhere.
To call a virtual function, the caller needs to know which object the function is being called on. That's because the object itself is what tells which function should really be called. (That's frequently implemented by giving the object a pointer to a vtable, a list of function-pointers, and the caller just knows it's supposed to call the first function on the list, without knowing in advance where that pointer points.)
But to call a non-virtual function, the caller doesn't need to know all that. The compiler knows exactly which function will get called, so it can generate a CALL
machine-code instruction to go directly to the desired function. It simply passes a pointer to the object the function was called on as a hidden parameter to the function. In other words, the compiler translates your function call into this:
void Foo_say_hi(Foo* this);
Foo_say_hi(foo);
Now, since the implementation of that function never makes reference to any members of the object pointed to by its this
argument, you effectively dodge the bullet of dereferencing a null pointer because you never dereference one.
Formally, calling any function — even a non-virtual one — on a null pointer is undefined behavior. One of the allowed results of undefined behavior is that your code appears to run exactly as you intended. You shouldn't rely on that, although you will sometimes find libraries from your compiler vendor that do rely on that. But the compiler vendor has the advantage of being able to add further definition to what would otherwise be undefined behavior. Don't do it yourself.