I was reading a post on some nullptr
peculiarities in C++, and a particular example caused some confusion in my understanding.
Consider (simplified example from the aforementioned post):
struct A {
void non_static_mem_fn() {}
static void static_mem_fn() {}
};
A* p{nullptr};
/*1*/ *p;
/*6*/ p->non_static_mem_fn();
/*7*/ p->static_mem_fn();
According to the authors, expression /*1*/
that dereferences the nullptr
does not cause undefined behaviour by itself. Same with expression /*7*/
that uses the nullptr
-object to call a static function.
The justification is based on issue 315 in C++ Standard Core Language Closed Issues, Revision 100 that has
...
*p
is not an error whenp
is null unless the lvalue is converted to an rvalue (7.1 [conv.lval]), which it isn't here.
thus making a distinction between /*6*/
and /*7*/
.
So, the actual dereferencing of the nullptr
is not undefined behaviour (answer on SO, discussion under issue 232 of C++ Standard, ...). Thus, the validity of /*1*/
is understandable under this assumption.
However, how is /*7*/
guaranteed to not cause UB? As per the cited quote, there is no conversion of lvalue to rvalue in p->static_mem_fn();
. But the same is true for /*6*/
p->non_static_mem_fn();
, and I think my guess is confirmed by the quote from the same issue 315 regarding:
/*6*/
is explicitly noted as undefined in 12.2.2 [class.mfct.non-static], even though one could argue that sincenon_static_mem_fn();
is empty, there is no lvalue->rvalue conversion.
(in the quote, I changed "which" and f()
to get the connection to the notation used in this question).
So, why is such a distinction made for p->static_mem_fn();
and p->non_static_mem_fn();
regarding the causality of UB? Is there an intended use of calling static functions from pointers that could potentially be nullptr
?
Appendix:
nullptr
is undefined behaviour. While I agree that in most cases it is a bad idea, I do not believe the statement is absolutely correct as per the links and quotes here.nullptr
dereferencing issue. Maybe I missed some obvious answer.Null dereferencing In C, dereferencing a null pointer is undefined behavior. Many implementations cause such code to result in the program being halted with an access violation, because the null pointer representation is chosen to be an address that is never allocated by the system for storing objects.
According to ISO C++, dereferencing a null pointer is undefined behaviour.
A NULL pointer dereference occurs when the application dereferences a pointer that it expects to be valid, but is NULL, typically causing a crash or exit. NULL pointer dereference issues can occur through a number of flaws, including race conditions, and simple programming omissions.
nullptr is a (literal) constant, and these don't have a memory address, like any other literal constant in your code.
Standard citations in this answer are from the C++17 spec (N4713).
One of the sections cited in your question answers the question for non-static member functions. [class.mfct.non-static]/2:
If a non-static member function of a class
X
is called for an object that is not of typeX
, or of a type derived fromX
, the behavior is undefined.
This applies to, for example, accessing an object through a different pointer type:
std::string foo;
A *ptr = reinterpret_cast<A *>(&foo); // not UB by itself
ptr->non_static_mem_fn(); // UB by [class.mfct.non-static]/2
A null pointer doesn't point at any valid object, so it certainly doesn't point to an object of type A
either. Using your own example:
p->non_static_mem_fn(); // UB by [class.mfct.non-static]/2
With that out of the way, why does this work in the static case? Let's pull together two parts of the standard:
[expr.ref]/2:
... The expression
E1->E2
is converted to the equivalent form(*(E1)).E2
...
[class.static]/1 (emphasis mine):
... A static member may be referred to using the class member access syntax, in which case the object expression is evaluated.
The second block, in particular, says that the object expression is evaluated even for static member access. This is important if, for example, it is a function call with side effects.
Put together, this implies that these two blocks are equivalent:
// 1
p->static_mem_fn();
// 2
*p;
A::static_mem_fn();
So the final question to answer is whether *p
alone is undefined behavior when p
is a null pointer value.
Conventional wisdom would say "yes" but this is not actually true. There is nothing in the standard that states dereferencing a null pointer alone is UB and there are several discussions that directly support this:
*p
is not UB when the result is unused.There are core issues surrounding the undefined behavior of dereferencing a null pointer. It appears the intent is that dereferencing is well defined, but using the result of the dereference will yield undefined behavior. This topic is too confused to be the reference example of undefined behavior, or should be stated more precisely if it is to be retained.
*p
as defined behavior when p
is a null pointer, as long as the result is not used.In conclusion:
p->non_static_mem_fn(); // UB by [class.mfct.non-static]/2
p->static_mem_fn(); // Defined behavior per issue 232 and 315.
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