True, but that does not contradict what I said.
§4.10
"A null pointer constant is an integral constant expression (5.19)
rvalue of integer type that evaluates to zero. A null pointer constant
can be converted to a pointer type; the result is the null pointer
value of that type..."
As I pointed out, the null pointer constant is not the same thing as a
null pointer so I was agreeing with you.
This is, perhaps, pedantic, but 0 and NULL are integer literals. They are
replaced by the compiler with (instructions to generate) temporary integer
values which are used to initialize the lvalues to which they are assigned.
A null pointer constant would be something like `const int null_ptr = 0'.
In that case null_ptr would live throughout the life of the scope in which
it is defined. And that probably means global in this case.
Using null_ptr instead of NULL is also an error, but it is a different kind
of error. In the case of NULL, the error is attempting to initialize a
non-const reference with a temporary. In the case of null_ptr the error is
attempting to initialize a non-const reference with a const object. In the
former, you are trying to establish a persistent means of accessing
something which is transient. In the latter, you are trying to establish a
means of accessing and modifying something that is constant.
Undefined simply means you've left the world of standard C++ and
your program could do anything. As some on Usenet are fond of saying,
blue demons could fly out of your nose.
You enter the land of undefined behavior as soon as you dereference
a null pointer. How and when that manifests in some particular way
in a particular case is... well... undefined.
Exactly. It's the C++ analog to proving 1 = 0 in mathematics, or true =
false formal logic.
A segfault is one possible consequence of doing something that causes
undefined behavior.
I am aware that the Standard does not dictate that a segfault will happen.
There are times when undefined behavior is identical to what would happen
if the statement resulting in undefined behavior did not appear in the
program. A crash is actually more desirable than the alternative of having
the program continue to run with some subtle undefined behavior lurking in
it. For example, the program may run perfectly for almost all input, but
when it encounters a field beyond a certain length, it may truncate, or
overwrite the end of the field with garbage.
An lvalue is just an expression that refers to an object or function
(§3.10) and an object occupies storage. A local variable of type int
is an lvalue, for example. This says nothing about how a C++
implementation might choose to represent pointers and references
under the hood.
But to say that something occupies storage basically means it has an address
of some kind. I don't care if you want to say an lvalue is a box in a
warehouse. There will still be some way of referring to it by means of
coordinates.
Very well, the result of dereferencing a null pointer is undefined.
This is probably a more important distinction than it may seem. Undefined
behavior may appear anywhere, and at any time in the execution of the
program after the statement producing it has been encountered.
You said "AFAIK, you *can* pass a null to func() in your example",
and the example in question declared func as taking a parameter of
reference type.
I believe that was in response to the example using the integer literal 0 as
the argument to a function taking a non-const reference as a parameter. My
point was that you /can/ dereference a pointer and use it as an argument in
that situation. That code will compile, and the pointer being dereferenced
may be null when the program is executed. I had left it as implied that
this is probably not what the programmer would want.
Technically, the null pointer is passed to the indirection expression
appearing in the argument list, and the (result of evaluating the)
indirection expression is passed to func(). So what I really meant by
'passing a null' is that a null pointer can appear in the indirection
expression passed as an argument to func() without a compiler error being
produced.
At a practical level, I think I understand your general point that
using references instead of pointers does not really protect you
against undefined behavior. Undefined behavior could still occur
if you form a reference by dereferencing a null pointer, return
a reference to a local object, etc.
My point in the particular instance was that the examples presented by the
OP were not addressing the issues they were intended to address.
However, my response would be that it's the responsibility of
the code that initializes the reference (e.g., by dereferencing
a pointer in your example) to make sure the object actually exists.
As a general rule, I don't believe that's possible. Firstly, you don't want
to dereference a null pointer, you want to compare it to 0, or compare the
result of a dynamic_cast on it to 0. That protects against dereferencing a
null pointer. Unfortunately, you cannot depend on the result of a
dynamic_cast to determine that an object is valid. A dynamic_cast will
only tell you if the operand "claims to be" valid. For example, after
calling delete on an object of type Foo, the memory holding Foo may
(probably will) still hold exactly what it held before the delete.
dynamic_casting a pointer to that memory location will result in returning
a pointer to type Foo with the address of the deleted object. The object
is, by definition, invalid but may well behave as if it were valid.
That's the code that needs to be scrutinized for possible undefined
behavior. A function that takes a reference parameter should be
able to assume that the reference is actually bound to an object;
if it is not then the program is already undefined and the fault
lies elsewhere.
Yes, and I believe part of the OP's argument for using references over
pointers was that it reduces the need for detecting null pointers, and thus
results in more efficient code. I don't believe that reasoning is viable.