Date: Sun, 24 Aug 2025 00:50:40 -0700
Sorry Simon, this is a combination of follow on from your comments and direct response to organicoman*
> On Aug 23, 2025, at 11:44 PM, Simon Schröder via Std-Proposals <std-proposals_at_[hidden]> wrote:
>
>
>
>> On Aug 23, 2025, at 6:36 PM, organicoman via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> Really?
>>
>> void somefn() {
>> auto ptr = std::make_unique(5);
>> auto ptr2 = std::unique_ptr<int>(ptr.get()); // Maybe this should
>> have been .release() ?
>> }
>>
>> In this example you did two things:
>> 1- you are breaking the smart pointer contract intentionally (that's not how we use them)
>> 2- you are jumping back and forth between two worlds, smart pointers and raw pointers
>>
>> My proposal is meant to catch unintentional wrong usage.
>> If you break an API contract you have to be careful since there is no safe guards.
>
> You are right: If you are using smart pointers correctly, there is no problem. To this I say: If you are using raw pointer correctly, we don’t need your proposal. This example showed that it is possible to do stupid things with smart pointers as well and not just raw pointer. It is much harder, but it is possible.
>
> BTW, the use of smart pointers does not preclude the use of raw pointers. Using smart pointers in function calls is a bad idea most of the time. Raw pointers should still be used for non-owning pointers (and you therefore never create a smart pointer from a raw pointers).
Follow on from Simon’s comment:
It is also very hard to avoid smart pointer decay: I’ve often asked about having some mechanism to _force_ that the |this| pointer is always owned - someSmartPointerType->memberFunction(…) always drops the protection from the shared pointer - you can almost do so with deduced this, but that does not work with virtual functions. This is exploitable, and has been exploited in bugs I have encountered. I know that the WebKit project at least has now been actively developing static analysis in clang specifically for the purpose of ensuring things like local protection of |this| pointers; this is _possible_ in WK as it uses invasive refcounting: e.g. it is possible for WK to guarantee that any pointer (within the relevant types) can be arbitrarily protected by taking a ref count because any allocation of the type is definitionally refcounted.
>> If you scan through the standard library, you will find some function that deals with raw pointers directly
>> std::destroy_at, std::construct_at, std::deallocate.....etc
>> All these take pointers by copy, so if you fall in the following case:
>> {
>> m_ptr = std::allocate(...);
>> //.....
>> std::deallocate(m_ptr);
>> //.... after some lines
>> std::deallocate(m_ptr);
>> }
>> Because the pointer is passed by copy, nothing will tell if the resource is still alive.
Response to organicoman:
Yes, but the proposal has also failed to address owner of the memory owning the reference - the examples presented all assume lexically bound lifetime - which permits the use of a reference - but this fails for any code where the ability to guarantee lexical lifetime of the pointer being referenced is not possible.
>
>> > Let's forget the passing by reference thing....just focus on the delete operator which nullify it's pointer parameter. I don't see why it is not the standard..
>>
>> So throw away the part of the proposal that (if it didn't break things
>> along the way) was supposed to address the truly useful part (also
>> assuming that dereferencing nullptr were defined behaviour)? OK:
>>
>> Undefined behavior as per the standard , but segfault consensus by CPU's and microchips....
>> Almost all processing units of any size, consider the address 0 and its vicinity as special memory addresses.
>
> This is not necessarily the case for embedded processors. There might be something really important at address 0. And embedded processors also don’t necessarily have virtual memory and thus cannot produce segfaults. You make too many assumptions about processors.
Follow on from Simon’s comment:
It is not a requirement of any processor I am aware of, embedded or otherwise, the fact that null pointer derefences fault is a decision by the host OS (as Simon says there are _many_ platforms for which null pointer dereferences by design do not fault) - assuming an MMU that enforces memory protection - that intentionally prohibit or limit allocations of the 0-page (on 32 bit machines) or the entire lower 4gig on 64bit systems, but this is a _decision_ made by the OS (in the case of 64bit systems this is an intentional choice to make truncation to 32 bit immediately noticeable).
More importantly though it is not even true:
int* foo.= nullptr;
foo[1ull << 33] = 5;
Is absolutely not guaranteed to fault, despite being a (by definition) a null dereference.
>
> Furthermore, there was a recent discussion that showed (on Compiler Explorer) that even major compilers for certain pointer types produce the value -1 for nullptr. So, nullptr is not always 0, even on the compilers you are using.
To simon: I’m willing to accept that this proposal said assigning nullptr to the pointer, so on those platforms that result would be assigning -1.
>>
>> If we're focusing on just having the delete operator able to set its
>> argument to nullptr: it doesn't address the plethora of other copies
>> of the pointer still pointing at now released memory,
>>
>> Setting its argument to nullptr alone has local effect only on the pointer itself, but adding the guideline 'passing the pointer by reference instead of copy to callee' will add convention to how it will live and die.
>>
>> The 'reference' concept, means that the referenced object ensures that it will outlive all its references.
> No, it doesn’t mean that as soon as you add function calls. What it does mean is that you always are using double indirection which is bad for performance. And I’m not even sure if the compiler will always reload the pointer before every use just in case somebody else has changed it. You might have to make it volatile or something. That’s a terrible idea.
+1
>>
>> Expl
>> {
>> // std::vector v{1,2 3} : defined outside
>> auto val = v[1];
>> foo_clears_vec(v);
>> val = ...; // dangling ref
>> }
>>
>> Operator[ ] returns a reference, but does not guarantee that the referenced value outlives its reference.
>
> auto val = v[1];
> creates a copy and does not store a reference even though a reference was returned. You have to write
> auto &val = v[1];
> And it is certainly a good idea that operator[] returns a reference because you can also store really large objects inside containers. You might even want to be able to write
> v[1] = 2;
> which is only possible with a reference.
> A reference also means you can put non-copyable objects inside the container, like unique_ptr. You see, a reference is almost mandatory here for C++ to succeed.
+1
> One more additional thing: If you assign a new address to a pointer after it has been deleted and you never read from that pointer again, the compiler is allowed to optimize away this assignment. It does not matter that others have a reference to it. There is currently no legal way to write the kind of code in C++ and actually do what you want. (And disallowing this optimization will not get you any friends.) This is why this belongs into the compiler as a flag because you cannot express it in the language.
To Simon: I am not sure this is correct, e.g take (making each step very explicit):
void f(T*& ptr) {
T* local = ptr;
delete local;
ptr = nullptr;
}
The object referenced by ptr has ended, the lifetime of the object pointed to by the reference has not (pointer to the storage of the pointer, not the object pointed to by the pointer)
—Oliver
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
> On Aug 23, 2025, at 11:44 PM, Simon Schröder via Std-Proposals <std-proposals_at_[hidden]> wrote:
>
>
>
>> On Aug 23, 2025, at 6:36 PM, organicoman via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> Really?
>>
>> void somefn() {
>> auto ptr = std::make_unique(5);
>> auto ptr2 = std::unique_ptr<int>(ptr.get()); // Maybe this should
>> have been .release() ?
>> }
>>
>> In this example you did two things:
>> 1- you are breaking the smart pointer contract intentionally (that's not how we use them)
>> 2- you are jumping back and forth between two worlds, smart pointers and raw pointers
>>
>> My proposal is meant to catch unintentional wrong usage.
>> If you break an API contract you have to be careful since there is no safe guards.
>
> You are right: If you are using smart pointers correctly, there is no problem. To this I say: If you are using raw pointer correctly, we don’t need your proposal. This example showed that it is possible to do stupid things with smart pointers as well and not just raw pointer. It is much harder, but it is possible.
>
> BTW, the use of smart pointers does not preclude the use of raw pointers. Using smart pointers in function calls is a bad idea most of the time. Raw pointers should still be used for non-owning pointers (and you therefore never create a smart pointer from a raw pointers).
Follow on from Simon’s comment:
It is also very hard to avoid smart pointer decay: I’ve often asked about having some mechanism to _force_ that the |this| pointer is always owned - someSmartPointerType->memberFunction(…) always drops the protection from the shared pointer - you can almost do so with deduced this, but that does not work with virtual functions. This is exploitable, and has been exploited in bugs I have encountered. I know that the WebKit project at least has now been actively developing static analysis in clang specifically for the purpose of ensuring things like local protection of |this| pointers; this is _possible_ in WK as it uses invasive refcounting: e.g. it is possible for WK to guarantee that any pointer (within the relevant types) can be arbitrarily protected by taking a ref count because any allocation of the type is definitionally refcounted.
>> If you scan through the standard library, you will find some function that deals with raw pointers directly
>> std::destroy_at, std::construct_at, std::deallocate.....etc
>> All these take pointers by copy, so if you fall in the following case:
>> {
>> m_ptr = std::allocate(...);
>> //.....
>> std::deallocate(m_ptr);
>> //.... after some lines
>> std::deallocate(m_ptr);
>> }
>> Because the pointer is passed by copy, nothing will tell if the resource is still alive.
Response to organicoman:
Yes, but the proposal has also failed to address owner of the memory owning the reference - the examples presented all assume lexically bound lifetime - which permits the use of a reference - but this fails for any code where the ability to guarantee lexical lifetime of the pointer being referenced is not possible.
>
>> > Let's forget the passing by reference thing....just focus on the delete operator which nullify it's pointer parameter. I don't see why it is not the standard..
>>
>> So throw away the part of the proposal that (if it didn't break things
>> along the way) was supposed to address the truly useful part (also
>> assuming that dereferencing nullptr were defined behaviour)? OK:
>>
>> Undefined behavior as per the standard , but segfault consensus by CPU's and microchips....
>> Almost all processing units of any size, consider the address 0 and its vicinity as special memory addresses.
>
> This is not necessarily the case for embedded processors. There might be something really important at address 0. And embedded processors also don’t necessarily have virtual memory and thus cannot produce segfaults. You make too many assumptions about processors.
Follow on from Simon’s comment:
It is not a requirement of any processor I am aware of, embedded or otherwise, the fact that null pointer derefences fault is a decision by the host OS (as Simon says there are _many_ platforms for which null pointer dereferences by design do not fault) - assuming an MMU that enforces memory protection - that intentionally prohibit or limit allocations of the 0-page (on 32 bit machines) or the entire lower 4gig on 64bit systems, but this is a _decision_ made by the OS (in the case of 64bit systems this is an intentional choice to make truncation to 32 bit immediately noticeable).
More importantly though it is not even true:
int* foo.= nullptr;
foo[1ull << 33] = 5;
Is absolutely not guaranteed to fault, despite being a (by definition) a null dereference.
>
> Furthermore, there was a recent discussion that showed (on Compiler Explorer) that even major compilers for certain pointer types produce the value -1 for nullptr. So, nullptr is not always 0, even on the compilers you are using.
To simon: I’m willing to accept that this proposal said assigning nullptr to the pointer, so on those platforms that result would be assigning -1.
>>
>> If we're focusing on just having the delete operator able to set its
>> argument to nullptr: it doesn't address the plethora of other copies
>> of the pointer still pointing at now released memory,
>>
>> Setting its argument to nullptr alone has local effect only on the pointer itself, but adding the guideline 'passing the pointer by reference instead of copy to callee' will add convention to how it will live and die.
>>
>> The 'reference' concept, means that the referenced object ensures that it will outlive all its references.
> No, it doesn’t mean that as soon as you add function calls. What it does mean is that you always are using double indirection which is bad for performance. And I’m not even sure if the compiler will always reload the pointer before every use just in case somebody else has changed it. You might have to make it volatile or something. That’s a terrible idea.
+1
>>
>> Expl
>> {
>> // std::vector v{1,2 3} : defined outside
>> auto val = v[1];
>> foo_clears_vec(v);
>> val = ...; // dangling ref
>> }
>>
>> Operator[ ] returns a reference, but does not guarantee that the referenced value outlives its reference.
>
> auto val = v[1];
> creates a copy and does not store a reference even though a reference was returned. You have to write
> auto &val = v[1];
> And it is certainly a good idea that operator[] returns a reference because you can also store really large objects inside containers. You might even want to be able to write
> v[1] = 2;
> which is only possible with a reference.
> A reference also means you can put non-copyable objects inside the container, like unique_ptr. You see, a reference is almost mandatory here for C++ to succeed.
+1
> One more additional thing: If you assign a new address to a pointer after it has been deleted and you never read from that pointer again, the compiler is allowed to optimize away this assignment. It does not matter that others have a reference to it. There is currently no legal way to write the kind of code in C++ and actually do what you want. (And disallowing this optimization will not get you any friends.) This is why this belongs into the compiler as a flag because you cannot express it in the language.
To Simon: I am not sure this is correct, e.g take (making each step very explicit):
void f(T*& ptr) {
T* local = ptr;
delete local;
ptr = nullptr;
}
The object referenced by ptr has ended, the lifetime of the object pointed to by the reference has not (pointer to the storage of the pointer, not the object pointed to by the pointer)
—Oliver
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Received on 2025-08-24 07:50:56