Reputation: 5807
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
Upvotes: 5
Views: 1305
Reputation: 11546
Starting with the contrivance described in the question, an already allocated int* p
and:
int* q = p;
q = new int(42); // assume it returns the same memory
We now have a scenario that's identical to this:
int* p = new int(42);
int* q = p;
Because it meets the preconditions that we can assume p
and q
point to the same memory location; that being the case the fact that this location was allocated something then deleted then allocated again doesn't matter. Nothing that happened before this point matters because we are assuming the two pointers are in a state identical to the one just described.
With regard to "the value of both is unspecified" in step #2, I would say the value of q
is unspecified at that point because it was passed to delete
, but the value of p
is unchanged.
The behavior of delete
here is actually not undefined under C++14, it's implementation defined; a bit from some documentation of delete
:
Any use of a pointer that became invalid in this manner, even copying the pointer value into another variable, is undefined behavior. (until C++14)
Indirection through a pointer that became invalid in this manner and passing it to a deallocation function (double-delete) is undefined behavior. Any other use is implementation-defined. (since C++14)
https://en.cppreference.com/w/cpp/memory/new/operator_delete
So, to answer what I think is your question, in that circumstance, then no, neither on is a zombie pointer.
So why is this a zombie pointer and supposedly UB?
It's not, so what ever has lead you to that conclusion is a misunderstanding or misinformation.
Upvotes: 1
Reputation: 141618
A deleted pointer value has an invalid pointer value, not an unspecified value.
As of C++17 the behaviour of invalid pointer values is defined in [basic.stc]/4:
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.
So, trying to compare two invalid pointers has implementation-defined behaviour. There is a footnote clarifying that this behaviour could include a runtime fault.
In your first snippet p
has an invalid pointer value after the delete; anything you might do to q
has no bearing on this. Invalid pointer values cannot magically become valid again. There is no way (within the confines of Standard C++) to determine whether a new allocation is at the "same location" as a previous allocation.
std::launder
is no help; again you use an invalid pointer value and therefore trigger implementaiton-defined behaviour.
Perhaps you could consult the documentation for your implementation and see what it defines this behaviour as.
In your question you mention C DR 260, however that is irrelevant. C is a different language to C++. In C++, deleted pointers have invalid pointer value, not indeterminate value.
Upvotes: 4
Reputation: 6805
I would disagree with this:
// 3) Both pointers are valid again and can be dereferenced
In fact, you got fooled because at the second new
, the program usually reallocate the same memory block at it just became available, but one cannot rely on this (it is not guaranteed that the same memory block will be reused).
For example, if you use the good practice at setting deleted pointers to nullptr
, the following program:
int main()
{
int * p = new int(1);
int * q = p;
std::cout << (p == q) << std::endl;
delete q;
q = nullptr;
p = nullptr;
q = new int(42);
std::cout << (p == q) << std::endl;
delete q;
return 0;
}
would result in:
1
0
Upvotes: 1