user4607239
user4607239

Reputation: 101

Is it undefined behavior to dereference a dangling pointer?

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.

Upvotes: 10

Views: 2877

Answers (5)

Shafik Yaghmour
Shafik Yaghmour

Reputation: 158459

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.

Upvotes: 2

M.M
M.M

Reputation: 141554

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

Upvotes: 2

Mark B
Mark B

Reputation: 96243

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.

Upvotes: 6

Damon
Damon

Reputation: 70126

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.

Upvotes: 1

Slava
Slava

Reputation: 44238

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

Upvotes: 0

Related Questions