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?
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
Xis called for an object that is not of typeX, or of a type derived fromX, 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->E2is 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With