10

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
user4607239
  • 101
  • 3
  • 2
    I believe `p` will still point to the memory location where `n` was, but you have no way of knowing what will be there. – mstbaum Feb 25 '15 at 19:04
  • @mstbaum and how is that related to the question? – Slava Feb 25 '15 at 19:16
  • @remyabel (My previous deleted comment asked if "can't predict the results" implies UB.) Perhaps I'm not clear on what undefined behaviour is. My reasoning is probably similar to mstbaum's, in that we don't know what is in that location in memory, so we can't predict the results. Is not that sufficient? Do I need to look it up in the standard to be sure? – eigenchris Feb 25 '15 at 19:42
  • A highly related question: http://stackoverflow.com/questions/6441218/can-a-local-variables-memory-be-accessed-outside-its-scope – eigenchris Feb 25 '15 at 19:58
  • `basic.stc.dynamic.deallocation/4` says that it's UB if `p` was pointing to dynamic storage that has now been freed, but doesn't say anything about automatic storage – M.M Feb 25 '15 at 20:00
  • 1
    @eigenchris none of those answers appear to cite whether or not it is UB (they just announce it is or isn't, without evidence) – M.M Feb 25 '15 at 20:03
  • @MattMcNabb there is one example from the standard that kind of fits this case from section `2.2` which is covered in [this question](http://stackoverflow.com/q/28506342/1708801) but clearly non-normative and it is not backed up by any normative text I can find. – Shafik Yaghmour Feb 25 '15 at 21:46
  • 1
    @eigenchris An object with an indetermine value has an unpredictable value. Would you say that behavior is *undefined*? No, because the Standard mandates it. –  Feb 25 '15 at 22:12
  • No current version of the Standard describes the behavior of Indeterminate Values in a way which could be reconciled with compiler behavior in all cases except by saying that indeterminate values are not required to behave as anything representable as a bit pattern for a type; such behavior is not limited to unusual architectures. Short types are often stored in longer registers, and if their values are never set the registers' contents may hold values that aren't valid for the shorter types. – supercat May 24 '16 at 14:23
  • Why not assign `n` to 42? – user2023370 Jan 08 '20 at 14:40

5 Answers5

6

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.

M.M
  • 138,810
  • 21
  • 208
  • 365
Mark B
  • 95,107
  • 10
  • 109
  • 188
  • 1
    No idea where this is in the standard, but since n lives on the stack here, one would think that the memory would be "released" when it's going out of scope. – Alexander Rautenberg Feb 25 '15 at 19:45
  • Additionally, my understanding of 3.7.3 ("automatic storage duration") is that the storage which the object occupied is not required to exist after the block exits. – Josh Kelley Feb 25 '15 at 19:46
  • 1
    Can you identify when an storage is *reused or released*, since it depends on that phrase. Especially since this requires this happens *before*. – Shafik Yaghmour Feb 25 '15 at 19:48
  • I'd say that "the storage is released" happens when the variable goes out of scope, so this doesn't apply (see 3.7.3/1). That section is trying to talk about situations such as placement delete and new – M.M Feb 25 '15 at 19:49
  • Re. the updated version, "such a glvalue" is still referring to " before the storage which the object occupied is reused or released," – M.M Feb 25 '15 at 20:08
  • Is it undefined behavior just to dereference it, though? Is it guaranteed that the result will be `45` for example? –  Feb 25 '15 at 22:06
2

*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.

M.M
  • 138,810
  • 21
  • 208
  • 365
2

Yes, this is undefined behavior.

n has automatic storage duration see [basic.stc.auto]p1:

Variables that belong to a block or parameter scope and are not explicitly declared static, thread_­local, or extern have automatic storage duration. The storage for these entities lasts until the block in which they are created exits.

The storage ends when the block exits, which is has in this case.

We can see from [basic.stc]p4 that p is an invalid pointer and indirection through an invalid pointer is undefined behavior:

When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values. Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.26

For completeness sake, if we look at [basic.compund]p3 we see there are four value for pointers types:

Every value of pointer type is one of the following:

  • a pointer to an object or function (the pointer is said to point to the object or function), or
  • a pointer past the end of an object ([expr.add]), or
  • the null pointer value for that type, or
  • an invalid pointer value.

and \expr.unary]p1 tells us that the expression unary * is applied to shall be an object type or a pointer to function type:

The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points. If the type of the expression is “pointer to T”, the type of the result is “T”.

Since an invalid pointer is neither of these we have undefined behavior.

Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
  • Interesting, I am looking back the older C++ drafts and this wording did not exist in C++11 nor the C++14 draft but shows up in the C++17 draft. Which is why no one found this back in 2015. – Shafik Yaghmour Oct 28 '21 at 04:28
  • Looks like this was added by [p0137r1](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0137r1.html) to address [DR 1776](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1776) – Shafik Yaghmour Oct 28 '21 at 04:32
  • Could you prove that the indirection happens through an invalid pointer value and not something else? – Language Lawyer Oct 28 '21 at 11:07
  • @LanguageLawyer I am not sure what you looking for, the storage for `n` is out of duration by the first quote, `p` point to the storage of `n` and by the second quote it is an invalid pointer. Are you looking for me to be a little more specific about this? – Shafik Yaghmour Oct 28 '21 at 16:17
  • The unary indirection operator `*` wants a value. [The object denoted by] `p` contains a[n invalid pointer] value, but this expression is originally doesn't designate the value. It is of wrong value category. What should happen to make `*` happy and how does it interact with invalid pointer values? – Language Lawyer Oct 28 '21 at 16:22
1

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 of static_cast, except when the conversion is to pointer to cv void, or to pointer to cv void and subsequently to pointer to either cv char or cv unsigned char
- [...] used as the operand of dynamic_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.

Damon
  • 67,688
  • 20
  • 135
  • 185
0

So first of all according to 3.7.3 Automatic storage duration storage of your object is released:

Block-scope variables explicitly declared register or not explicitly declared static or extern have automatic storage duration. The storage for these entities lasts until the block in which they are created exits.

And from 3.8 Object lifetime

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, 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

so dereferencing pointer to variable which storage released leads to UB

Slava
  • 43,454
  • 1
  • 47
  • 90