A question came up here on SO asking "Why is this working" when a pointer became dangling. The answers were that it's UB, which means it may work or not.
I learned in a tutorial that:
#include <iostream>
struct Foo
{
int member;
void function() { std::cout << "hello";}
};
int main()
{
Foo* fooObj = nullptr;
fooObj->member = 5; // This will cause a read access violation but...
fooObj->function(); // Because this doesn't refer to any memory specific to
// the Foo object, and doesn't touch any of its members
// It will work.
}
Would this be the equivalent of:
static void function(Foo* fooObj) // Foo* essentially being the "this" pointer
{
std::cout << "Hello";
// Foo pointer, even though dangling or null, isn't touched. And so should
// run fine.
}
Am I wrong about this? Is it UB even though as I explained just calling a function and not accessing the invalid Foo pointer?
Compiler isn't obliged by standard to implement member function by passing it a pointer to the class instance. Yes, there is pseudo-pointer "this", but it is unrelated element, guaranteed to be "understood".
nullptr
pointer doesn't point on any existing object, and -> () calls a member of that object. From standard's view, this is nonsense and result of such operation is undefined (and potentially, catastrophic).If
function()
would be virtual, then call is allowed to fail, because address of function would be unavailable (vtable might be implemented as part of object and doesn't exist if object doesn't).if the member function (method) behaves like that and meant to be called like that it should be a static member function (method). Static method doesn't access non-static fields and doesn't call non-static methods of class. If it is static, the call could look like this as well:
In practice this is usually how major compilers implement member functions, yes. This means that your test program would probably appear to run "just fine".
Having said that, dereferencing a pointer pointing to
nullptr
is undefined behavior which means that all bets are off and the whole program and it's output is meaningless, anything could happen.You can never rely on this behavior, optimizers in particular could mess all of this code up because they're allowed to assume that
fooObj
is nevernullptr
.You're reasoning about what happens in practice. Undefined behavior is allowed to do the thing you expect... but it is not guaranteed.
For the non-static case, this is straightforward to prove using the rule found in
[class.mfct.non-static]
:Note that there's no consideration about whether the non-static member function accesses
*this
. The object is simply required to have the correct dynamic type, and*(Foo*)nullptr
certainly does not.In particular, even on platforms which use the implementation you describe, the call
gets converted to
and is optimization-unstable.
Here's an example which will work contrary to your expectations:
On real systems, this is likely to end up with an access violation on the commented line, because the compiler used the fact that
fooObj
can't be null infooObj->func()
to eliminate theif
test following it.Don't do things that are UB even if you think you know what your platform does. Optimization instability is real.
Also, the Standard is even more restrictive that you might think. This will also cause UB:
The relevant portions of the Standard are found in
[expr.ref]
:and the accompanying footnote
This means that the code in question definitely evaluates
(*fooObj)
, attempting to create a reference to a non-existent object. There have been several proposals to make this allowed and only forbid allowing lvalue->rvalue conversion on such a reference, but those have been rejected this far; even forming the reference is illegal in all versions of the Standard to date.