Re: setter for deleter in boost::shared_ptr (and alike)
* Kai-Uwe Bux:
Alf P. Steinbach wrote:
* Kai-Uwe Bux:
Alf P. Steinbach wrote:
* James Dennett:
You *may* be able to assign to it,
There is no "may" about it.
Unless you're talking again about an inaccessible assignment operator,
which has nothing to do with what shared_ptr enforces or not.
Of course it has to do with that. The design of shared_ptr<> opting for
get_deleter and leaving out
template < typename D >
set_deleter( D d )
is enforcing that the type of the deleter cannot be changed. That client
code _can_ disable assignment is a direct consequence of the guarantees
made by shared_ptr.
I'm sorry, that's incorrect.
It is a little imprecise. The client can prevent a change of the deleters
behavior because of this.
Yes.
And I'm always able to assign.
Because I'm providing the deleter.
As long as shared_ptr does not require assignable deleters, a deleter
can be non-assignable.
True but irrelevant.
Oh, it's very relevant to the above (that shared_ptr's guarantees has
nothing to do with whether it can be done), and I think the point you're
trying to make is irrelevant /in that context/, although of course it is
a valid point on its own, therefore also made by others, including me,
earlier in this thread. But this is just nitpicking and rehashing.
We're discussing the fine points of validity of arguments about how many
angels can fit in a barrel of sardines, and that's just silly.
The point is that there is a definite difference in
guarantees made by
shared_ptr {
template < typename D >
D * get_deleter();
};
and
shared_ptr {
template < typename D >
D * get_deleter();
template < typename D >
void set_deleter( D d );
};
The later would allow replacing the deleter object no matter what. Making
the deleter non-assignable would buy you nothing. That making the deleter
non-assignable isn't pointless is because shared_ptr guarantees the
identity
of the deleter object (although not the invariance of its value).
Largely true: what the client code /can achieve/ by making a deleter
non-assignable is a consequence of the shared_ptr guarantees.
However, whether the client code can assign is independent of the
guarantees. shared_ptr does not prevent assignment. By your own
argument that client code can prevent assignment, it can also enable
assignment, thus, you can always assign.
There is no "may" about it.
No matter what guarantees shared_ptr provide or not, that's still the
case: assignability of the deleter itself has nothing to do with what
shared_ptr enforces or not, but the ability to assign if the deleter is
assignable, the ability to change the effective deleter, /is/ a direct
consequence of lack of enforcement.
It's a consequence of design. I do not see a lack of enforcement because I
do not agree that changing the value of the deleter is a bad idea per se.
Well. Then I face a very uphill battle in trying to convince you. It
reminds me of some debates about
int a[1000];
memset( a, 0, 1000*sizeof(int) );
versus
int a[1000] = {0};
The first few engagements with the opponent usually goes fine: OK, it
actually is possible, OK, it doesn't generate that bad machine code, OK,
it might even generate nearly as good code as the treasured memset. But
then comes the problem that calling memset is perceived as "freedom",
including the freedom to just copy and paste memset-using code, and
restricting oneself to C++ initialization etc. as a -- restriction.
Convincing someone that a restriction can be Good is almost hopeless.
I don't think I've ever succeeded in that.
We should not loose sight of the main contention that the return type of
get_deleter() being non-const is a design flaw. That has been claimed by
you
Right.
(but not demonstrated).
I'm sorry, that incorrect.
It is. You have demonstrated that there are funny things that can happen
when you change the value of a deleter of type pointer-to-function. You
have not demonstrated that get_deleter() returning a non-const pointer is
at fault:
Consider the following patches to TR1:
a) Changing the return type of get_deleter():
template < typename D >
D const * get_deleter()
b) Changing the conceptual requirement on the deleter:
The deleter shall be copy constructible _and of class type_.
I think both patches would take care of the examples you provided.
Therefore, you have not demonstrated that the fault lies with the return
type.
Well, the first patch, (a), fixes the problem by only changing /the
return type/ (except, as Alberto has remarked over in comp.std.c++, that
it still allows a const_cast; a value return would be preferable).
To me that suggests strongly that the return type is at fault. :-)
Patch (b) seems to be like throwing out the baby with the bathwater,
complicating the usage, and doesn't fix the problem of emptying that
tub: there remains the problem of stateful deleters that are changed.
All three or four articles before my first appeared in this thread,
explained how having a getter but not a setter is extremely desirable
and is tied directly to the fundamental purpose of shared_ptr.
They argued why a
template < typename D >
set_deleter( D d )
is bad. That is an entirely different matter.
As far as I can see the above is an overly optimistic re-interpretation,
for as far as I can see they didn't argue that at all.
But I have no further information than the articles published in clc++m.
Thiago Adams wrote "There is no "SetDeleter" because it should be
provided together with the resource. Generally the resource and deleter
are dependent.". The last sentence indicates that it is the dependency
between raw pointer and deleter action, that they be treated as a unit
and not be changed independently, that is important, and that precludes
not only complete replacement (object identity), but also value change.
Alberto Ganesh Barbati wrote "The idea is that the deleter is
indissolubly connected with the way the stored pointer has been
obtained, therefore it can only be specified together with the stored
pointer (either in the constructor or in reset()). Changing the deleter
while keeping the same pointer would open a huge hole in the safety net
provided by shared_ptr.". Since Alberto has gone on record as
distinguishing very clearly between "change" and "replace", what he
means here cannot, in my view, be open for discussion. Unless it's a
typo and he clarifies that here he really meant replace.
Daniel Kr?gler wroet "And no, it does *not* make sense, to provide an
*independent* setter for the deleter, because the deleter is always
associated with the currently stored object, so you can provide a new
deleter whereever you can set a new resource.". Always associated with
the currently stored object. Again, that precludes change.
It really needs no further demonstration, but just to help, consider why
you can't change the contained raw pointer similarly.
Uh, I see you're not responding to that plea.
Did you?
In the way of convincing you, I think your own conclusions will have
more weight than mine! :-)
[snip]
Right. This should be about const-correctness. It has been demonstrated
in code that the non-const return type does not prevent (or make
difficult) safe usage of shared_ptr, i.e., at the point where you say
shared_ptr ptr ( new X ... , my_deleter );
the type of my_deleter is _your_ choice. If it is class type. you _know_
which operator() will be executed upon deletion. No subsequent assignmen
can void the guarantess you put into this line.
So what exactly is the flaw?
I think you can demonstrate, satisfactorily to yourself, that a
setContainedPointer(T*) function that can be disabled would not prevent,
or make difficult, safe usage of shared_ptr. Simply refrain from using
it. And for good measure, always disable it, and voil?, safe.
Please take a few moments to actually demonstrate that, to your own
satisfaction, before reading on.
Not responsive to the question. You have to distinguish two levels of
client
code: (a) the code that initializes the shared_ptr and (b) the code that
uses the shared_ptr later. Invariance of the deleter object implies that
code of (a) type can make guarantees that cannot be subverted by
subsequent
actions in code of type (b). That would not be true if there is a
set_deleter template. Thus your setContainedPointer(T*) analogy is missing
the point.
Hm. Well, I have discussed the distinction between the levels (a) and
(b) elsewhere in this thread, mostly as a summary, see <url:
http://groups.google.no/group/comp.lang.c++.moderated/msg/ea16321837dd1335>,
and that distinction was part of the background for my plea to think
about it. So you're not entirely missing the point! :-)
You have now filled in some background that I, not very good at
communication, didn't provide.
Now I beg you to go further and consider the argument with that
background in place and in mind.
[snip]
I've demonstrated by actual code that you can change the delete action,
what actually /happens/ when a shared_ptr invokes its deleter, at will
(if there is an accessible operator of course).
That is possible if the type of the deleter is a function pointer. For
class types, the code of operator()(T*) will be executed no matter what
you try.
The point isn't that shared_ptr can't be used in safe ways.
The point is that with the current design you can make guarantees in (a)
type code without requiring (b) type code from refraining voluntarily to
to
funny stuff.
OK, I think I understand what you think I don't grok. Namely, that you
think somehow I didn't understand that shared_ptr allows instantiation
code to provide a non-assignable guarantee. My point is that that
guarantee should be provided by shared_ptr, that one shouldn't have to
write perfect client code and use only perfect client code (such as
perfect factory functions) in order to have that guarantee: a default of
safe versus default unsafe. But in trying to convince you of that I'm
up against the "freedom" v. "restriction" battle mentioned earlier.
Anything
can be used in safe ways, even goto. It's the opposite view that is of
interest when we talk about what the design enforces, namely the fact
that it needlessly and rather easily can be used in unsafe ways.
Needlessly: other than those unsafe ways necessitated by its very
nature, e.g. circular dependencies.
Ok, let's talk concrete design changes. I propose the wording that the
deleter shall be of class type as an additional requirement. I would not
change the return type of get_deleter() to non-const (although, maybe
there
should be a const and a non-const version of get_deleter). With that
little
change, which preserves the current flexibility (see below), your examples
would no longer apply. The conceptual requirement would make it clear that
at the point where you initialize a shared_ptr you have to take
responsibility of what can be done with the deleter object.
As mentioned earlier I think that's throwing out the baby with the
bathwater, and furthermore it doesn't even succeed in emptying the tub.
But others may agree that it's a good idea.
So, if you want to make that proposal, there is a current thread in
[comp.std.c++] that is relevant, <url:
http://groups.google.no/group/comp.std.c++/browse_thread/thread/5fffc83ce4e8
c1fe>.
I agree that the ideal would be that you couldn't.
I disagree that this would be idea. Somehow, my previous post did not
get
through, so I reproduce the use case here:
template < typename T >
class MyDeleter {
typedef std::tr1::function< void(T*) > command;
std::list< command > bye_bye;
public:
template < typename Command >
void push_command ( Command c ) {
bye_bye.push_back( c );
}
void operator() ( T * t_ptr ) const {
for ( std::list< command >::const_iterator iter =
bye_bye.begin();
iter != bye_bye.end(); ++ iter ) {
(*iter)(t_ptr);
}
free (t_ptr);
}
};
Note that this deleter has an accessible assignment operator. Indeed,
you
can use that to ditch all registered actions, but you cannot use it to
change the deletion from free() to, says, delete[].
Uhm, good point, I didn't think of that: the current shared_ptr design
allows you to dynamically add or remove destruction side-effects, per
contained pointer, without using an extra indirection or wrapper.
On the other hand, I've never encountered that need.
And if this was seemingly really needed, I don't think the extra
indirection (of letting the shared_ptr contain a pointer to a C++ object
doing things in its destructor) or wrapper (of shared_ptr itself), or
perhaps some other solution, would be a prohibitive cost, and it might
even be that a redesign where these dynamically specified destruction
side effects weren't needed, would be to the Good.
Well, that can be a little bit tricky. Putting notification code into the
destructor of the deleted object will notify the clients (who may hold
weak
pointers) at a moment when the destruction of the pointee has already
started. They better refrain form doing things like asking the pointee if
there are any last words to be recorded.
When I mentioned "extra indirection" I meant replacing
shared_ptr -> original pointee
with
shared_ptr -> C++ object -> original pointee
And the other alternative I mentioned is to wrap shared_ptr,
wrapper . shared_ptr -> original_pointee
A third alternative is a static map of per-pointee cleanup actions. And
I'm sure there are more ways, for this functionality that I've never
needed and never seen! But that doesn't mean that it isn't a valid use
case when you already have the functionality of current get_deleter, it
just means that to me it isn't very convincing as a use case that
requires something like the current get_deleter.
Instead, one such use case is the transferring of a unique()
shared_pointer's pointer to some other smart pointer (as I've discussed
and exemplified with code earlier; Alberto then reported over in
comp.std.c++ that something similar is done in Boost's Python binding
library, and that that seems to be the only case of actually using
get_deleter found using Google code search).
However, my view of that use case is that it is a specialized usage with
its own very important constraints on the operation, and that this
functionality therefore should be directly supported on its own,
directly (and then treating the raw pointer + deleter as a pair).
Since it is ultimately the shared_ptr that decides when deletion will
happen, the deleter object is the natural point for any clean-up code.
Hm. I nearly always think in terms of destructors doing clean-up. :-)
But anyway, see above.
Now, that is clearly a use case. It far outweighs, in my opinion, the
possible dangers of using shared_ptr<> with a stupid deleter.
Bah. :-)
Note that the only stupid deleters so far are function pointers. It is
easy
to get rid of them.
Stateful deleters can also be stupid. :-)
And just to throw in something from the side-line:
I notice that in the C++0x get_deleter is not listed under "modifiers",
but has its own little section, which indicates that it is possibly not
/intended/ as a modifier, that it's current functionality is unintended.
And while I'm at it (the draft), I mentioned earlier that I would dearly
like a std::destroy and a std::destroy_array, used by shared_ptr as default.
Thus exposing my ignorance of the C++0x draft standard where there is
std::default_delete (object and array versions, a template class) --
now I can say with more weight that if would be really nice if
shared_ptr used std::default_delete as default, instead of delete.
Cheers, & hth.,
- Alf
Disclaimer: written late at night, so may not be prefetc.
--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]