Date: Sat, 22 Feb 2025 23:07:33 +0000
This is a long post so I have split it into sections so you don't get lost reading it.
---------------------------------------------------------------------------------------------------------------------------------------------
Who has conflicting design goals? The proposed ptr_to_unique or our criteria for judging if something is acceptable?
---------------------------------------------------------------------------------------------------------------------------------------------
C++ is an enabling language (I would call it an engineer's language) that lets you construct anything including the means to shoot yourself in the foot or blow your head off. It generally does not a reject a useful feature just because a way can be found of using it wrongly. Although we may use the type system to constrain its use, it has always been the policy that there is no obligation that a feature must come with all the means to prevent its incorrect use. It has always been considered too big an ask that is likely to burden or compromise well formed code or even be impossible to achieve.
However the language is now at a crossroads where there is an existential imperative to reduce its exposure to undefined behaviour caused by coding errors. This impacts on the above policy and has started to produce conflicting criteria in determining if a feature is good to use. Some cases of undetected 'ill formed code' have caused a lot of trouble so now there is a sense that a new feature perhaps should do more to prevent its incorrect use. This is a sense rather than a criteria or policy but it is at play and produces a new perspective.
This crossroads puts the proposed ptr_to_unique in the cross hairs of both perspectives and they are conflicting. It isn't ptr_to_unique that has conflicting design goals, it is our criteria for judging its acceptance.
According to the traditional approach, ptr_to_unique is acceptable because it has a large domain of correct use and incorrect use would be 'ill formed code'. Burdening it with thread checking just in case it is used incorrectly would be considered an abomination where you are paying for something that good code will never use.
However we now feel a need to reduce the opportunities for ill formed code to be written undetected and certainly worry about introducing new ones. The proposed ptr_to_unique is new, it is a smart pointer and ill formed code can be written with it. So naturally that triggers concerns from this new perspective. To address those concerns I have provided thread checking to catch ill formed code at run time but this sorely offends the traditional perspective.
-----------------------------------------------------------------------
The way forward, perhaps
-----------------------------------------------------------------------
How can ptr_to_unique be true to the traditional criteria but also address the concerns of the new perspective?
I think the answer is that ptr_to_unique comes without thread checking and satisfies the traditional criteria of acceptance.
But to meet the concerns of the new perspective, the three lines of code that implement thread checking can be conditionally compiled in as a kind of run-time lint that can be run now and again to catch non threadsafe use of ptr_to_unique. It might be wise the run all code through it before release just to make sure rookie programmers haven't got the wrong idea and been making a mess of bombs with long fuses – as I have already outlined as a ' horror scenario' in a previous post.
The thread checking option does not have to be part of the standard. It can be something that implementations could choose to include.
'-----------------------------------------------------------------------------------------------------------------------
The distinct domains of C++ whose users may not always understand each other very well.
------------------------------------------------------------------------------------------------------------------------
I am starting to realise C++ and its programmers can occupy very different domains that can produce different perspectives of what is useful.
If you work on the server side you will be sharing resources with multiple clients running in multiple threads and you will be doing this with shared_ptr/weak_ptr. You may use unique_ptr within a thread but probably will have no reason to hold any other persistent reference to it. If you live in this world then you would have no use for ptr_to_unique and might therefore not perceive it as offering anything useful.
I have mostly worked on the client side only serving the one supreme master, the user. When the user closes a document or deletes an item in a collection, it should be gone and its destructor called. There should be no phantom objects living on still being referenced by something which could possibly still access them. To correctly model this, single ownership is required and unique_ptr is used as the primary owning reference to heap objects.
Yes, you can model it with shared_ptr and some do, but it will never be a faithful model and will be subject to unintended memory retention and even unintended data access to something that was supposed to be gone – very similar to using a garbage collector. Not everyone wants to do it this way, C++ can manage tighter lifetime control using single ownership. So the choice of using single ownership must continue to be respected and fully supported. It is more fundamental than shared ownership,
-----------------------------------------------------------------------------------------------------
Visually serving the user and the unavoidable use cases for ptr_to_unique
------------------------------------------------------------------------------------------------------
I typically serve the user by providing a graphical user interface that allows them to interact with data in the form of a polymorphic collection which is held as vector of unique_ptrs. Usually the entire collection is displayed on the screen and some items within it can be selected for special attention One will be the item that has the current user focus for deletion, editing or soliciting further information. There may be others that are selected for monitoring in different ways, perhaps displaying extra information in a different window or a corner of the screen.
When you are serving the user with a dynamic and interactive visual interface with high granularity, you have to take care to avoid screen lag. So you can't have screen repainting routines walking the collection each time to find what item they should be dealing with. They need to have that information at their fingertips. So we store pointers to those selected items that can be used by the repainting routines. If a repainting routine finds the pointer to its selected item still valid then it has direct access to the item. If it is not then it either does nothing or runs a procedure to find a new item to select. This is a necessary optimisation. Without it I would have to reduce the granularity of the display or tolerate unacceptable levels of screen lag.
The problem is that raw pointers don't zero automatically when their pointee is deleted and therefore provide no indication that they have become invalid. So I had to write code to intercept deletes and keep a list of references to all the selection pointers being used so they could be zeroed if they reference the item being deleted. It was horrible hugely intrusive and fragile spaghetti code but I had no choice. Accidents were not rare. A delete might by-pass my special delete function that was supposed to catch all deletes or I might fail to add a new reference to the list. It only worked because all selection pointers were members of the class that held the collection and couldn't go out of scope. If one were to be held by a module that can be unloaded then my zeroing routine could hit a dangling reference. It also occurred to me that as shared_ptr, weak_ptr and unique_ptr can't dangle, it is likely that it is this scenario that is still hitting us with dangling pointers. This is the use case for ptr_to_unique.
I hold those selection pointers with ptr_to_unique and they will zero automatically when their pointee is deleted. I don't have to write all that spaghetti code and non-null is an adequate test of their validity before use. Not only does it make my code safe and simple, it also enables me to do things that would previously have been unmanageable, like having members of a collection hold safe ptr_to_unique references to each other.
-----------------------------------------------------------------------
The design goal of ptr_to_unique
-----------------------------------------------------------------------
The design goal of ptr_to_unique is one thing only, to serve the scenario I have just described so that it is easier to manage and doesn't persist in being a cause of dangling pointers. Managing the scenario without it could be described as something that it is unreasonably difficult to get right.
It is not its design aim that it can be passed to another thread where it can be used in a threadsafe manner. That would be using it outside of its scope (and the scope of unique_ptr) and therefore would be ill formed code. I know we have discussed creating such a thing by either stretching single ownership or putting a lock on deletions but they are other design aims. They are not the design aims of the proposed ptr_to_unique. We have shared_ptr/weak_ptr to manage threadsafe access to items with multithread visibility.
-----------------------------------------------------------------------
Towards safer C++
-----------------------------------------------------------------------
If you want C++ to be safer then addressing dangling pointers coming from a scenario that is difficult to get right is low hanging fruit. The utility of ptr_to_unique in facilitating design and closing a gaping dangling pointer vulnerability must be balanced against concerns that it might accidentally be misused.
-----------------------------------------------------------------------
My overall opinion
-----------------------------------------------------------------------
I continue to believe that ptr_to_unique is sorely needed and that need should not remain unaddressed just because there is a way in which it can be misunderstood and misused - that is not in the spirit of C++.
I am still listening. My opinion may be enough to keep me pushing this proposal but it is not enough for it to prevail.
---------------------------------------------------------------------------------------------------------------------------------------------
Who has conflicting design goals? The proposed ptr_to_unique or our criteria for judging if something is acceptable?
---------------------------------------------------------------------------------------------------------------------------------------------
C++ is an enabling language (I would call it an engineer's language) that lets you construct anything including the means to shoot yourself in the foot or blow your head off. It generally does not a reject a useful feature just because a way can be found of using it wrongly. Although we may use the type system to constrain its use, it has always been the policy that there is no obligation that a feature must come with all the means to prevent its incorrect use. It has always been considered too big an ask that is likely to burden or compromise well formed code or even be impossible to achieve.
However the language is now at a crossroads where there is an existential imperative to reduce its exposure to undefined behaviour caused by coding errors. This impacts on the above policy and has started to produce conflicting criteria in determining if a feature is good to use. Some cases of undetected 'ill formed code' have caused a lot of trouble so now there is a sense that a new feature perhaps should do more to prevent its incorrect use. This is a sense rather than a criteria or policy but it is at play and produces a new perspective.
This crossroads puts the proposed ptr_to_unique in the cross hairs of both perspectives and they are conflicting. It isn't ptr_to_unique that has conflicting design goals, it is our criteria for judging its acceptance.
According to the traditional approach, ptr_to_unique is acceptable because it has a large domain of correct use and incorrect use would be 'ill formed code'. Burdening it with thread checking just in case it is used incorrectly would be considered an abomination where you are paying for something that good code will never use.
However we now feel a need to reduce the opportunities for ill formed code to be written undetected and certainly worry about introducing new ones. The proposed ptr_to_unique is new, it is a smart pointer and ill formed code can be written with it. So naturally that triggers concerns from this new perspective. To address those concerns I have provided thread checking to catch ill formed code at run time but this sorely offends the traditional perspective.
-----------------------------------------------------------------------
The way forward, perhaps
-----------------------------------------------------------------------
How can ptr_to_unique be true to the traditional criteria but also address the concerns of the new perspective?
I think the answer is that ptr_to_unique comes without thread checking and satisfies the traditional criteria of acceptance.
But to meet the concerns of the new perspective, the three lines of code that implement thread checking can be conditionally compiled in as a kind of run-time lint that can be run now and again to catch non threadsafe use of ptr_to_unique. It might be wise the run all code through it before release just to make sure rookie programmers haven't got the wrong idea and been making a mess of bombs with long fuses – as I have already outlined as a ' horror scenario' in a previous post.
The thread checking option does not have to be part of the standard. It can be something that implementations could choose to include.
'-----------------------------------------------------------------------------------------------------------------------
The distinct domains of C++ whose users may not always understand each other very well.
------------------------------------------------------------------------------------------------------------------------
I am starting to realise C++ and its programmers can occupy very different domains that can produce different perspectives of what is useful.
If you work on the server side you will be sharing resources with multiple clients running in multiple threads and you will be doing this with shared_ptr/weak_ptr. You may use unique_ptr within a thread but probably will have no reason to hold any other persistent reference to it. If you live in this world then you would have no use for ptr_to_unique and might therefore not perceive it as offering anything useful.
I have mostly worked on the client side only serving the one supreme master, the user. When the user closes a document or deletes an item in a collection, it should be gone and its destructor called. There should be no phantom objects living on still being referenced by something which could possibly still access them. To correctly model this, single ownership is required and unique_ptr is used as the primary owning reference to heap objects.
Yes, you can model it with shared_ptr and some do, but it will never be a faithful model and will be subject to unintended memory retention and even unintended data access to something that was supposed to be gone – very similar to using a garbage collector. Not everyone wants to do it this way, C++ can manage tighter lifetime control using single ownership. So the choice of using single ownership must continue to be respected and fully supported. It is more fundamental than shared ownership,
-----------------------------------------------------------------------------------------------------
Visually serving the user and the unavoidable use cases for ptr_to_unique
------------------------------------------------------------------------------------------------------
I typically serve the user by providing a graphical user interface that allows them to interact with data in the form of a polymorphic collection which is held as vector of unique_ptrs. Usually the entire collection is displayed on the screen and some items within it can be selected for special attention One will be the item that has the current user focus for deletion, editing or soliciting further information. There may be others that are selected for monitoring in different ways, perhaps displaying extra information in a different window or a corner of the screen.
When you are serving the user with a dynamic and interactive visual interface with high granularity, you have to take care to avoid screen lag. So you can't have screen repainting routines walking the collection each time to find what item they should be dealing with. They need to have that information at their fingertips. So we store pointers to those selected items that can be used by the repainting routines. If a repainting routine finds the pointer to its selected item still valid then it has direct access to the item. If it is not then it either does nothing or runs a procedure to find a new item to select. This is a necessary optimisation. Without it I would have to reduce the granularity of the display or tolerate unacceptable levels of screen lag.
The problem is that raw pointers don't zero automatically when their pointee is deleted and therefore provide no indication that they have become invalid. So I had to write code to intercept deletes and keep a list of references to all the selection pointers being used so they could be zeroed if they reference the item being deleted. It was horrible hugely intrusive and fragile spaghetti code but I had no choice. Accidents were not rare. A delete might by-pass my special delete function that was supposed to catch all deletes or I might fail to add a new reference to the list. It only worked because all selection pointers were members of the class that held the collection and couldn't go out of scope. If one were to be held by a module that can be unloaded then my zeroing routine could hit a dangling reference. It also occurred to me that as shared_ptr, weak_ptr and unique_ptr can't dangle, it is likely that it is this scenario that is still hitting us with dangling pointers. This is the use case for ptr_to_unique.
I hold those selection pointers with ptr_to_unique and they will zero automatically when their pointee is deleted. I don't have to write all that spaghetti code and non-null is an adequate test of their validity before use. Not only does it make my code safe and simple, it also enables me to do things that would previously have been unmanageable, like having members of a collection hold safe ptr_to_unique references to each other.
-----------------------------------------------------------------------
The design goal of ptr_to_unique
-----------------------------------------------------------------------
The design goal of ptr_to_unique is one thing only, to serve the scenario I have just described so that it is easier to manage and doesn't persist in being a cause of dangling pointers. Managing the scenario without it could be described as something that it is unreasonably difficult to get right.
It is not its design aim that it can be passed to another thread where it can be used in a threadsafe manner. That would be using it outside of its scope (and the scope of unique_ptr) and therefore would be ill formed code. I know we have discussed creating such a thing by either stretching single ownership or putting a lock on deletions but they are other design aims. They are not the design aims of the proposed ptr_to_unique. We have shared_ptr/weak_ptr to manage threadsafe access to items with multithread visibility.
-----------------------------------------------------------------------
Towards safer C++
-----------------------------------------------------------------------
If you want C++ to be safer then addressing dangling pointers coming from a scenario that is difficult to get right is low hanging fruit. The utility of ptr_to_unique in facilitating design and closing a gaping dangling pointer vulnerability must be balanced against concerns that it might accidentally be misused.
-----------------------------------------------------------------------
My overall opinion
-----------------------------------------------------------------------
I continue to believe that ptr_to_unique is sorely needed and that need should not remain unaddressed just because there is a way in which it can be misunderstood and misused - that is not in the spirit of C++.
I am still listening. My opinion may be enough to keep me pushing this proposal but it is not enough for it to prevail.
Received on 2025-02-22 23:07:38