After watching CppCons Will Your Code Survive the Attack of the Zombie Pointers? I'm a bit confused about pointer lifetime and need some clarification.
First some basic understanding. Please correct me if any comments are wrong:
int* p = new int(1);
int* q = p;
// 1) p and q are valid and one can do *p, *q
delete q;
// 2) both are invalid and cannot be dereferenced. Also the value of both is unspecified
q = new int(42); // assume it returns the same memory
// 3) Both pointers are valid again and can be dereferenced
I'm puzzled by 2). Obviously they cannot be dereferenced, but why can't their values be used (e.g. to compare one against another, even unrelated and valid pointer?) This is stated in around 25:38. I can't find anything about this on cppreference, which is where I got 3) from.
Note: The assumption, that the same memory is returned, cannot be generalized as it may or may not happen. For this example it should be taken as granted that it "randomly" returned the same memory as is the case in the video example and (maybe?) required for the below code to break.
The multi-threaded example code from the LIFO list can be put simulated in a single thread as:
Node* top = ... //NodeA allocated before;
Node* newnodeC = new Node(v);
newnodeC->next = top;
delete top; top = nullptr;
// newnodeC->next is a zombie pointer
Node* newnodeD = new Node(u); // assume same memory as NodeA is returned
top = newnodeD;
if(top == newnodeC->next) // true
top = newnodeC;
// Now top->next is (still) a zombie pointer
This should be valid, unless Node
contains nonstatic const members or references according to the rules under
If a new object is created at the address that was occupied by another object, then all pointers, references, and the name of the original object will automatically refer to the new object and, once the lifetime of the new object begins, can be used to manipulate the new object, but only if the following conditions are satisfied [which they are]
So why is this a zombie pointer and supposedly UB?
Could the (single-threaded) condensed code above be fixed (in case there are const-members) by a newnodeC->next = std::launder(newnodeC->next)
as of
If the conditions listed above are not met, a valid pointer to the new object may still be obtained by applying the pointer optimization barrier std::launder
I'd expect this to fix the "zombie pointer" and compilers to not emit instructions for the assignment but simply treat it as an optimization barrier (e.g. when the function is inlined and a const-member is accessed again)
So in summary: I haven't heard of "zombie pointers" before. Am I correct that any pointer to a destroyed/deleted object cannot be used (for reading [the pointers value] and dereferencing [read of the pointee]) unless the pointer is reassigned or the memory reallocated with the same object type recreated there (and without const/reference members)? Can't this be fixed by C++17 std::launder
already? (baring multi-threaded issues)
Also: At 3)
in the first code would if(p==q)
even be generally valid? Because from my understanding of the (second part of the) video it is not valid to read p
.
Edit: As an explanation where I'm pretty sure UB happens: Again assume that by pure chance the same memory is returned with the new
:
// Global
struct Node{
const int value;
};
Node* globalPtr = nullptr;
// In some func
Node* ptr = new Node{42};
globalPtr = ptr;
const int value = ptr->value;
foo(value);
// Possibly on another thread (if multi-threaded assume proper synchronisation so that this scenario happens)
delete globalPtr;
globalPtr = new Node{1337}; // Assume same memory returned
// First thread again (and maybe on serial code too)
if(ptr == globalPtr)
foo(ptr->value);
else
foo(globalPtr->value);
According to the video, after delete globalPtr
also the ptr
is a "zombie pointer" and cannot be used (aka "would be UB"). A sufficiently optimizing compiler can make use of this and assume the pointee was never freed (especially when the delete/new happens on another functon/thread/...) and optimize foo(ptr->value)
to foo(42)
Note also the mentioned Defect Report 260:
Where a pointer value becomes indeterminate because the object pointed to has reached the end of its lifetime, all objects whose effective type is a pointer and that point to the same object acquire an indeterminate value. Thus p at point X, and p, q, and r at point Z, can all change their value.
I think this is the definitive explanation: After delete globalPtr
the value of ptr
is also indeterminate. But how can this align with
If a new object is created at the address that was occupied by another object, then all pointers [...] of the original object will automatically refer to the new object and[...] can be used to manipulate the new object
if(p==q)
Both pointers are valid again and can be dereferenced, right?" – Fluctuateif
succeeds. Can easily happen for allocators that simply look if they can serve a request with recently freed memory chunks. Onif(p==q)
: The point is: Can I do this? The object pointed to byp
was deleted, sop
is a "zombie pointer". I really recommend (at least) the first part of the video. Quite entertaining :) – Biogenesisq
dereferencable, but notp
. – Burschenschaftnew
returns the address of the newly created object, which is not guaranteed to be the same as the previous deleted one. So after your secondnew
,p
andq
cannot be considered to point at the same location in memory. See my answer for more explanation. – Pointsmanq
happens by chance to return the same virtual address that is already stored inp
. This can certainly happen. If it does, the address stored inp
will therefore become a valid virtual memory address again (will point to that newly allocated object) which holds the value 42, and so willq
. – Mckieif(std::memcmp(&p, &q, sizeof(void*)) == 0)
would be a safe version ofif(p==q)
? Still unclear ifp
is valid if that condition is true. – Fluctuate