C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Delete...why not a parameter by reference?!

From: Oliver Hunt <oliver_at_[hidden]>
Date: Sun, 24 Aug 2025 22:01:57 -0700
> On Aug 24, 2025, at 7:13 PM, organicoman <organicoman_at_[hidden]> wrote:
>
> Hi,
> I'm really surprised about how it is difficult to some people to understand this proposal.
> This will be my last take.
> ------------
> Intro:
> C++ as language guarantee backward compatibility and support for very old code. This has been said, it means that there is so many lines of code, out there, still using raw pointers. For those code bases, I want to propose a way to catch bugs with very minimal change.
>
> Agreement:
> 1- I'm not trying to downgrade the usage of smart pointers compain, so if there is any mean to switch to smart pointers, then go for it.
> 2- I'm not talking about containers, or how to store references.
> Take the proposal as is, no extrapolation.
>
> The problem:
> The problem is illustrated with the following code snippet.
>
> 1. {
> 2. T* ptr = new T{....}; // T well defined type
> 3. Foo(ptr); // opaque callee
> 4. // this line: see discussion below
> 5. }
>
> Explanation:
> On line 2, I allocate a memory resource to store a variable of type T. T is a type defined in previous code.
> On line 3, I call a function 'Foo', which has the following signature:
> void Foo(T*);
>
> This function comes from a closed source code, so there is no way that I can analyze its code.
> But, when I read its documentation, I find the following scenario (each scenario will be discussed separately)
>
> 1- case: the documentation promises that the consumed pointer will not be deleted (not owning function)
> ******* but the function has a bug*******
>
> As a user of that function, I take that doc as true, and in line 4 above, I can follow up with this code.
>
> 4. delete ptr; // (A)
> Or
> 4. *ptr = T{...}; // (B)
>
> The buggy function in actuality, does break its contract by mistake and free the resource owned by 'ptr'

That is one way that happens, but more commonly the function you call reenters your code, which then invalidates the pointer. Again, straightline errors are easy to diagnose, but...


> Because of that, I can fall in 2 bugs, extremely difficult to catch.
> (A) double free
> (B) use after free
>

<snip>

>
> The proposal:
> To help detect this kind of mistakes, I propose the following correction to the delete expression signature
>
> Previous signature:
> void delete(void*);
> Become:
> void delete (void* &);

… if we make this absolutely trivial, your proposal changes literally nothing in your own example, so first we’ll define foo:

void foo(T* ptr) {
  delete ptr;
}

Hypothetically, the delete expression writes null to ptr - again there is literally no reason to touch the definition or resolution rules for operator delete, so the code is semantically identical to what today would be written as:

void foo(T *ptr) {
  delete ptr;
  ptr = nullptr;
}

Now we go to your example code

T* ptr = new T
foo(ptr)
/* ptr is now dangling */

Your two next steps are still broken:

delete ptr

Is still a double free

*ptr

Is still a use after free

>
> Also to help this change takes a full effect on catching the 3 categories of bugs above, I like to add the following guide line with the proposal:
>
> It would be optional, but extremely useful, to make any callee take the argument per a reference to pointer, so if your callee signature is as follow:
> RetType fnName(T*);
> Change it to:
> RetType fnName(T* &);

This means developers have to make wide ranging changes to there code, that can silently fail if they don’t make the changes at the right places, even if they do go through and change every function definition, it results in broken code, e.g.

struct Foo {
  int* buffer;
  size_t length;
};
unsigned find(std::function<bool(int)> predicate, int *haystack, size_t len) {
  unsigned index = 0;
  while (index<len && !predicate(*i++)) ++index;
  return index;
}

Unsigned foundIdx = find(somePredicate, myFoo.buffer, myFoo.length);

Now the author adopts your proposed stylistic change:

unsigned find(std::function<bool(int)> predicate, int *&haystack, size_t len) {
  unsigned index = 0;
  while (index<len && !predicate(*i++)) ++index;
  return index;
}

Now

Unsigned foundIdx = find(somePredicate, myFoo.buffer, myFoo.length);

Produces an OoB access bug.

It also breaks for code that does have abstraction:

struct Foo {
  int* data() { return buffer; }
  size_t length() { return _length; }
private:
  int* buffer;
  size_t _length;
};

size_t foundIdx = find(somePredicate, myFoo.data(), myFoo.length()); // Compile fails

Similarly

unsigned foundIdx = find(somePredicate, myFoo.buffer + start, myFoo.length); // Compile fails

At the same time while we use the proposed version:

unsigned find(int needle, int *&haystack, size_t len) {

You have not done anything to ensure the lifetime of the original pointer - let’s remove the syntactic sugar for a moment, you’ve changed us from
unsigned find(int needle, int *haystack, size_t len)
To`
unsigned find(int needle, int **haystack, size_t len)

So now we have two pointers that need to be kept alive, and need their lifetimes tracked. Let’s imagine that the predicate is non-trivial: hypothetically there’s a std::vector<Foo> and the predicate modifies that. Your proposal has done nothing to stop or detect that, but the impact is that now in addition to the developer needing to be sure the original `int*` is not destroyed, but neither is the `int**`.

This is what I have been trying to point out: your proposal rests entirely upon the assumption that adding indirection does not immediately result in increasing the number of object lifetime errors that can be triggered through a single evaluation path. The only place it makes an improvement, if at all, is if you have a single allocation point, a single deallocation point, and no logic invoking further dynamic allocation or deallocation within that stretch.

This part of the proposal also does not provide an explanation of how subobject pointers work either.

Finally I believe this proposal will open the potential to break deletion of cyclic/self referential structures that would previously be safe, consider:

struct Type1 {
  Type2 *other;
};

Type1 *object1 = …;
delete object2->other;

If destruction of `other` causes `object1` to be destroyed (this is entirely plausible in refcounted object graphs scenarios), the post deletion assignment of nullptr to `object2->other` is a now a use after free.

So I’m not sure that this change is sound even ignoring the API changes it requires to achieve its stated safety properties.


Now as we get into this last part: It is not possible to talk about this proposal without including the requirement for widespread API changes to move from pointers to unprotected indirect pointers, as the proposal immediately deflects to this once anything trivially non-local is involved.

> Proposal cost:
> Changing the delete expression signature, won't affect the calling code.

We can drop this, it is absolutely unnecessary for the ABI or API of operator delete or operator delete[] to change at all. There is literally no reason to do this, it makes the proposal vastly more complex, and simultaneously limits how widely useable it is. The behavior you are trying to get via this API change is trivially met by saying “if the subject of a delete expression is an lvalue, after the call to the resolved operator delete, it shall be set to nullptr” (or something to this effect). This immediately means you are not blocked on every allocator implementing support for a new abi, and you don’t run into problems with defining resolution priority, and similar.

> So there is no source code change, no grep and modify or any type of code breakage.

No. Your proposal has made claims of security benefits, but those benefits require significant adoption work, if your proposal wishes to include those benefits, then the proposal costs need to include those adoption costs.

The costs for this proposal includes the work by developers and libraries to update all of their interfaces to take pointers by reference, which is both a large undertaking, an ABI break, and an API break. The errors from this proposal vary from silently missing a reference at one location thus losing the “benefit” of your proposal, through to breaking existing code as they are no longer changing local copies of pointers.

The costs also include the performance penalties of these changes (indirection, aliasing, etc).

> Following the guidline suggested above, will affect only the header files, included by the consuming source code.

No, you’ve changed the requirements for functions to take references to pointers, so the headers and implementations need to be updated.

> ABI wise, hopefully there is a way, at compilers implementers level, to not break the ABI, this is based on the assumption that, previously the delete expression has undefined behaviour when passed a nullptr, but nowadays it handles the nullptr correctly, this change was done without breaking any ABI.
> If done once, I hope that It can be done twice.

No? You cannot change that API and expected that to not be an ABI break.

I feel there is a fundamental misunderstanding about how delete works.

It is not magic - the compiler looks at the type being deleted, resolves the appropriate operator delete for the type and context, and then calls it. You want to change the function being called. If you do that the ABI has changed. Every existing allocator library needs to be updated to support it.

You also need to define the resolution precedence and prioritisation.

ABI and API wise the security properties your are claiming are dependent on the changes to pointer reference based APIs. That’s both a significant amount of work for the implementers of libraries, but also application developers. At the same time these changes are enormously error prone, and have significant potentially to negatively impact performance, both through direct costs from additional indirection, but also the introduction of such indirection means significantly increased aliasing hazards impacting the ability for compilers to reason about local code (because definitionally this changes makes previously local state changes impact external state) - this is by design, as the changes required to permit

T* ptr = …;
foo(ptr);
// ptr is now null

Also permit
// ptr is not a different object

And the abstract machine does not provide any mechanism for that to not be the case.


> Conclusion:
> The proposal doesn't bash actual or future replacement for using raw pointers, but to reach the top floor of security, we need to climbe the stairs, one stair a time.

This is not a conclusion, it’s a vacuous statement that says nothing at all about your proposal, and I’m choosing to believe is not intentionally patronizing. No one in the committee believes there is a single step to perfect safety, that’s why there are multiple proposals aiming to move forward on multiple axes.

From what you have written here

* This proposal tries to make a much larger change to the language than is needed for the desired behavioral change;
* The security benefits of just that change alone are dubious at best;
* The proposal's broader security claims require significant source changes, breaking API and ABI. These source changes are also error prone, introducing new lifetime, aliasing, and correctness hazards, and the costs of this are not discussed;
* Existing language features already provide the same or better guarantees and semantics, with similar source change overhead for adoption, but without the additional hazards from this proposal.

It is perhaps better to ignore the behavior changes to operator delete, and consider the intended semantics of the T* -> T**/T*& change recommended as being the actual desired semantics of this proposal. The idea there is to have pointer references that are all nullified when the target object is deallocated, ie. A weak reference. For such desired behavior std::weak_ptr already exists as well.

If you really want to go forward with this, I would recommend you read prior papers, especially those discussing changes to the semantics of new and delete, and writing a formal paper that covers the same material and issues, and specify *exactly* what changes you are proposing - language, library, code, etc, and what security properties come from that change, and accurately describe the costs and hazards, which you certainly did not do here. i.e if you do not want to discuss the difficulties and costs of migrating code from pointers to pointer references, you can’t include the results of those changes in your potential benefits. It’s also worth considering if those are even relevant: there is nothing in those changes the depends on your proposed change to delete.

I am not asking for another summary document as you produced here, I am asking for a detailed and researched paper, that may not be as complete as a full committee paper, but certainly a document that is clearly moving in that direction.

—Oliver




Received on 2025-08-25 05:02:12