There is a lot of email traffic in the community and that's good to see.
I just saw your reply, buried inside.
Thanks again for your time and efforts, they are much appreciated.
It looks like we are both trying so hard to repeat the same things we are saying over and over.
I would like to reboot this thread, but this time we will use a Q&A approach, likewise, I can lay down my thought process, and we can pin point any wrong assumptions.
If the error is correct-able, then I will adjust and carry on, otherwise the proposal is void since one of it's pilar is weak.
On Aug 30, 2025, at 3:08 PM, organicoman via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
> On Aug 27, 2025, at 9:41 PM, organicoman via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
>
> In the example above, we have a double free bug despite the correct code.
>
It is not correct code. With or without the ‘throw’ you’ll have a double free. The destructor of A always gets called at some point. You are violating basic class invariance principles of RAII. And again: a unique_ptr would have solved this problem without any overhead.
Nope, std::unique_ptr won't solve this problem.
No: any case where you have pulled the pointer out of a unique_ptr represents a control flow path leads to raw pointer usage, in which case your proposal is also not going to do anything.
More over as I have stated repeatedly, you cannot just blindly carry a reference around. A reference is literally just a pointer, requiring passing a reference around, means requiring that the storage for that reference is live, so now you just have another pointer that can result in a use after free. Following from that, any case where you do end up - out of necessity - with a non-glvalue of the pointer, you’ve made all your functions require a reference, so you now have to store your pointer somewhere, and pass a reference to that copy.
At that point you now have all of the downsides and problems of this proposal *and* you are not getting any benefit because that reference is not a reference to the storage you plan on passing to delete. On top of that, people may believe that because they’re passing these values by reference they’re protected by this feature, despite that not being that case now, as the reference that they have is not to a pointer that will be cleared.
You must know that the delete expression can throw if the destructor of the object throws, thus, you will be UB.
Yes, and that is true for your proposal as well.
Plus, before RAII we should observe the "easy to use hard to misuse " principle for designing API's....delete as many other memory related functions like , std::free, std::deallocate, or kfree (kernell free)..are easy to misuse, unsafe to use(otherwise we won't have the famous memory safety issues)....if inside the call the function throws there's no way to reason about the resource (freed no freed?).
You are literally saying: RAII - a technique that manages lifetime automatically - goes wrong, if you try to simultaneously manage that memory manually. Yes, if you write code that is that wrong, you will have real bugs, but then you are turning around and saying your proposal works, because a developer who manually deletes a pointer managed by an automatic memory management tool, can be assumed to do the correct thing with your proposal, which requires extensive, subtle, and trivially failing code to be written instead.
In other words: a developer who writes code that is extremely and blatantly incorrect, when using a mechanism that automatically manages lifetime for them, is assumed to correctly do something that is significantly harder to do correctly, and significantly easier to do incorrectly.
That's why , taking the pointer by reference and null it asap, is the best strategy for safety.
No, as has been said multiple times, it makes the code less safe.
>
> Anyway, I'm past this proposal now. If it is difficult to be understood, it will be difficult to be advocated for, thus not worth the effort.
Actually, your proposal is quite easy to understand: You want to automatically null a pointer when it gets deleted. It is just impossible to implement without breaking any (reasonable) existing C++ code (unless recompiled).
I'm not talking about the implementation (of course it is easy to understand) , I'm talking about the benefits.
If all the functions above take the pointer by reference and nulls it out directly after freeing the resource, we will have the chance to:
1- inspect it, either in the natural return from that function or when we return because of an exception.
But the inspection does not provide you with meaningful information - you certainly cannot say “if it is not zero it has not been freed”, because if the object has been deleted you cannot read the pointer.
2- get a safe behavior, by default, if we double call the function on the same pointer
No we don’t: as has been said repeatedly this does not (and cannot) make all use of the pointer be through a single storage location, it _creates_ new opportunities for use after free errors, it silently changes the semantics of existing code to introduce memory errors.
3- changing the provenance of the pointer by reassigning it, doesn't cause a memory leak.
4- we can optimize some pointer comparisons like for example:
{
T* p = new T;
Foo(p); // takes by reference and may free
Or it is freed through any of the myriad ways in which have been explained previously without updating this reference
T* q = new T;
if( p == q) // this can be optimized out
{ }
}
In the snippet above, q == p is always false, even if Foo calls delete on p then its old value gets recycled then assigned into q in the next allocation.
Given (q == p) is always false, then the if statement can be optimized out.
Correct, this check does nothing at all. But that has nothing to do with your feature, and it again demonstrates a failure to understand core concepts of the language.
Creates a new object, there for *by definition* that pointer cannot have the same value as any other pointer, so by the language specification this check is always false - the actual values, or the preceding control flow does not matter. `q` points to an object with a lifetime that has just begun. Nothing could have modified the value of `p` prior to
Therefore it does not matter what value might actually have at runtime, it does not matter whether you have or have not written null to it, it does not matter what your allocator does. By definition, any value that `p` has is from before the creation of the object that `q` is pointing to, and so it is definitionally impossible for it to have the same value. The fact that in an actual implementation it _could_ have the same value does not matter: the only way that could happen in a conforming implementation is if the lifetime of the object p points to has ended, in which case examine the value of the pointer at all is already an error - I believe even a null check would be UB (*if* the object was deleted).
As with previous replies on this thread, you are misunderstand core language behavior. I’ll try to explain with an explicit example:
struct Foo { /*...*/ };
void noDelete(Foo *&f) {
}
void nulledAfterDelete(Foo *&f) {
delete f;
f = nullptr; // modelling your proposal
}
void notNulledAfterDelete(Foo *&f) {
delete f;
}
void test(void (*doSomething)(Foo*&)) {
Foo *f1 = new Foo;
doSomething(f1);
Foo *f2 = new Foo;
if (f1 == f2) { // The condition.
}
};
Let’s now consider what is the case when we call each of these functions:
* Calling noDelete => the object has not been deleted, so it still points to the original object, and therefore cannot have the same value as f2
* Calling nulledAfterDelete => the object has been deleted, and we set the value to null, the comparison always fails because `new` has created a new object, and that definitionally cannot be a nullptr
* Calling notNulledAfterDelete => the object has been deleted, so the f1 is no longer pointing to an object, we just allocated a new object and f2 is pointing to it - f1 is definitionally not pointing to an object so they cannot be the same.
I.e. f1 and f2 are by definition never able to be the same. Moreover in the case of notNulledAfterDelete, we are comparing to an invalid pointer (it is a non-null pointer that is not pointing to an object), which makes the comparison UB, so if the compiler sees that path it would also be permitted to assume that the comparison is always true.
Writing null to the pointer does not open up this option, it already exists — as a trivial example:
https://godbolt.org/z/16Meacds3 the compiled code does not even contain the string literal for that branch. The “-fno-exceptions” bit is just to simplify the code, it does not change the optimization.
5- by setting the pointer to nullptr, it is always a good indicator for a possible change in pointer provenance, which can be used for pointer zap, or pointer provenance optimization techniques.
It absolutely is not. Again, this has been explained many times by multiple people in this thread. After the object is deleted any comparison to that pointer is UB if it is not set to the nullptr, which means any comparison can be assumed to be false if the compiler can show that it is not possible for the pointer to have been set to the value of the pointer you’re comparing to.
I’m just going to be blunt sorry.
This proposal is bad.
You have ignored the repeated explanations of why this is the case, and you do not seem to have understood any of the explanations of why this does not actually solve any of the hard cases, the language details that have been explained as part of explaining why it does not work, and you also seem uninterested in trying to understand these reasons.
There seems to be a very fundamental failure to understand both the bug class, the attack vector, the abstract machine memory model, and the language semantics, and without understanding these things, you are not understanding why what you are proposing does not work, except in very specific niche cases, and in many it actually makes things worse and introduces additional memory errors.
A bullet pointed summary, this proposal
* Tries to resolve UaF errors, but having the delete and presumably delete[], by set the object parameter in the expression set to nullptr if the expression is an lvalue. It fails if the expression is not.
* The practical benefit of the proposal is to make specific use after free paths less exploitable, these paths are already either trivially detectable, if not already, by the compiler itself. Beyond the compiler, standard static analysis tools can already statically detect all realistic cases this feature can prevent, and do so statically ahead of time. These tools can also detect many cases this proposal does not address.
At the same time:
* It simply does not solve the problem it is presented as solving - multiple people has tried to explain why, and you do not appear to have been interested in understanding the explanations.
* It _adds_ rather than removes opportunities for use-after-free bugs.
* It significantly impacts performance: directly through the mandatory indirection, the additional pointers stores required to maintain that indirection, and most expensively: removing the ability to reason about the value of a pointer: the value of your pointers may alias, any opaque operation forces at least a reload, etc.
* The “improvement” it provides mandates global use of pointer references, as the moment a pointer is ever copied by values any potential safety the proposal disappears completely
* It is extremely error prone: it is trivial to accidentally decay to a value, meaning again any potential benefit is lost
* Adoption is extremely error prone: there’s a lot of existing code that passes T*&, but now that no longer means the same thing - instead of referencing say a local work value, you’re referencing the original storage. Knowing the correct behavior at any given point will require analysis of the caller and the callee. If those semantics are wrong, the caller needs to store the value locally, and provide a reference to that storage, and again you have lost and potential advantage.