[interest] rich-typed smart pointers
Dear Boost members, I have designed a smart pointer that relies on the type system to offer safe and flexible automatic memory management at no runtime cost. It combines single ownership as in std::unique_ptr and reliable access from multiple places as in std::shared_ptr with some additional features. I believe it to be about as safe and practically useful as a smart pointer in C++11 could get. Further explanation and a proof-of-concept implementation is available over here: https://github.com/jgonggrijp/rich-typed-pointers There is no license yet, but you have my explicit permission to try the library and the example programs at your own risk. :-) Could this library be useful addition to Boost? I look forward to your comments. Thanks in advance, -Julian PS: a possible minor seed for discussion is the current lack of an implicit cast to bool. I agree with Jeff Flinn's argument why the implicit cast may be desirable [1]. However, it interferes with the comparison operators when one of the operands should be implicitly cast to weak_ptr, as explained in [2]. I could repeat myself and write comparison functions for all possible overloads, but this seems inelegant. [1] http://boost.2283326.n4.nabble.com/Looking-for-thoughts-on-a-new-smart-point... [2] https://github.com/jgonggrijp/rich-typed-pointers/commit/89fcf2821cb39bbb9fc...
On 10/02/13 13:34, Julian Gonggrijp wrote:
Dear Boost members,
I have designed a smart pointer that relies on the type system to offer safe and flexible automatic memory management at no runtime cost. It combines single ownership as in std::unique_ptr and reliable access from multiple places as in std::shared_ptr with some additional features. I believe it to be about as safe and practically useful as a smart pointer in C++11 could get.
Further explanation and a proof-of-concept implementation is available over here:
https://github.com/jgonggrijp/rich-typed-pointers
There is no license yet, but you have my explicit permission to try the library and the example programs at your own risk. :-)
[snip] Reading: https://github.com/jgonggrijp/rich-typed-pointers makes it sound useful, especially the section containing: Note that rich typed pointers can never be part of multiple ownership cycles at the same time. This is a true impossibility because at any time exactly one pointer will own a given object. However, that statement seems to be contradicted by one just above it: Cyclic ownership using data_ptr and move is possible with some effort, but cannot happen by accident. Could you please clarify? -regards, Larry
Larry Evans wrote:
Reading:
https://github.com/jgonggrijp/rich-typed-pointers
makes it sound useful, especially the section containing:
Note that rich typed pointers can never be part of multiple ownership cycles at the same time. This is a true impossibility because at any time exactly one pointer will own a given object.
However, that statement seems to be contradicted by one just above it:
Cyclic ownership using data_ptr and move is possible with some effort, but cannot happen by accident.
Could you please clarify?
Of course! It is possible to create a ring of owning data_ptrs (with effort, by design), where each pointer owns the next and the last owns the first. However, there can be no pointer outside the ring that owns a pointer inside the ring, because then the pointer inside the ring would be owned by two other pointers at the same time (i.e. the previous pointer in the ring as well as the external pointer) and that's impossible because of the way the rich-typed pointers are implemented. In order for a pointer to be part of two ownership cycles at the same time it would have to be part of two rings. But then the previous pointer from ring #1 would own it and be external to ring #2 (and vice versa). We just saw that this is impossible. In summary, any data_ptr can be part of at most one ownership cycle at a time. Other rich-typed pointers cannot be part of any ownership cycle at all. Hope this helps! -Julian
Julian Gonggrijp
I have designed a smart pointer that relies on the type system to offer safe and flexible automatic memory management at no runtime cost. It combines single ownership as in std::unique_ptr and reliable access from multiple places as in std::shared_ptr with some additional features. I believe it to be about as safe and practically useful as a smart pointer in C++11 could get.
From reading the documentation, it seems that owner_ptr/data_ptr are basically equivalent in functionality to std::unique_ptr, and weak_ptr is basically equivalent to a raw pointer/raw reference. (Referring to std::shared_ptr seems a bit misleading given that you don't address the problem of multiple ownership.) As I understand it, the goal of your library is not to add any functionality over std::unique_ptr, but to provide a little bit more compile-time safety at the cost of some additional typing (pun intended). It does seem that wrappers around unique_ptr and shared_ptr that disable default construction and discourage storing a null pointer would be useful for many people. It would be much more useful if they explicitly interoperated with those types, though.
I think the primary use case for your proposed weak_ptr (possibly renamed param_ptr or arg_ptr) would be as the declared type for function parameters. When used only in that context, the implicit conversion from an owning pointer to a raw pointer is safe, because any values used as function arguments are guaranteed to live for the duration of the function. For any other use, though, the implicit conversion is actually dangerous, and I would say that the warning signal that syntax like: T *x = foo.get(); or T &x = *foo; provides is helpful.
Jeremy Maitin-Shepard wrote:
From reading the documentation, it seems that owner_ptr/data_ptr are basically equivalent in functionality to std::unique_ptr, and weak_ptr is basically equivalent to a raw pointer/raw reference. (Referring to std::shared_ptr seems a bit misleading given that you don't address the problem of multiple ownership.)
Well I think there are two ways to see it. I was motivated by both of them: std::unique_ptr + added safety + non-owning companion smart pointer type (as you suggested) or std::shared_ptr + std::weak_ptr - accidental cyclic ownership - runtime overhead I believe multiple ownership in std::shared_ptr is not a goal in itself, but just a way to ensure that objects stay live as long as they're needed. In my opinion I do actually address the problem of multiple ownership by taking it away. :-) As for the rich-typed weak_ptr, I think you should really think of it as a smart pointer, if only because it doesn't allow the user to deallocate the referenced object.
As I understand it, the goal of your library is not to add any functionality over std::unique_ptr, but to provide a little bit more compile-time safety at the cost of some additional typing (pun intended).
Well, yes. I do consider that additional functionality.
It does seem that wrappers around unique_ptr and shared_ptr that disable default construction and discourage storing a null pointer would be useful for many people. It would be much more useful if they explicitly interoperated with those types, though.
Fortunately it is trivial to add some interoperability with the std:: pointers. I'll add that to the roadmap.
I think the primary use case for your proposed weak_ptr (possibly renamed param_ptr or arg_ptr) would be as the declared type for function parameters.
That, and as a way to create arbitrarily complex linked data structures without introducing ownership problems. Note how this makes it more similar to std::weak_ptr than you may have thought initially. I do like your suggestion for the name param_ptr or arg_ptr, though. I've been considering whether I should split weak_ptr in two types, one for function arguments and one for linked data structures. In that case your name suggestions would be excellent for the former while I could keep using the name weak_ptr for the latter. This would allow me to enforce that the proposed param_ptr is always non-null so that functions can rely on that.
When used only in that context, the implicit conversion from an owning pointer to a raw pointer is safe, because any values used as function arguments are guaranteed to live for the duration of the function.
For any other use, though, the implicit conversion is actually dangerous,
Could you elaborate on why implicit conversion to my weak_ptr would be dangerous?
and I would say that the warning signal that syntax like:
T *x = foo.get();
or
T &x = *foo;
provides is helpful.
Since type deduction is only rudimental in C++11, there actually is such a warning signal: auto x = weak(foo); Hope this helps. Thanks for your feedback! -Julian
Julian Gonggrijp
Jeremy Maitin-Shepard wrote:
From reading the documentation, it seems that owner_ptr/data_ptr are basically equivalent in functionality to std::unique_ptr, and weak_ptr is basically equivalent to a raw pointer/raw reference. (Referring to std::shared_ptr seems a bit misleading given that you don't address the problem of multiple ownership.)
Well I think there are two ways to see it. I was motivated by both of them:
std::unique_ptr + added safety + non-owning companion smart pointer type
(as you suggested) or
std::shared_ptr + std::weak_ptr - accidental cyclic ownership - runtime
overhead
I believe multiple ownership in std::shared_ptr is not a goal in itself, but just a way to ensure that objects stay live as long as they're needed. In my opinion I do actually address the problem of multiple ownership by taking it away.
Well, regardless of how you want to look at it, shared_ptr allows an arbitrary acyclic ownership/"ensure pointers stay alive" graph, while unique_ptr only allows an ownership forest.
As for the rich-typed weak_ptr, I think you should really think of it as a smart pointer, if only because it doesn't allow the user to deallocate the referenced object.
As I understand it, the goal of your library is not to add any functionality over std::unique_ptr, but to provide a little bit more compile-time safety at the cost of some additional typing (pun intended).
Well, yes. I do consider that additional functionality.
It does seem that wrappers around unique_ptr and shared_ptr that disable default construction and discourage storing a null pointer would be useful for many people. It would be much more useful if they explicitly interoperated with those types, though.
Fortunately it is trivial to add some interoperability with the std:: pointers. I'll add that to the roadmap.
I think the primary use case for your proposed weak_ptr (possibly renamed param_ptr or arg_ptr) would be as the declared type for function parameters.
That, and as a way to create arbitrarily complex linked data structures without introducing ownership problems. Note how this makes it more similar to std::weak_ptr than you may have thought initially.
I think that's overselling it a bit. It only supports tree structures (with possible back pointers using raw pointers/your proposed weak_ptr). Certainly in many cases a tree structure is all that is needed, but sometimes a more general ownership graph is needed.
I do like your suggestion for the name param_ptr or arg_ptr, though. I've been considering whether I should split weak_ptr in two types, one for function arguments and one for linked data structures. In that case your name suggestions would be excellent for the former while I could keep using the name weak_ptr for the latter. This would allow me to enforce that the proposed param_ptr is always non-null so that functions can rely on that.
Perhaps it would be useful to have both a possibly null and a non-null variant of param_ptr, where the non-null version has a runtime check.
When used only in that context, the implicit conversion from an owning pointer to a raw pointer is safe, because any values used as function arguments are guaranteed to live for the duration of the function.
For any other use, though, the implicit conversion is actually dangerous,
Could you elaborate on why implicit conversion to my weak_ptr would be dangerous?
Because the weak_ptr may outlive the underlying object, and the implicit conversion makes it silent. The only context in which this can't happen is if the weak_ptr is used only as a function parameter type. A dangling reference is quite possible if used as a return type or struct/class member, or even a local variable. It is best to limit implicit conversions to cases that are guaranteed to be safe, and can't possibly result in a dangling reference. If it is possibly unsafe, there should be some syntactic cue indicating as much.
Since type deduction is only rudimental in C++11, there actually is such a warning signal:
auto x = weak(foo);
Sure, in this case there is a syntactic cue, although it is perhaps not strong enough: "weak reference" typically means a non-owning reference that nonetheless knows when the reference becomes invalid, and therefore is still safe. This is the case for std::weak_ptr, as well as weak references in most other languages. For that reason I think it would be better to choose a different name.
namespace rtp = rich_typed_ptr; Jeremy Maitin-Shepard wrote:
I believe multiple ownership in std::shared_ptr is not a goal in itself, but just a way to ensure that objects stay live as long as they're needed. In my opinion I do actually address the problem of multiple ownership by taking it away.
Well, regardless of how you want to look at it, shared_ptr allows an arbitrary acyclic ownership/"ensure pointers stay alive" graph, while unique_ptr only allows an ownership forest.
[...]
I think the primary use case for your proposed weak_ptr (possibly renamed param_ptr or arg_ptr) would be as the declared type for function parameters.
That, and as a way to create arbitrarily complex linked data structures without introducing ownership problems. Note how this makes it more similar to std::weak_ptr than you may have thought initially.
I think that's overselling it a bit. It only supports tree structures (with possible back pointers using raw pointers/your proposed weak_ptr). Certainly in many cases a tree structure is all that is needed, but sometimes a more general ownership graph is needed.
Let us be very precise. Ownership graphs using rtp::data_ptr allow rings and trees. Rings cannot overlap, but each ring node may at the same time be the root node of a tree. To be honest I find myself unable to imagine a data structure that needs even more complex ownership than that. Graph containers with a linked implementation for example only need tree-like ownership, with reference cycles closed by non-owning pointers. Regardless, this is just the *automatic* ownership part of the general pointer graph; using rtp::weak_ptr (to be renamed) one can create any arbitrary graph, and the user can implement ownership on top of that because it is possible to break a rtp::data_ptr chain using access through a rtp::weak_ptr. I like to call this meta-ownership and I have demonstrated its use for a cyclically owned data structure in my linked ring example.
Could you elaborate on why implicit conversion to my weak_ptr would be dangerous?
Because the weak_ptr may outlive the underlying object, and the implicit conversion makes it silent. The only context in which this can't happen is if the weak_ptr is used only as a function parameter type. A dangling reference is quite possible if used as a return type or struct/class member, or even a local variable. It is best to limit implicit conversions to cases that are guaranteed to be safe, and can't possibly result in a dangling reference. If it is possibly unsafe, there should be some syntactic cue indicating as much.
Since type deduction is only rudimental in C++11, there actually is such a warning signal:
auto x = weak(foo);
Sure, in this case there is a syntactic cue, although it is perhaps not strong enough: "weak reference" typically means a non-owning reference that nonetheless knows when the reference becomes invalid, and therefore is still safe. This is the case for std::weak_ptr, as well as weak references in most other languages. For that reason I think it would be better to choose a different name.
I agree that the name is too suggestive of semantics similar to std::weak_ptr. I'll change it. As for safety, I think std::weak_ptr is not really safer that rtp::weak_ptr because it's just as easy to produce a dangling pointer (in fact std takes only 3 lines of code to do it while rtp requires 4) and that's a disaster no matter what. The fact that it can detect that it's dangling does however make it easier to debug. Again, thanks for your feedback. -Julian
On 10/6/2013 8:34 AM, Quoth Julian Gonggrijp:
As for safety, I think std::weak_ptr is not really safer that rtp::weak_ptr because it's just as easy to produce a dangling pointer (in fact std takes only 3 lines of code to do it while rtp requires 4) and that's a disaster no matter what. The fact that it can detect that it's dangling does however make it easier to debug.
I fail to see why "dangling pointers" are a disaster with std::weak_ptr. Their implementation does not permit any access without locking the pointer, which will either succeed (and consequently prevent the object from being destroyed while it is being accessed) or fail (if the object has already been destroyed) and return a null pointer. The code using the pointer has to be able to cope gracefully with failure to obtain a pointer, but this is built into the semantics of weak_ptrs -- if you can't write the code such that it can fail gracefully, then you should have been using a shared_ptr instead so that you can guarantee that the object won't disappear before you're ready for it to do so. Bare pointers, on the other hand (including rtp::weak_ptr, from what I understand from this discussion, though I haven't examined the code specifically) have no such tracking/locking capability so there is no way to ensure that a particular pointer value still points at the same object as it originally did. Sure, it's still possible to get a null pointer access with std::weak_ptr, by locking one and then accessing the resulting shared_ptr without checking for validity (ie. lazy programmer). But in this case it will assert, and even failing that null accesses are far easier to track down and deal with than non-null-invalid-object accesses. (I'm not saying that shared_ptrs don't have problems -- for example it's far too easy to get into trouble with cyclic references if you're not careful, and I've yet to find any good tools to track these down. But they don't have *this* particular problem, since solving that is pretty much their entire reason for existing in the first place.)
Gavin Lambert wrote:
On 10/6/2013 8:34 AM, Quoth Julian Gonggrijp:
As for safety, I think std::weak_ptr is not really safer that rtp::weak_ptr because it's just as easy to produce a dangling pointer (in fact std takes only 3 lines of code to do it while rtp requires 4) and that's a disaster no matter what. The fact that it can detect that it's dangling does however make it easier to debug.
I fail to see why "dangling pointers" are a disaster with std::weak_ptr.
Their implementation does not permit any access without locking the pointer, which will either succeed (and consequently prevent the object from being destroyed while it is being accessed) or fail (if the object has already been destroyed) and return a null pointer.
The code using the pointer has to be able to cope gracefully with failure to obtain a pointer, but this is built into the semantics of weak_ptrs -- if you can't write the code such that it can fail gracefully, then you should have been using a shared_ptr instead so that you can guarantee that the object won't disappear before you're ready for it to do so.
The std::weak_ptr detects the dangling pointer and changes it into a null pointer. This makes sense, because null pointers are easier to detect. However, as the surrounding code probably relies on a live pointer (because dangling pointers are never planned) the program is still going to fail. This is what I meant by "disaster". You say that the caller should have been using std::shared_ptr instead, but similar reasoning applies to the rtp pointers. All pointers (smart and raw) are associated with pitfalls that the user has to avoid. Now, you would be right to point out that I haven't properly spelled out the pitfalls for the rtp pointers; I will fix that omission in the next alpha version. Further note that the user probably chose std::weak_ptr in the first place in order to avoid cyclic ownership. If the only way to avoid the dangling pointer is to switch back to std::shared_ptr, this boils down to a choice between two evils: a dangling pointer or a memory leak. Fortunately this will usually not be the case, because (I believe) dangling pointers can always be avoided with more careful program logic. This however is equally true of the rtp pointers.
Bare pointers, on the other hand (including rtp::weak_ptr, from what I understand from this discussion, though I haven't examined the code specifically) have no such tracking/locking capability
Please forgive me for nagging about terminology, but rtp::weak_ptr should really be thought of as a smart pointer. It is a wrapper around a raw pointer that helps to take ownership management out of the user's hands, in this case by ensuring that the corresponding rtp::owner_ptr (or rtp::data_ptr) is the only entity that can deallocate the object. If you think that a smart pointer must on top of that be able to track whether a pointer is still live, you have effectively equated "smart pointer" to "reference-counted pointer" and you are forced to subscribe to the view that std::unique_ptr is a "bare" pointer.
so there is no way to ensure that a particular pointer value still points at the same object as it originally did.
Sure, it's still possible to get a null pointer access with std::weak_ptr, by locking one and then accessing the resulting shared_ptr without checking for validity (ie. lazy programmer). But in this case it will assert, and even failing that null accesses are far easier to track down and deal with than non-null-invalid-object accesses.
(I'm not saying that shared_ptrs don't have problems -- for example it's far too easy to get into trouble with cyclic references if you're not careful, and I've yet to find any good tools to track these down. But they don't have *this* particular problem, since solving that is pretty much their entire reason for existing in the first place.)
They don't have the specific problem that dangling pointers are hard to track down, thanks to reference counting, because dangling pointers change into null pointers on the spot. I think calling that the sole purpose of std::shared_ptr and std::weak_ptr would be giving them too little credit, they can do much more for the user than just that. Still, as you say, there are serious problems associated with them and I believe this is sufficient reason to look into different approaches. -Julian
On 7 October 2013 11:01, Julian Gonggrijp wrote:
The std::weak_ptr detects the dangling pointer and changes it into a null pointer. This makes sense, because null pointers are easier to detect. However, as the surrounding code probably relies on a live pointer (because dangling pointers are never planned) the program is still going to fail. This is what I meant by "disaster".
Why is it going to fail? Expired pointers (not dangling ones) most certainly are planned, and weak_ptr is designed to support (and detect) that case. Users of std::weak_ptr know that it needs to be checked, and the explicit conversion that is needed makes it hard to forget to do that. Either you say: std::shared_ptr<X> sp(wp); which will throw if weak_ptr.expired() is true, or you use the non-throwing form in a conditional: if (auto sp = wp.lock()) /* ... */ ; else /* deal with it */ ;
On 10/07/13 05:45, Jonathan Wakely wrote:
On 7 October 2013 11:01, Julian Gonggrijp wrote:
The std::weak_ptr detects the dangling pointer and changes it into a null pointer. This makes sense, because null pointers are easier to detect. However, as the surrounding code probably relies on a live pointer (because dangling pointers are never planned) the program is still going to fail. This is what I meant by "disaster".
Why is it going to fail? Expired pointers (not dangling ones) most certainly are planned, and weak_ptr is designed to support (and detect) that case. Users of std::weak_ptr know that it needs to be checked, and the explicit conversion that is needed makes it hard to forget to do that. Either you say:
std::shared_ptr<X> sp(wp);
which will throw if weak_ptr.expired() is true, or you use the non-throwing form in a conditional:
if (auto sp = wp.lock()) /* ... */ ; else /* deal with it */ ;
To provide a concrete example, the attached code produces: ***test_run<1>::run() { obj_id+:10, obj_count:1} { obj_id+:11, obj_count:2} inner empty(wp)=0 inner deref(wp).my_id=11 { obj_id-:11, obj_count:1} exited inner outer empty(wp)=0 outer deref(wp).my_id=0 { obj_id-:10, obj_count:0} exited outer ***test_run<0>::run() { obj_id+:12, obj_count:1} { obj_id+:13, obj_count:2} inner empty(wp)=0 inner deref(wp).my_id=13 { obj_id-:13, obj_count:1} exited inner outer empty(wp)=1 make: *** [run] Segmentation fault (core dumped) where the output prefixed with ***test_run<0>::run used the std:: smart pointers and shows the detection of expired weak_ptr: outer empty(wp)=1 and caused a segfault when it deref'ed the wp. In contrast, the output prefixed with ***test_run<1>::run, which uses the rich_type_ptr smart pointers, does *not* detect the expired weak pointer; consequently, does not cause a segfault when dereferencing that pointer, which, in a real program, would make it harder to find the bug. (BTW, the obj_id.cpp file is found in my other post on a different branch of this thread. ) HTH. -regards, Larry
Larry Evans wrote:
On 10/07/13 05:45, Jonathan Wakely wrote:
On 7 October 2013 11:01, Julian Gonggrijp wrote:
The std::weak_ptr detects the dangling pointer and changes it into a null pointer. This makes sense, because null pointers are easier to detect. However, as the surrounding code probably relies on a live pointer (because dangling pointers are never planned) the program is still going to fail. This is what I meant by "disaster".
Why is it going to fail? Expired pointers (not dangling ones) most certainly are planned, and weak_ptr is designed to support (and detect) that case. Users of std::weak_ptr know that it needs to be checked, and the explicit conversion that is needed makes it hard to forget to do that. Either you say:
std::shared_ptr<X> sp(wp);
which will throw if weak_ptr.expired() is true, or you use the non-throwing form in a conditional:
if (auto sp = wp.lock()) /* ... */ ; else /* deal with it */ ;
To provide a concrete example, the attached code produces:
***test_run<1>::run() { obj_id+:10, obj_count:1} { obj_id+:11, obj_count:2} inner empty(wp)=0 inner deref(wp).my_id=11 { obj_id-:11, obj_count:1} exited inner outer empty(wp)=0 outer deref(wp).my_id=0 { obj_id-:10, obj_count:0} exited outer ***test_run<0>::run() { obj_id+:12, obj_count:1} { obj_id+:13, obj_count:2} inner empty(wp)=0 inner deref(wp).my_id=13 { obj_id-:13, obj_count:1} exited inner outer empty(wp)=1 make: *** [run] Segmentation fault (core dumped)
where the output prefixed with ***test_run<0>::run used the std:: smart pointers and shows the detection of expired weak_ptr:
outer empty(wp)=1
and caused a segfault when it deref'ed the wp.
In contrast, the output prefixed with ***test_run<1>::run, which uses the rich_type_ptr smart pointers, does *not* detect the expired weak pointer; consequently, does not cause a segfault when dereferencing that pointer, which, in a real program, would make it harder to find the bug.
As far as I'm concerned this outcome is expected. I thought we already agreed about the existence of this difference in behaviour between std::weak_ptr and rtp::weak_ptr? I even already announced that I would change the name of rtp::weak_ptr to something else for exactly this reason, several emails ago. -Julian
Jonathan Wakely wrote:
On 7 October 2013 11:01, Julian Gonggrijp wrote:
The std::weak_ptr detects the dangling pointer and changes it into a null pointer. This makes sense, because null pointers are easier to detect. However, as the surrounding code probably relies on a live pointer (because dangling pointers are never planned) the program is still going to fail. This is what I meant by "disaster".
Why is it going to fail? Expired pointers (not dangling ones) most certainly are planned, and weak_ptr is designed to support (and detect) that case. Users of std::weak_ptr know that it needs to be checked, and the explicit conversion that is needed makes it hard to forget to do that. Either you say:
std::shared_ptr<X> sp(wp);
which will throw if weak_ptr.expired() is true, or you use the non-throwing form in a conditional:
if (auto sp = wp.lock()) /* ... */ ; else /* deal with it */ ;
Of course a programmer can avoid the problem by doing the right thing. That's not what my paragraph above was about. As I stated in the follow-up, the same reasoning applies to the rtp pointers. Your own quick example clearly suggests how the programmer may do the wrong thing: auto sp = wp.lock(); // use without checking for null Please note that I'm not disputing that reference counting has a clear advantage to single ownership in cases like these. I'm just saying that dangling or expired pointers can still be a pitfall despite that advantage. -Julian
On 10/07/13 08:34, Julian Gonggrijp wrote:
Jonathan Wakely wrote:
On 7 October 2013 11:01, Julian Gonggrijp wrote:
The std::weak_ptr detects the dangling pointer and changes it into a null pointer. This makes sense, because null pointers are easier to detect. However, as the surrounding code probably relies on a live pointer (because dangling pointers are never planned) the program is still going to fail. This is what I meant by "disaster".
Why is it going to fail? Expired pointers (not dangling ones) most certainly are planned, and weak_ptr is designed to support (and detect) that case. Users of std::weak_ptr know that it needs to be checked, and the explicit conversion that is needed makes it hard to forget to do that. Either you say:
std::shared_ptr<X> sp(wp);
which will throw if weak_ptr.expired() is true, or you use the non-throwing form in a conditional:
if (auto sp = wp.lock()) /* ... */ ; else /* deal with it */ ;
Of course a programmer can avoid the problem by doing the right thing. That's not what my paragraph above was about. As I stated in the follow-up, the same reasoning applies to the rtp pointers.
Your own quick example clearly suggests how the programmer may do the wrong thing:
auto sp = wp.lock(); // use without checking for null
However, the key point, I believe, is that the programmer *can* check for an expired weak_ptr with std::pointer. OTOH, with rtp::weak_ptr (or whatever future name you choose), the programmer has no such ability.
Please note that I'm not disputing that reference counting has a clear advantage to single ownership in cases like these. I'm just saying that dangling or expired pointers can still be a pitfall despite that advantage.
I see your point. the rtp pointer has the advantage of much less overyhead, but the disadvantage of makeing it harder to detect dangling pointers and program a workaround. Is that a good summary? -regards, Larry
Larry Evans wrote:
On 10/07/13 08:34, Julian Gonggrijp wrote:
Please note that I'm not disputing that reference counting has a clear advantage to single ownership in cases like these. I'm just saying that dangling or expired pointers can still be a pitfall despite that advantage.
I see your point. the rtp pointer has the advantage of much less overyhead, but the disadvantage of makeing it harder to detect dangling pointers and program a workaround.
Is that a good summary?
Truthful but incomplete. Rich-typed pointers also prevent accidental cyclic ownership, while allowing for cyclic ownership by design that can be undone, i.e. without causing memory leaks. HTH, -Julian
On 7 October 2013 14:34, Julian Gonggrijp wrote:
Jonathan Wakely wrote:
On 7 October 2013 11:01, Julian Gonggrijp wrote:
The std::weak_ptr detects the dangling pointer and changes it into a null pointer. This makes sense, because null pointers are easier to detect. However, as the surrounding code probably relies on a live pointer (because dangling pointers are never planned) the program is still going to fail. This is what I meant by "disaster".
Why is it going to fail? Expired pointers (not dangling ones) most certainly are planned, and weak_ptr is designed to support (and detect) that case. Users of std::weak_ptr know that it needs to be checked, and the explicit conversion that is needed makes it hard to forget to do that. Either you say:
std::shared_ptr<X> sp(wp);
which will throw if weak_ptr.expired() is true, or you use the non-throwing form in a conditional:
if (auto sp = wp.lock()) /* ... */ ; else /* deal with it */ ;
Of course a programmer can avoid the problem by doing the right thing. That's not what my paragraph above was about. As I stated in the follow-up, the same reasoning applies to the rtp pointers.
Does it? How do I tell if an rtp::weak_ptr is dangling or not? You seem to be claiming that because sloppy programmers can forget to check a std::weak_ptr for validity that it's no safer than a type that doesn't even support such checking for validity. I say screw the sloppy programmers, the type that supports checking is safer than one that doesn't. With std::weak_ptr I can write my code to allow expired pointers and handle them correctly, but IIUC with the rtp (soon-to-be-renamed) weak_ptr I just have to "do the right thing" to avoid the possibility of expired pointers.
Jonathan Wakely wrote:
On 7 October 2013 14:34, Julian Gonggrijp wrote:
Of course a programmer can avoid the problem by doing the right thing. That's not what my paragraph above was about. As I stated in the follow-up, the same reasoning applies to the rtp pointers.
Does it? How do I tell if an rtp::weak_ptr is dangling or not?
You seem to be claiming that because sloppy programmers can forget to check a std::weak_ptr for validity that it's no safer than a type that doesn't even support such checking for validity. [...]
I'm not.
Please note that I'm not disputing that reference counting has a clear advantage to single ownership in cases like these. I'm just saying that dangling or expired pointers can still be a pitfall despite that advantage.
-Julian
On 10/02/13 13:34, Julian Gonggrijp wrote:
Dear Boost members,
I have designed a smart pointer that relies on the type system to offer safe and flexible automatic memory management at no runtime cost. It combines single ownership as in std::unique_ptr and reliable access from multiple places as in std::shared_ptr with some additional features. I believe it to be about as safe and practically useful as a smart pointer in C++11 could get.
Further explanation and a proof-of-concept implementation is available over here:
[snip] If there were a templated owned_ptr CTOR: template<typename Derived> owner_ptr (owner_ptr<Derived> && source) : pointer(source.pointer) { source.pointer = nullptr; } wouldn't that eliminate the need for the make_dynamic templated function? Just wondering. I haven't tried it, but I'd guess it would work because the ownd_ptr<Derived> could be constructed with the: make<Derived>(t1,t2,...tn) and then the templated owned_ptr CTOR would then convert that to the base class? -regards, Larry
Larry Evans wrote:
https://github.com/jgonggrijp/rich-typed-pointers [snip] If there were a templated owned_ptr CTOR:
template<typename Derived> owner_ptr (owner_ptr<Derived> && source) : pointer(source.pointer) { source.pointer = nullptr; }
wouldn't that eliminate the need for the make_dynamic templated function?
Just wondering. I haven't tried it, but I'd guess it would work because the ownd_ptr<Derived> could be constructed with the:
make<Derived>(t1,t2,...tn)
and then the templated owned_ptr CTOR would then convert that to the base class?
I agree this would be great, but unfortunately it appears it can't be done. owner_ptr<Derived> would have to friend-declare owner_ptr<Base>, but it can't know in advance that there is a Base. It is not possible to just friend-declare owner_ptr<U> for all U, either, because C++ only allows complete types as friends (there's no such thing as a templated friend declaration). Thanks for the suggestion though! -Julian
On 10/05/13 16:26, Julian Gonggrijp wrote:
Larry Evans wrote:
https://github.com/jgonggrijp/rich-typed-pointers [snip] If there were a templated owned_ptr CTOR:
template<typename Derived> owner_ptr (owner_ptr<Derived> && source) : pointer(source.pointer) { source.pointer = nullptr; }
wouldn't that eliminate the need for the make_dynamic templated function?
Just wondering. I haven't tried it, but I'd guess it would work because the ownd_ptr<Derived> could be constructed with the:
make<Derived>(t1,t2,...tn)
and then the templated owned_ptr CTOR would then convert that to the base class?
I agree this would be great, but unfortunately it appears it can't be done. owner_ptr<Derived> would have to friend-declare owner_ptr<Base>, I don't see why. AFAICT, you'd require *fewer* friend declarations, not more( you wouldn't need the:
template
On 10/05/13 20:14, Larry Evans wrote:
On 10/05/13 16:26, Julian Gonggrijp wrote:
Larry Evans wrote:
https://github.com/jgonggrijp/rich-typed-pointers [snip] If there were a templated owned_ptr CTOR:
template<typename Derived> owner_ptr (owner_ptr<Derived> && source) : pointer(source.pointer) { source.pointer = nullptr; }
wouldn't that eliminate the need for the make_dynamic templated function?
Just wondering. I haven't tried it, but I'd guess it would work because the ownd_ptr<Derived> could be constructed with the:
make<Derived>(t1,t2,...tn)
and then the templated owned_ptr CTOR would then convert that to the base class?
I agree this would be great, but unfortunately it appears it can't be done. owner_ptr<Derived> would have to friend-declare owner_ptr<Base>, I don't see why. AFAICT, you'd require *fewer* friend declarations, not more( you wouldn't need the:
template
friend owner_ptr<U1> make_dynamic (Us&& ...); the only friend needed would the existing:
template
friend owner_ptr<U> make (Us&& ...); that's because the proposed templated constructor would have the same access as the existing:
owner_ptr (owner_ptr && source);
CTOR, i.e. public. IOW, with the proposed templated CTOR, the following example code creating an owner_ptr<Base> should work, AFAICT:
owner_ptr<Base> owner_baee ( make<Derived> ( DerivedInit0() , DerivedInit1() . . . DerivedInitN() ) );
Where DeriveInit0, DerivedInit1, ... DerivedInitN are the types of the args to the CTOR for Derived.
Is there some reason why the above example code would *not* compile, given then proposed templated CTOR? [snip]
Coded it but it failed to compile because owner_ptr<shape> could not assign a nullptr to owner_ptr<circle>::pointer because (as you mentioned) owner_ptr<shape> is not a fried of owner_ptr<circle>. Sorry for noise. -regards, Larry
On 10/05/13 23:14, Larry Evans wrote:
On 10/05/13 20:14, Larry Evans wrote:
On 10/05/13 16:26, Julian Gonggrijp wrote:
Larry Evans wrote:
https://github.com/jgonggrijp/rich-typed-pointers [snip] If there were a templated owned_ptr CTOR:
template<typename Derived> owner_ptr (owner_ptr<Derived> && source) : pointer(source.pointer) { source.pointer = nullptr; }
wouldn't that eliminate the need for the make_dynamic templated function?
Just wondering. I haven't tried it, but I'd guess it would work because the ownd_ptr<Derived> could be constructed with the:
make<Derived>(t1,t2,...tn)
and then the templated owned_ptr CTOR would then convert that to the base class?
I agree this would be great, but unfortunately it appears it can't be done. owner_ptr<Derived> would have to friend-declare owner_ptr<Base>, I don't see why. AFAICT, you'd require *fewer* friend declarations, not more( you wouldn't need the:
template
friend owner_ptr<U1> make_dynamic (Us&& ...); the only friend needed would the existing:
template
friend owner_ptr<U> make (Us&& ...); that's because the proposed templated constructor would have the same access as the existing:
owner_ptr (owner_ptr && source);
CTOR, i.e. public. IOW, with the proposed templated CTOR, the following example code creating an owner_ptr<Base> should work, AFAICT:
owner_ptr<Base> owner_baee ( make<Derived> ( DerivedInit0() , DerivedInit1() . . . DerivedInitN() ) );
Where DeriveInit0, DerivedInit1, ... DerivedInitN are the types of the args to the CTOR for Derived.
Is there some reason why the above example code would *not* compile, given then proposed templated CTOR? [snip]
Coded it but it failed to compile because owner_ptr<shape> could not assign a nullptr to owner_ptr<circle>::pointer because (as you mentioned) owner_ptr<shape> is not a fried of owner_ptr<circle>.
Sorry for noise.
Googling: c++ templated friend lead to: http://stackoverflow.com/questions/3292795/template-friend which lead to the owner_ptr CTOR code: template<typename Derived> owner_ptr (owner_ptr<Derived>&& source) : pointer(source.pointer) { source.pointer = nullptr; } template <class Derived> friend class owner_ptr; in rich_typed_ptr.hpp which resulted in successful compile and run of the shapes.cpp example without using make_dynamic. HTH. -regards, Larry
Larry Evans wrote:
Googling:
c++ templated friend
lead to:
http://stackoverflow.com/questions/3292795/template-friend
which lead to the owner_ptr CTOR code:
template<typename Derived> owner_ptr (owner_ptr<Derived>&& source) : pointer(source.pointer) { source.pointer = nullptr; } template <class Derived> friend class owner_ptr;
in rich_typed_ptr.hpp which resulted in successful compile and run of the shapes.cpp example without using make_dynamic.
HTH.
Yes! Thanks a million. -Julian
On 10/02/13 13:34, Julian Gonggrijp wrote:
Dear Boost members,
I have designed a smart pointer that relies on the type system to offer safe and flexible automatic memory management at no runtime cost. It combines single ownership as in std::unique_ptr and reliable access from multiple places as in std::shared_ptr with some additional features. I believe it to be about as safe and practically useful as a smart pointer in C++11 could get. [snip] When the attached dangling_ptr.cpp driver is run (using the also attached obj_id.cpp), the output (last attachment) indicates the weak_ptr is accessing a dangling pointer. Refcounted smart pointers (such as boost::shared_ptr), would not allow that, IIUC.
Am I missing something? -regards, Larry
Larry Evans wrote:
I have designed a smart pointer that relies on the type system to offer safe and flexible automatic memory management at no runtime cost. It combines single ownership as in std::unique_ptr and reliable access from multiple places as in std::shared_ptr with some additional features. I believe it to be about as safe and practically useful as a smart pointer in C++11 could get. [snip] When the attached dangling_ptr.cpp driver is run (using the also attached obj_id.cpp), the output (last attachment) indicates the weak_ptr is accessing a dangling pointer. Refcounted smart pointers (such as boost::shared_ptr), would not allow that, IIUC.
Am I missing something?
No, you are not missing anything. In this respect the rtp pointers behave rather like std::unique_ptr with std::unique_ptr::get. Jeremy Maitin-Shepard was right to point out that the name rtp::weak_ptr is misleading for this reason. I will clear up the confusion and be more explicit about the risks in the next alpha version. Thanks again for the effort! -Julian
participants (5)
-
Gavin Lambert
-
Jeremy Maitin-Shepard
-
Jonathan Wakely
-
Julian Gonggrijp
-
Larry Evans