Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Post History

57%
+2 −1
Q&A Is it UB to read value of heap pointer after freeing?

EDIT 3: So the C standard example is now apparently being misinterpreted. Guess I’ll have to explain it. EDIT 2: So I’ve been asked to provide sources for my answer. Fair enough. To recap, my answ...

posted 26d ago by Indi‭  ·  edited 13d ago by Indi‭

Answer
#4: Post edited by user avatar Indi‭ · 2026-09-04T21:48:31Z (13 days ago)
  • **EDIT 2:** So I’ve been asked to provide sources for my answer. Fair enough. To recap, my answer is:
  • 1. A pointer to an object becomes indeterminate when that object’s lifetime ends.
  • 2. Accessing that indeterminate value is UB.
  • My source is the current ISO C standard, ISO/IEC 9899:2023, 6.2.4.p2. I’ve inserted bold highlighting, and my notes in parentheses:
  • > (...) If an object is referred to outside of its lifetime, the behavior is undefined. **If a pointer value is used in an evaluation after the object the pointer points to (or just past) reaches the end of its lifetime, the behavior is undefined.** (← that’s point 2) **The representation of a pointer object becomes indeterminate when the object the pointer points to (or just past) reaches the end of its lifetime.** (← that’s point 1)
  • There is also a second section later on that makes the same point in a more complex and roundabout way, in 6.5.3.6.p19-20. It shows a block of code with a fake loop construct using `goto`, where the pointer is set to an object that exists within the “loop”, then used in a comparison later. It says (in p19) that this is well-defined, because the object is still alive when the comparison happens. Then p20 says (again, the bold is from me):
  • > If an iteration statement were used instead of an explicit `goto` and a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time around **`p` would have indeterminate representation, which would result in undefined behavior**.
  • I don’t claim to be a language lawyer, but I see no other interpretation to that, other than the answer to the topic question is unequivocally “yes”. Not “maybe”. Not “on some platforms”. Just “yes, it’s UB”. Always.
  • I have also been asked for a pre-C++26 source. Again, fair enough; C++26 is *technically* not the *current* standard (yet!).
  • But I don’t have the C++23 standard, so here is something from the C++17 standard, which is from about a decade ago. This is from `[dcl.init]`, paragraph 12:
  • > If no initializer is specified for an object, the object is default-initialized. When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced (8.18). \[ Note: Objects with static or thread storage duration are zero-initialized, see 6.6.2. — end note ] **If an indeterminate value is produced by an evaluation, the behavior is undefined** except in the following cases:
  • (The “following cases” are exemptions for the “raw storage” types—`unsigned char` and `std::byte`—that are too complex to go into here, and don’t really matter.)
  • There may have been some confusion because in a section dedicated specifically to “`NullablePointer` requirements” (`[nullablepointer.requirements]`) it only says it **MAY** cause undefined behaviour:
  • > paragraph 1: A `NullablePointer` type is a pointer-like type that supports null values. A type `P` meets the requirements of `NullablePointer` if: (...)
  • >
  • > paragraph 2: A value-initialized object of type `P` produces the null value of the type. The null value shall be equivalent only to itself. A default-initialized object of type `P` may have an indeterminate value. \[ *Note:* Operations involving indeterminate values may cause undefined behavior. *— end note* ]
  • But `NullablePointer` is not *specifically* about raw pointers; it is a generic requirement. `std::unique_ptr` is a `NullablePointer`, and it does *not* get default-initialized to an indeterminate value, and thus does not cause undefined behaviour. A smart pointer type *could* conceivably get default-initialized to an indeterminate value, and thus (potentially) cause UB… but I don’t think any standard library ones do.
  • Also, it does only say that operations on indeterminate values *may* cause undefined behaviour. The reason why is simply because *not all operations on indeterminate values cause undefined behaviour*. Specifically, as stated in `[dcl.init]p12`, *replacing* an indeterminate value is fine (the section “8.18” mentioned in parentheses is the section on assignment operations). However, again, as `[dcl.init]p12` very clearly states, any operation that *produces* an indeterminate value… such as reading a variable that has indeterminate value… *is* UB. So that “may” is *not* saying that reading indeterminate values is sometimes not UB… it is saying what it literally says: that some operations on indeterminate values are not UB: Overwriting an indeterminate value is not UB; any operation that produces an indeterminate value (that is: any load) *is* UB.
  • ---
  • **EDIT:** Talked with a C++ expert, and I was misunderstanding EB.
  • **tl;dr** Reading a pointer value after free is definitely UB.
  • **RATIONALE:**
  • 1. After free, a pointer value becomes *indeterminate*.
  • 2. Any read of an indeterminate value is undefined behaviour.
  • My misconception about erroneous behaviour was that some reads of indeterminate values became EB rather than UB. That was wrong. What C++26 did was create a *new* type of value—an *erroneous value*—and said that *some* formerly indeterminate values were now erroneous values. Reading an erroneous value is EB… but reading any indeterminate value remains UB.
  • For example:
  • * Before C++26, `int a;` leaves `a` with an indeterminate value. Doing anything with an indeterminate value, like `int b = a;`, is UB.
  • * C++26 made the value of `a` in `int a;` an *erroneous* value instead. So now doing anything with it, like `int b = a;`, is EB, not UB.
  • But a pointer after free is still an indeterminate value, so reading it is still UB.
  • ---
  • **ORIGINAL ANSWER:**
  • I am not a C expert, but I believe the answer is that it definitely *used to be* undefined behaviour (UB)… but may now only be *implementation-defined* behaviour (IB), or something similar. To be clear, I am not *sure* that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.
  • *However*… I believe what you are actually doing in that code might be unequivocally elevating it to UB.
  • In case you aren’t aware, the difference is:
  • * IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) *incorrect* (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean *the whole program* is invalid.
  • * UB, undefined behaviour, means that “anything can happen”… *literally* anything… and no-one is obliged to even attempt to tell you what that might include, because *any* instance of UB means *the whole program* is invalid.
  • I think your reasoning about debugging is correct. *De-referencing* a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.
  • The *technical* wording of it boils down to that the value in a pointer becomes *indeterminate*… not necessarily *invalid*… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.
  • But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) *used to be* UB, but I *think* that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.
  • Again there’s the *however*…
  • You have passed a pointer with an indeterminate value to `printf()`. Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value *may* be invalid, and I see no exemption for indeterminate pointer values in `printf()`, so… kaboom goes the program. Maybe.
  • So in summary:
  • * A pointer value becomes *indeterminate* when it no longer points to a valid object, such as after de-allocation.
  • * Reading or copying an indeterminate value (of any kind, not just pointers) *was* definitely UB at some point, but that *may* have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
  • * A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
  • * Passing an invalid value to C library functions is UB… but passing an *indeterminate* value…?
  • Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.
  • But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: **Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.**
  • **EDIT 3:** So the C standard example is now apparently being misinterpreted. Guess I’ll have to explain it.
  • **EDIT 2:** So I’ve been asked to provide sources for my answer. Fair enough. To recap, my answer is:
  • 1. A pointer to an object becomes indeterminate when that object’s lifetime ends.
  • 2. Accessing that indeterminate value is UB.
  • My source is the current ISO C standard, ISO/IEC 9899:2023, 6.2.4.p2. I’ve inserted bold highlighting, and my notes in parentheses:
  • > (...) If an object is referred to outside of its lifetime, the behavior is undefined. **If a pointer value is used in an evaluation after the object the pointer points to (or just past) reaches the end of its lifetime, the behavior is undefined.** (← that’s point 2) **The representation of a pointer object becomes indeterminate when the object the pointer points to (or just past) reaches the end of its lifetime.** (← that’s point 1)
  • There is also a second section later on that makes the same point in a more complex and roundabout way, in 6.5.3.6.p19-20. It shows a block of code with a fake loop construct using `goto`, where the pointer is set to an object that exists within the “loop”, then used in a comparison later. It says (in p19) that this is well-defined, because the object is still alive when the comparison happens. Then p20 says (again, the bold is from me):
  • > If an iteration statement were used instead of an explicit `goto` and a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time around **`p` would have indeterminate representation, which would result in undefined behavior**.
  • I don’t claim to be a language lawyer, but I see no other interpretation to that, other than the answer to the topic question is unequivocally “yes”. Not “maybe”. Not “on some platforms”. Just “yes, it’s UB”. Always.
  • **<<<<<<<< (BEGIN EDIT 3) >>>>>>>>**
  • Let’s look at the actual code in the example:
  • ```c
  • struct s { int i; };
  • int f (void)
  • {
  • struct s *p = 0, *q;
  • int j = 0;
  • again:
  • q = p, p = &((struct s){ j++ });
  • if (j < 2) goto again;
  • return p == q && q->i == 1;
  • }
  • ```
  • And this is the actual text:
  • > (paragraph 19) EXAMPLE 9 Each compound literal creates only a single object in a given scope:
  • >
  • > The function `f()` always returns the value 1.
  • >
  • > (paragraph 20) If an iteration statement were used instead of an explicit `goto` and a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time around `p` would have indeterminate representation, which would result in undefined behavior.
  • It’s paragraph 20 that is most relevant to us. It’s describing a hypothetical modification to the example that might look like this:
  • ```c
  • struct s { int i; };
  • int f (void)
  • {
  • struct s *p = 0, *q;
  • int j = 0;
  • do
  • {
  • q = p, p = &((struct s){ j++ });
  • } while (j < 2);
  • return p == q && q->i == 1;
  • }
  • ```
  • On the entry to the loop the *first* time around, `p` is `0`, `q` is 🤷🏼, and `j` is `0`. That should be clear, I hope.
  • At the *end* of the loop the first time around, `p` points to the unnamed object, `q` is `0`, and `j` is `1`. Step through the code carefully to convince yourself that this is true.
  • Now here’s where the divergence happens. in the `goto` version, execution just jumps back up to the label… *but nothing else happens*. So `p` still points to the unnamed object created the first time through the loop, `q` is still `0`, and `j` is still `1`, and we continue from there. No problems.
  • But in the structured loop version, *before* jumping back to the start of the loop, the unnamed object `p` points to ends its lifetime. Per 6.2.4.p2… and **these are the literal words of the standard**: “The representation of a pointer object becomes indeterminate when the object the pointer points to (or just past) reaches the end of its lifetime.” The object `p` points to has reached the end of its lifetime, so `p` becomes indeterminate. Not `*p`. `p` becomes indeterminate.
  • So now, **on entry next time around `p` would have indeterminate representation**… that is the **literal text of the standard**… `q` is `0`, and `j` is `1`. And then there are 3 expressions within the loop:
  • 1. `q = p`
  • 2. `p = &((struct s){ j++ })`
  • 3. `j < 2`
  • (Technically there is a 4th and a 5th expression. One is the evaluation of the *result* of `q = p`… which is effectively evaluating `q` after the assignment. There is also the evaluation of the result of `p = &((struct s){ j++ })`, both as the result of the assignment, and due to the comma operator. But in both cases, if the assignment itself is kosher, then result of the assignment is also kosher (because both assignments are assignments to the same type, so no conversions are happening). So we’ll just skip worrying about those for brevity.)
  • Given that `j` is `1`, there is no possibility that either the evaluation of `j` in 2, or the entire expression 3, can either be UB.
  • That leaves 2 possibilities:
  • 1. `q = p`
  • 2. `p = <address of a struct>`
  • Now `p` is indeterminate, but even so, the assignment in 2 cannot be UB, because it is an assignment expression, and the left-hand operand of an assignment expression does not get evaluated (until after assignment). This should be obvious. If the lhs of an assignment were evaluated, then `int i; i = 0;` would be UB on any system where `int` has trap representations, and the uninitialized `int` gets set to one of those trap representation. But you don’t even need intuition to figure this out. The literal text of the standard describing how assignment works (6.5.17.2p2) says that the value in the left hand side gets replaced… not evaluated: replaced. (By contrast, the following section on *compound* assignment *does* literally specify that the left-hand side gets evaluated (once). So they obviously didn’t just “forget” to mention it.)
  • So the only possible source for UB “on entry next time around” is expression 1: `q = p`. And it can’t be the `q` causing the issue, because `0` is a perfectly cromulent value for a pointer (it sets the pointer value to the null value)… and even if it weren’t, well, the lhs of an assignment isn’t evaluated until after the assignment. So `q` can’t be the problem.
  • The only possible source for the UB has to be `p`. Not `*p`. Just `p`. `p` has indeterminate value. The simple act of evaluating that indeterminate value in order to put it in `q` is what triggers the UB.
  • This jibes with the text of 6.2.4.p2: “If a pointer value is used in an evaluation after the object the pointer points to (or just past) reaches the end of its lifetime, the behavior is undefined.”
  • It also matches the text describing the example: “If an iteration statement were used instead of an explicit `goto` and a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time around `p` would have indeterminate representation, which would result in undefined behavior.”
  • And it matches the case in the OP question exactly: There is an object which has been destroyed, and a dangling pointer to it… **and MERELY reading the value of that pointer… NOT dereferencing it, just evaluating it… is UB**.
  • There is literal and explicit text in the C standard saying that a pointer to an object becomes indeterminate when the object lifetime ends, and that evaluating that pointer—getting its value in any context—is, literally and explicitly, undefined behaviour. And it is backed up with an example that reinforces that.
  • There is not a single word in the standard saying that it is safe to access a pointer value after the object it points to has gone out of scope. There is not a single example showing that this is okay.
  • Q.E.D. ∎
  • **<<<<<<<< (END EDIT 3) >>>>>>>>**
  • I have also been asked for a pre-C++26 source. Again, fair enough; C++26 is *technically* not the *current* standard (yet!).
  • But I don’t have the C++23 standard, so here is something from the C++17 standard, which is from about a decade ago. This is from `[dcl.init]`, paragraph 12:
  • > If no initializer is specified for an object, the object is default-initialized. When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced (8.18). \[ Note: Objects with static or thread storage duration are zero-initialized, see 6.6.2. — end note ] **If an indeterminate value is produced by an evaluation, the behavior is undefined** except in the following cases:
  • (The “following cases” are exemptions for the “raw storage” types—`unsigned char` and `std::byte`—that are too complex to go into here, and don’t really matter.)
  • There may have been some confusion because in a section dedicated specifically to “`NullablePointer` requirements” (`[nullablepointer.requirements]`) it only says it **MAY** cause undefined behaviour:
  • > paragraph 1: A `NullablePointer` type is a pointer-like type that supports null values. A type `P` meets the requirements of `NullablePointer` if: (...)
  • >
  • > paragraph 2: A value-initialized object of type `P` produces the null value of the type. The null value shall be equivalent only to itself. A default-initialized object of type `P` may have an indeterminate value. \[ *Note:* Operations involving indeterminate values may cause undefined behavior. *— end note* ]
  • But `NullablePointer` is not *specifically* about raw pointers; it is a generic requirement. `std::unique_ptr` is a `NullablePointer`, and it does *not* get default-initialized to an indeterminate value, and thus does not cause undefined behaviour. A smart pointer type *could* conceivably get default-initialized to an indeterminate value, and thus (potentially) cause UB… but I don’t think any standard library ones do.
  • Also, it does only say that operations on indeterminate values *may* cause undefined behaviour. The reason why is simply because *not all operations on indeterminate values cause undefined behaviour*. Specifically, as stated in `[dcl.init]p12`, *replacing* an indeterminate value is fine (the section “8.18” mentioned in parentheses is the section on assignment operations). However, again, as `[dcl.init]p12` very clearly states, any operation that *produces* an indeterminate value… such as reading a variable that has indeterminate value… *is* UB. So that “may” is *not* saying that reading indeterminate values is sometimes not UB… it is saying what it literally says: that some operations on indeterminate values are not UB: Overwriting an indeterminate value is not UB; any operation that produces an indeterminate value (that is: any load) *is* UB.
  • ---
  • **EDIT:** Talked with a C++ expert, and I was misunderstanding EB.
  • **tl;dr** Reading a pointer value after free is definitely UB.
  • **RATIONALE:**
  • 1. After free, a pointer value becomes *indeterminate*.
  • 2. Any read of an indeterminate value is undefined behaviour.
  • My misconception about erroneous behaviour was that some reads of indeterminate values became EB rather than UB. That was wrong. What C++26 did was create a *new* type of value—an *erroneous value*—and said that *some* formerly indeterminate values were now erroneous values. Reading an erroneous value is EB… but reading any indeterminate value remains UB.
  • For example:
  • * Before C++26, `int a;` leaves `a` with an indeterminate value. Doing anything with an indeterminate value, like `int b = a;`, is UB.
  • * C++26 made the value of `a` in `int a;` an *erroneous* value instead. So now doing anything with it, like `int b = a;`, is EB, not UB.
  • But a pointer after free is still an indeterminate value, so reading it is still UB.
  • ---
  • **ORIGINAL ANSWER:**
  • I am not a C expert, but I believe the answer is that it definitely *used to be* undefined behaviour (UB)… but may now only be *implementation-defined* behaviour (IB), or something similar. To be clear, I am not *sure* that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.
  • *However*… I believe what you are actually doing in that code might be unequivocally elevating it to UB.
  • In case you aren’t aware, the difference is:
  • * IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) *incorrect* (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean *the whole program* is invalid.
  • * UB, undefined behaviour, means that “anything can happen”… *literally* anything… and no-one is obliged to even attempt to tell you what that might include, because *any* instance of UB means *the whole program* is invalid.
  • I think your reasoning about debugging is correct. *De-referencing* a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.
  • The *technical* wording of it boils down to that the value in a pointer becomes *indeterminate*… not necessarily *invalid*… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.
  • But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) *used to be* UB, but I *think* that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.
  • Again there’s the *however*…
  • You have passed a pointer with an indeterminate value to `printf()`. Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value *may* be invalid, and I see no exemption for indeterminate pointer values in `printf()`, so… kaboom goes the program. Maybe.
  • So in summary:
  • * A pointer value becomes *indeterminate* when it no longer points to a valid object, such as after de-allocation.
  • * Reading or copying an indeterminate value (of any kind, not just pointers) *was* definitely UB at some point, but that *may* have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
  • * A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
  • * Passing an invalid value to C library functions is UB… but passing an *indeterminate* value…?
  • Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.
  • But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: **Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.**
#3: Post edited by user avatar Indi‭ · 2026-08-31T19:48:30Z (17 days ago)
  • **EDIT:** Talked with a C++ expert, and I was misunderstanding EB.
  • **tl;dr** Reading a pointer value after free is definitely UB.
  • **RATIONALE:**
  • 1. After free, a pointer value becomes *indeterminate*.
  • 2. Any read of an indeterminate value is undefined behaviour.
  • My misconception about erroneous behaviour was that some reads of indeterminate values became EB rather than UB. That was wrong. What C++26 did was create a *new* type of value—an *erroneous value*—and said that *some* formerly indeterminate values were now erroneous values. Reading an erroneous value is EB… but reading any indeterminate value remains UB.
  • For example:
  • * Before C++26, `int a;` leaves `a` with an indeterminate value. Doing anything with an indeterminate value, like `int b = a;`, is UB.
  • * C++26 made the value of `a` in `int a;` an *erroneous* value instead. So now doing anything with it, like `int b = a;`, is EB, not UB.
  • But a pointer after free is still an indeterminate value, so reading it is still UB.
  • ---
  • **ORIGINAL ANSWER:**
  • I am not a C expert, but I believe the answer is that it definitely *used to be* undefined behaviour (UB)… but may now only be *implementation-defined* behaviour (IB), or something similar. To be clear, I am not *sure* that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.
  • *However*… I believe what you are actually doing in that code might be unequivocally elevating it to UB.
  • In case you aren’t aware, the difference is:
  • * IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) *incorrect* (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean *the whole program* is invalid.
  • * UB, undefined behaviour, means that “anything can happen”… *literally* anything… and no-one is obliged to even attempt to tell you what that might include, because *any* instance of UB means *the whole program* is invalid.
  • I think your reasoning about debugging is correct. *De-referencing* a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.
  • The *technical* wording of it boils down to that the value in a pointer becomes *indeterminate*… not necessarily *invalid*… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.
  • But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) *used to be* UB, but I *think* that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.
  • Again there’s the *however*…
  • You have passed a pointer with an indeterminate value to `printf()`. Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value *may* be invalid, and I see no exemption for indeterminate pointer values in `printf()`, so… kaboom goes the program. Maybe.
  • So in summary:
  • * A pointer value becomes *indeterminate* when it no longer points to a valid object, such as after de-allocation.
  • * Reading or copying an indeterminate value (of any kind, not just pointers) *was* definitely UB at some point, but that *may* have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
  • * A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
  • * Passing an invalid value to C library functions is UB… but passing an *indeterminate* value…?
  • Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.
  • But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: **Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.**
  • **EDIT 2:** So I’ve been asked to provide sources for my answer. Fair enough. To recap, my answer is:
  • 1. A pointer to an object becomes indeterminate when that object’s lifetime ends.
  • 2. Accessing that indeterminate value is UB.
  • My source is the current ISO C standard, ISO/IEC 9899:2023, 6.2.4.p2. I’ve inserted bold highlighting, and my notes in parentheses:
  • > (...) If an object is referred to outside of its lifetime, the behavior is undefined. **If a pointer value is used in an evaluation after the object the pointer points to (or just past) reaches the end of its lifetime, the behavior is undefined.** (← that’s point 2) **The representation of a pointer object becomes indeterminate when the object the pointer points to (or just past) reaches the end of its lifetime.** (← that’s point 1)
  • There is also a second section later on that makes the same point in a more complex and roundabout way, in 6.5.3.6.p19-20. It shows a block of code with a fake loop construct using `goto`, where the pointer is set to an object that exists within the “loop”, then used in a comparison later. It says (in p19) that this is well-defined, because the object is still alive when the comparison happens. Then p20 says (again, the bold is from me):
  • > If an iteration statement were used instead of an explicit `goto` and a label, the lifetime of the unnamed object would be the body of the loop only, and on entry next time around **`p` would have indeterminate representation, which would result in undefined behavior**.
  • I don’t claim to be a language lawyer, but I see no other interpretation to that, other than the answer to the topic question is unequivocally “yes”. Not “maybe”. Not “on some platforms”. Just “yes, it’s UB”. Always.
  • I have also been asked for a pre-C++26 source. Again, fair enough; C++26 is *technically* not the *current* standard (yet!).
  • But I don’t have the C++23 standard, so here is something from the C++17 standard, which is from about a decade ago. This is from `[dcl.init]`, paragraph 12:
  • > If no initializer is specified for an object, the object is default-initialized. When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced (8.18). \[ Note: Objects with static or thread storage duration are zero-initialized, see 6.6.2. — end note ] **If an indeterminate value is produced by an evaluation, the behavior is undefined** except in the following cases:
  • (The “following cases” are exemptions for the “raw storage” types—`unsigned char` and `std::byte`—that are too complex to go into here, and don’t really matter.)
  • There may have been some confusion because in a section dedicated specifically to “`NullablePointer` requirements” (`[nullablepointer.requirements]`) it only says it **MAY** cause undefined behaviour:
  • > paragraph 1: A `NullablePointer` type is a pointer-like type that supports null values. A type `P` meets the requirements of `NullablePointer` if: (...)
  • >
  • > paragraph 2: A value-initialized object of type `P` produces the null value of the type. The null value shall be equivalent only to itself. A default-initialized object of type `P` may have an indeterminate value. \[ *Note:* Operations involving indeterminate values may cause undefined behavior. *— end note* ]
  • But `NullablePointer` is not *specifically* about raw pointers; it is a generic requirement. `std::unique_ptr` is a `NullablePointer`, and it does *not* get default-initialized to an indeterminate value, and thus does not cause undefined behaviour. A smart pointer type *could* conceivably get default-initialized to an indeterminate value, and thus (potentially) cause UB… but I don’t think any standard library ones do.
  • Also, it does only say that operations on indeterminate values *may* cause undefined behaviour. The reason why is simply because *not all operations on indeterminate values cause undefined behaviour*. Specifically, as stated in `[dcl.init]p12`, *replacing* an indeterminate value is fine (the section “8.18” mentioned in parentheses is the section on assignment operations). However, again, as `[dcl.init]p12` very clearly states, any operation that *produces* an indeterminate value… such as reading a variable that has indeterminate value… *is* UB. So that “may” is *not* saying that reading indeterminate values is sometimes not UB… it is saying what it literally says: that some operations on indeterminate values are not UB: Overwriting an indeterminate value is not UB; any operation that produces an indeterminate value (that is: any load) *is* UB.
  • ---
  • **EDIT:** Talked with a C++ expert, and I was misunderstanding EB.
  • **tl;dr** Reading a pointer value after free is definitely UB.
  • **RATIONALE:**
  • 1. After free, a pointer value becomes *indeterminate*.
  • 2. Any read of an indeterminate value is undefined behaviour.
  • My misconception about erroneous behaviour was that some reads of indeterminate values became EB rather than UB. That was wrong. What C++26 did was create a *new* type of value—an *erroneous value*—and said that *some* formerly indeterminate values were now erroneous values. Reading an erroneous value is EB… but reading any indeterminate value remains UB.
  • For example:
  • * Before C++26, `int a;` leaves `a` with an indeterminate value. Doing anything with an indeterminate value, like `int b = a;`, is UB.
  • * C++26 made the value of `a` in `int a;` an *erroneous* value instead. So now doing anything with it, like `int b = a;`, is EB, not UB.
  • But a pointer after free is still an indeterminate value, so reading it is still UB.
  • ---
  • **ORIGINAL ANSWER:**
  • I am not a C expert, but I believe the answer is that it definitely *used to be* undefined behaviour (UB)… but may now only be *implementation-defined* behaviour (IB), or something similar. To be clear, I am not *sure* that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.
  • *However*… I believe what you are actually doing in that code might be unequivocally elevating it to UB.
  • In case you aren’t aware, the difference is:
  • * IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) *incorrect* (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean *the whole program* is invalid.
  • * UB, undefined behaviour, means that “anything can happen”… *literally* anything… and no-one is obliged to even attempt to tell you what that might include, because *any* instance of UB means *the whole program* is invalid.
  • I think your reasoning about debugging is correct. *De-referencing* a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.
  • The *technical* wording of it boils down to that the value in a pointer becomes *indeterminate*… not necessarily *invalid*… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.
  • But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) *used to be* UB, but I *think* that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.
  • Again there’s the *however*…
  • You have passed a pointer with an indeterminate value to `printf()`. Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value *may* be invalid, and I see no exemption for indeterminate pointer values in `printf()`, so… kaboom goes the program. Maybe.
  • So in summary:
  • * A pointer value becomes *indeterminate* when it no longer points to a valid object, such as after de-allocation.
  • * Reading or copying an indeterminate value (of any kind, not just pointers) *was* definitely UB at some point, but that *may* have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
  • * A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
  • * Passing an invalid value to C library functions is UB… but passing an *indeterminate* value…?
  • Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.
  • But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: **Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.**
#2: Post edited by user avatar Indi‭ · 2026-08-23T13:54:45Z (25 days ago)
  • I am not a C expert, but I believe the answer is that it definitely *used to be* undefined behaviour (UB)… but may now only be *implementation-defined* behaviour (IB), or something similar. To be clear, I am not *sure* that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.
  • *However*… I believe what you are actually doing in that code might be unequivocally elevating it to UB.
  • In case you aren’t aware, the difference is:
  • * IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) *incorrect* (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean *the whole program* is invalid.
  • * UB, undefined behaviour, means that “anything can happen”… *literally* anything… and no-one is obliged to even attempt to tell you what that might include, because *any* instance of UB means *the whole program* is invalid.
  • I think your reasoning about debugging is correct. *De-referencing* a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.
  • The *technical* wording of it boils down to that the value in a pointer becomes *indeterminate*… not necessarily *invalid*… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.
  • But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) *used to be* UB, but I *think* that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.
  • Again there’s the *however*…
  • You have passed a pointer with an indeterminate value to `printf()`. Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value *may* be invalid, and I see no exemption for indeterminate pointer values in `printf()`, so… kaboom goes the program. Maybe.
  • So in summary:
  • * A pointer value becomes *indeterminate* when it no longer points to a valid object, such as after de-allocation.
  • * Reading or copying an indeterminate value (of any kind, not just pointers) *was* definitely UB at some point, but that *may* have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
  • * A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
  • * Passing an invalid value to C library functions is UB… but passing an *indeterminate* value…?
  • Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.
  • But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: **Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.**
  • **EDIT:** Talked with a C++ expert, and I was misunderstanding EB.
  • **tl;dr** Reading a pointer value after free is definitely UB.
  • **RATIONALE:**
  • 1. After free, a pointer value becomes *indeterminate*.
  • 2. Any read of an indeterminate value is undefined behaviour.
  • My misconception about erroneous behaviour was that some reads of indeterminate values became EB rather than UB. That was wrong. What C++26 did was create a *new* type of value—an *erroneous value*—and said that *some* formerly indeterminate values were now erroneous values. Reading an erroneous value is EB… but reading any indeterminate value remains UB.
  • For example:
  • * Before C++26, `int a;` leaves `a` with an indeterminate value. Doing anything with an indeterminate value, like `int b = a;`, is UB.
  • * C++26 made the value of `a` in `int a;` an *erroneous* value instead. So now doing anything with it, like `int b = a;`, is EB, not UB.
  • But a pointer after free is still an indeterminate value, so reading it is still UB.
  • ---
  • **ORIGINAL ANSWER:**
  • I am not a C expert, but I believe the answer is that it definitely *used to be* undefined behaviour (UB)… but may now only be *implementation-defined* behaviour (IB), or something similar. To be clear, I am not *sure* that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.
  • *However*… I believe what you are actually doing in that code might be unequivocally elevating it to UB.
  • In case you aren’t aware, the difference is:
  • * IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) *incorrect* (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean *the whole program* is invalid.
  • * UB, undefined behaviour, means that “anything can happen”… *literally* anything… and no-one is obliged to even attempt to tell you what that might include, because *any* instance of UB means *the whole program* is invalid.
  • I think your reasoning about debugging is correct. *De-referencing* a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.
  • The *technical* wording of it boils down to that the value in a pointer becomes *indeterminate*… not necessarily *invalid*… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.
  • But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) *used to be* UB, but I *think* that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.
  • Again there’s the *however*…
  • You have passed a pointer with an indeterminate value to `printf()`. Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value *may* be invalid, and I see no exemption for indeterminate pointer values in `printf()`, so… kaboom goes the program. Maybe.
  • So in summary:
  • * A pointer value becomes *indeterminate* when it no longer points to a valid object, such as after de-allocation.
  • * Reading or copying an indeterminate value (of any kind, not just pointers) *was* definitely UB at some point, but that *may* have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
  • * A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
  • * Passing an invalid value to C library functions is UB… but passing an *indeterminate* value…?
  • Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.
  • But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: **Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.**
#1: Initial revision by user avatar Indi‭ · 2026-08-23T00:07:01Z (26 days ago)
I am not a C expert, but I believe the answer is that it definitely *used to be* undefined behaviour (UB)… but may now only be *implementation-defined* behaviour (IB), or something similar. To be clear, I am not *sure* that is the case, and I couldn’t tell you in which standard things changed (if any). I am extrapolating from what I know about C++, so take it all with a grain of salt.

*However*… I believe what you are actually doing in that code might be unequivocally elevating it to UB.

In case you aren’t aware, the difference is:

*   IB, implementation-defined behaviour, means that “anything can happen” (including simply crashing), but the implementation is obliged to tell you exactly what, and the program isn’t (necessarily) *incorrect* (unless the implementation definition tells you so). Importantly, IB doesn’t necessarily mean *the whole program* is invalid.
*   UB, undefined behaviour, means that “anything can happen”… *literally* anything… and no-one is obliged to even attempt to tell you what that might include, because *any* instance of UB means *the whole program* is invalid.

I think your reasoning about debugging is correct. *De-referencing* a pointer that doesn’t point to a live object is straight-up UB, obviously, but if merely having, copying, and reading the value of such a pointer were UB, it would make debugging more difficult and dangerous. It still has to be IB (or something similar), though, to allow for trapping and other such unpleasant (to your program) stuff.

The *technical* wording of it boils down to that the value in a pointer becomes *indeterminate*… not necessarily *invalid*… when it no longer points to a valid object. It’s invalid for de-referencing, but merely indeterminate for anything else.

But this is the part where my knowledge of the C standard gets fuzzy: merely reading or copying an indeterminate value (whether a pointer or anything else) *used to be* UB, but I *think* that may have been relaxed recently. I know that is the case in C++, at least, where they have recently created a new class of “erroneous behaviour” to account for this kind of stuff. But I don’t know if/what the C committee has done about it.

Again there’s the *however*…

You have passed a pointer with an indeterminate value to `printf()`. Passing things with invalid values to any library function is unequivocally UB. An indeterminate pointer value *may* be invalid, and I see no exemption for indeterminate pointer values in `printf()`, so… kaboom goes the program. Maybe.

So in summary:

*   A pointer value becomes *indeterminate* when it no longer points to a valid object, such as after de-allocation.
*   Reading or copying an indeterminate value (of any kind, not just pointers) *was* definitely UB at some point, but that *may* have been relaxed in recent years to IB (or whatever the C standard has that is analogous to C++’s EB).
*   A pointer that no longer points to a valid object is invalid for de-referencing; doing that is definitely UB.
*   Passing an invalid value to C library functions is UB… but passing an *indeterminate* value…?

Again, I am basing this on C++, which has recently been trying to remove a lot of UB (because it is so terrifyingly dangerous) by demoting things to “erroneous behaviour” (EB) where possible. Reading indeterminate values has been demoted to EB in C++. Still bad, but not nuclear-nasal-demon bad. Usually the C and C++ committees work together on stuff that applies to both languages, and this seems like it would qualify. If someone more familiar with the C standard, and especially recent history, could chime in, that would be great.

But all that is the really technical, language-lawyer-level stuff. Real programmers in the real world (usually) don’t care whether something is UB or EB… they just want to know if it’s wrong to do in a program intended for portable, real-world use. And the answer to that is easy and clear: **Reading the address returned by a heap allocation function after its de-allocation is wrong. Don’t do it.**