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]
:
If a non-static member function of a class X
is called for an object that is not of type X
, or of a type derived from X
, the behavior is undefined.
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
fooObj->func();
gets converted to
__assume(fooObj); Foo_func(fooObj);
and is optimization-unstable.
Here's an example which will work contrary to your expectations:
int main()
{
Foo* fooObj = nullptr;
fooObj->func();
if (fooObj) {
fooObj->member = 5; // This will cause a read access violation!
}
}
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 in fooObj->func()
to eliminate the if
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:
struct Foo
{
int member;
void func() { std::cout << "hello";}
static void s_func() { std::cout << "greetings";}
};
int main()
{
Foo* fooObj = nullptr;
fooObj->s_func(); // well-formed call to static member,
// but unlike Foo::s_func(), it requires *fooObj to be a valid object of type Foo
}
The relevant portions of the Standard are found in [expr.ref]
:
The expression E1->E2
is converted to the equivalent form (*(E1)).E2
and the accompanying footnote
If the class member access expression is evaluated, the subexpression evaluation happens even if the result is unnecessary to determine the value of the entire postfix expression, for example if the id-expression denotes a static member.
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.