I can't find where in the standard that it says this program is undefined:
#include <iostream>
int main()
{
int *p;
{
int n = 45;
p = &n;
}
std::cout << *p;
}
None of the cases in §3.8 object lifetime seem to apply here.
I'm not 100% sure because of the wording but it looks like this is covered by 3.8/6 (the reason I think this interpretation is correct is because of the non-normative example in 3.8/5, // undefined behavior, lifetime of *pb has ended
):
...after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways....The program has undefined behavior if:
Then the first bullet is the culprit: an lvalue-to-rvalue conversion (4.1) is applied to such a glvalue,
: This conversion has to happen either at the point of call to operator<<
or finally at the point where the integral value is read for formatting within ostream
code.
*p
is a glvalue. The code cout << *p
necessitates an lvalue-to-rvalue conversion. This is defined by C++14 [conv.lval].
Point 2 lists various cases and describes the behaviour in each case. None of those apply to *p
. Particularly, the last point is:
Otherwise, the value contained in the object indicated by the glvalue is the prvalue result.
However, *p
does not indicate an object.
In section [basic.life] are a few cases that define what lvalue-to-rvalue conversion does, beyond what is said in [conv.lval]. Those cases relate to when storage for an object has been obtained, but we are outside the object's lifetime. However they do not apply to *p
because storage is released when the previous block ends.
So, the behaviour of this code is undefined by omission: nowhere in the Standard does it define what it means to perform rvalue conversion when the lvalue does not indicate an object and does not indicate valid storage for an object.
It can feel unsatisfactory for something to be "undefined by omission", we always like to see a concrete statement "this is undefined behaviour" to be sure we haven't overlooked something. But sometimes that is how it is.
That's certainly undefined behavior (by common sense, and by the wording of the standard).
As far as the standard goes, 3.8/5 is rather concrete about what is allowed and about what isn't:
[...] after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways [...] and using the pointer as if the pointer were of type void*, is well-defined.
Indirection [...] is permitted [...] as described below. The program has undefined behavior if:
- ...
- [...] used as operand ofstatic_cast
, except when the conversion is to pointer to cvvoid
, or to pointer to cvvoid
and subsequently to pointer to either cvchar
or cvunsigned char
- [...] used as the operand ofdynamic_cast
The object's storage ends at the end of the scope per 3.7.3/1 (in practice this is most likely not true, the stack frame will probably be reset at the end of the function, but formally that's what happens). Therefore, the dereference doesn't happen after the end of lifetime but before the release of the storage. It happens after release of the storage.
The special conditions under which you may dereference the pointer anyway do therefore not apply (the same is true for any similar paragraphs with the same precondition such as 3.8/6).
Further, assuming that the previous paragraph wasn't true, it is only allowable to dereference the pointer as cv void*
or to cast it to cv char
(signed or unsigned) prior to dereferencing. In other words, you are not allowed to look at the pointed-to int
as if it were an int
. As stated in 3.8/5, the int*
is really only a mere void*
after the lifetime of the object. Which means dereferencing it as int*
is the equivalent of doing a cast (not explicitly, but still).
One would really wish that this attempt produces an error, but I guess that's a really tough one for the compiler to detect. The pointer itself is well and alive, and it has been safely derived by taking a valid object's address, that's probably near impossible to diagnose.
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