C++ Logo

std-proposals

Advanced search

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

From: Simon Schröder <dr.simon.schroeder_at_[hidden]>
Date: Mon, 25 Aug 2025 10:03:51 +0200
Thank you for providing a more detailed description of your proposal. You’ll find a few responses at appropriate places inside your proposal.

> On Aug 25, 2025, at 4:13 AM, 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)

Your example is closed source. This means the behavior of the closed source will not change with this proposal if the closed source is not recompiled accordingly. The pointer inside the closed source cannot be nulled by retroactively changing the implementation of delete (delete might have even be inlined, but even if it is not inlined it is impossible). Also, if the closed source library does not take a reference to a pointer instead of the pointer your proposal will not change that. If you can get the maintainer of the closed source library to change the API, you can also get them to fix the bug. (I believe it is much easier to get them to fix the bug.)
>
> 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'
> Because of that, I can fall in 2 bugs, extremely difficult to catch.
> (A) double free
> (B) use after free

You should be writing against the specification of the library. It is not your own source code that has a bug, but theirs. With your proposal (A) double free goes away (because you are allowed to give a nullptr to delete), but (B) use after free is still there. A single test case can catch the bug you are describing (write a test case according to the specification and you’ll get a double free; the implementer of the library should do this and would have caught this exact bug).
>
> 2- case: the documentation of the function, guarantees that the pointer is owned and delete was called on it.
> ****** yet the function is buggy******
>
> Because of the documentation again, I can follow up with this code
>
> 4. ptr = new T{....}; // (C)
>
> The function in reality doesn't free the resource by mistake, which induces me in error, and make me fall in another category of bugs called memory leak (C)

Your proposal does nothing about the memory leak. The only (current) solution to strictly avoid memory leaks is to use owning (smart?) pointers.

(Maybe what you mean here is that with your proposal you can check if the pointer is 0 before using it again. But why should I sacrifice performance with this check?)
>
> All of the bugs above encounter in the function 'Foo' are unintentional.

For the first bug you still need to check (after your proposal has been implemented by the library) if the pointer is a nullptr after the call to the library function before you use the pointer again. And you have to decide what to do if the pointer is 0. It would be really cumbersome to write code to treat errors that are not supposed to happen (which means you can’t even write tests for your error handling as long as there is no bug inside the library function). The easiest thing would be to just throw an exception and terminate. This is not better than the double free you get right now. It is also error prone not to forget to check for the nullptr after every call to the library function (which the documentation tells you is not necessary).
>
> 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* &);

These are two distinct functions. They have to be different function names for the linker (have a look at name mangling of function names). This is done to allow function overloading and static type checking of function calls. This means that if the closed source library is not recompiled it will continue to call delete(void*) and not delete(void*&). It might even have inlined the call to delete. There is also no compiler/linker magic that could somehow change this. The current delete function only gets the address of the object the pointer points to. The calling code of the closed source library will only provide this address as a function argument without a recompile of that library. What the new proposed delete function needs is the address of the pointer instead. There is really nothing we can do for an existing closed library unless it is recompiled.

Because of these problems your proposals should contain that for the compiler only the new delete(void*&) signature should be detectable during overload resolution. But, every C++ standard library still needs to link in an implementation of delete(void*) so that old libraries which have not been recompiled (and maybe you cannot get a recompiled version of them) still work (just without the added safety guarantee).

>
> 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* &);

Again, there is no compiler magic that can do that. The closed source library needs to change that. There is no language change necessary to support what you are suggesting. The library needs to change their API to take a reference to a pointer. At the same time they can also implement to null the pointer after the delete. In the buggy version of your library function that accidentally deletes the function parameter (which is now a reference to a pointer) nulling the pointer after a delete will not be optimized away. In the version without the bug the optimization is still done. If the library is written this way your proposal is moot.

One problem I see with your proposed API change is that previously it was valid C++ to write
fnName(new T{});
This is not allowed anymore. For this to be still allowed we would need the function signature
RetType fnName(T *&const);
If this is our signature, accidentally deleting the function parameter would not compile. The same would actually be true if we just write
RetType fnName(T*const);
(I hope I am correct about this one. Somebody better double-check me on that.)
So, we should educate people to add const to the pointer if it is not supposed to be deleted inside the function. In almost all cases the pointer can just be const. Also your proposed new style is mostly about educating people.

>
> Proposal cost:
> Changing the delete expression signature, won't affect the calling code. So there is no source code change, no grep and modify or any type of code breakage.
Your proposed changes mostly impact closed source libraries. They need to be recompiled to use the new delete function signature. But, that wouldn’t change anything at all. In addition they need to change all their functions to take a T*& instead of a T*. This makes your claim “no source code change, no grep and modify” incorrect for the library. It is only correct for the user of the library.

With the bug 1) you described previously (the library function accidentally deleted your pointer) you are in the terrain of UB. You might have gotten lucky and your program ran correctly with a UaF. Recompiling with your proposal would then break your own code. And a double free error would have caught the bug, but your proposal just hides it. I still prefer to have the double free error to know that there is a bug. Hiding bugs instead is never a good idea.

> Following the guidline suggested above, will affect only the header files, included by the consuming source code.
Not just the header, but also the .cpp file of the library. I repeat myself: there is no compiler magic that makes T*& compatible with an already compiled T*. If you change the header you need to recompile the library as well.

> 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.
Nope. The ABI needs to be broken. The library currently expects the address of an object. Now, you are handing it the address of a pointer to the object (in a simplified view for function parameters references and pointers are the same concerning the assembler code).
> If done once, I hope that It can be done twice.
It worked before because a) it was something in the specification that the compiler couldn’t check. The compiled code was truly the exact same (except the implementation of delete changed, but not its function signature). And b) the specification allowed more than before. You are actually trying to restrict (all pointers are now null instead of having the original value after calling delete). Certainly, your restriction doesn’t prohibit anything useful.
>
> 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.
>
My conclusion is that you are relying on compiler magic that is not possible. Also, the only real motivation you are giving is to hide bugs. The only bug that is actually hidden is a double free. A UaF is still not prevented (but your proposal would allow to detect a nullptr if there is a bug in the library). The third bug category, a memory leak, is not changed in any way by this proposal. I don’t like hiding bugs.

If a closed source library function always accidentally deletes your pointer, this function does not have any useful functionality. Nobody will be able to use this function correctly. Every user of this library will run into this bug. The bug needs to be fixed. If the maintainer does not want to fix the bug you should stop using the library because it is not reliable. Your proposal does not change that.

If your proposal is implemented, you still need to check for nullptr after every library call to avoid UaF. This significantly hurts performance and is not even required by the library documentation. If you are willing to sacrifice performance, you can also use (some) smart pointers and get actual safety.

We should be (hopefully) on track for safety profiles. They could add the assumption that raw pointers are non-owning. A simple annotation will still get you around this restriction (if you even enable the safety profile in the first place). In this way a safety profile could prevent calling delete on (specific) raw pointers. I think this is a much better solution that addresses the actual bug (a library function accidentally deleting the wrong raw pointers). Can somebody confirm that safety profiles are still on track? Do they include something like I have described?

Nobody is stopping you from going forward with the proposal. I predict it has a very low chance of being accepted by the committee (this very long discussion is a very good indicator for that). The committee resources are also very restricted. It is highly likely they will consider more important proposals before yours. And there will always be more important proposals. If I’m correct about this you are just wasting your time to make your proposal useful. In the meantime your proposal might be superseded by other proposals like safety profiles.

You should also note that the earliest time your proposal can be made mandatory is C++29. The feature set for C++26 is already fixed. You need to put your proposal into the context of at least the C++26 features, but also into the context of some of the C++29 features.

Received on 2025-08-25 08:04:06