Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

shared_ptr member templates

371 views
Skip to first unread message

Scott Meyers

unread,
Jun 8, 2004, 7:25:10 PM6/8/04
to
I just noticed the following in TR1:

template<class T> class shared_ptr {
public:
template<class Y> explicit shared_ptr(Y * p);
template<class Y> void reset(Y * p);
...
};

I don't understand why these functions are member templates. What will
they accept that the corresponding non-template would not accept? Pointer
conversions are implicit, so a function taking a T* will also take anything
convertible to a T*. Why not just declare these as functions taking a T*?
What do the member templates buy us in the above?

Thanks,

Scott

---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std...@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.jamesd.demon.co.uk/csc/faq.html ]

David Abrahams

unread,
Jun 8, 2004, 8:37:08 PM6/8/04
to
Use...@aristeia.com (Scott Meyers) writes:

> I just noticed the following in TR1:
>
> template<class T> class shared_ptr {
> public:
> template<class Y> explicit shared_ptr(Y * p);
> template<class Y> void reset(Y * p);
> ...
> };
>
> I don't understand why these functions are member templates. What will
> they accept that the corresponding non-template would not accept? Pointer
> conversions are implicit, so a function taking a T* will also take anything
> convertible to a T*. Why not just declare these as functions taking a T*?
> What do the member templates buy us in the above?

You get a deleter that's appropriate to the pointer being passed:

struct B { }; // no virtual dtor

struct D
: B
{
// nontrivial dtor
~D() { std::cout << "bye\n"; }
};

shared_ptr<B> p(new D); // OK
std::auto_ptr<B> p(new D); // undefined behavior

--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com

Scott Meyers

unread,
Jun 9, 2004, 12:53:45 AM6/9/04
to
On Wed, 9 Jun 2004 00:37:08 +0000 (UTC), David Abrahams wrote:
> You get a deleter that's appropriate to the pointer being passed:
>
> struct B { }; // no virtual dtor
>
> struct D
> : B
> {
> // nontrivial dtor
> ~D() { std::cout << "bye\n"; }
> };
>
> shared_ptr<B> p(new D); // OK
> std::auto_ptr<B> p(new D); // undefined behavior


Well, sometimes you do. Then again:

B* makeAB() { return new D; }

shared_ptr<B> p(makeAB()); // undefined behavior, I assume

Is the motivation for the current design really to protect people using
inherently dangerous practices? Do we really want to help people work with
polymorphic smart pointers on non-polymorphic hierarchies?

Unrelatedly, the effects clause for the shared_ptr destructor in the April
TR1 draft makes no mention of downcasting the stored B* to a D* before
using delete on it. Shouldn't it, since that would change the behavior of
delete from undefined to well defined? Also, on the implementation side,
I'm scratching my head trying to figure out how a type specified only
during construction can be made available during destruction. The only way
I can think of is to use a virtual function during invocation of the
shared_ptr destructor, and that seems rather heavyweight. I could check
the Boost implementation, but I'm kind of hoping that somebody can tell me
how one member function of a class can communicate a type to a separate
member function of the class such that the second member function can use
that type in a cast... Or maybe I'm just thinking about this completely
incorrectly. Regardless, I'd love to be enlightened.

Scott

tom_usenet

unread,
Jun 9, 2004, 1:06:51 PM6/9/04
to
On Wed, 9 Jun 2004 04:53:45 +0000 (UTC), Use...@aristeia.com (Scott
Meyers) wrote:

>On Wed, 9 Jun 2004 00:37:08 +0000 (UTC), David Abrahams wrote:
>> You get a deleter that's appropriate to the pointer being passed:
>>
>> struct B { }; // no virtual dtor
>>
>> struct D
>> : B
>> {
>> // nontrivial dtor
>> ~D() { std::cout << "bye\n"; }
>> };
>>
>> shared_ptr<B> p(new D); // OK
>> std::auto_ptr<B> p(new D); // undefined behavior
>
>
>Well, sometimes you do. Then again:
>
> B* makeAB() { return new D; }
>
> shared_ptr<B> p(makeAB()); // undefined behavior, I assume
>
>Is the motivation for the current design really to protect people using
>inherently dangerous practices? Do we really want to help people work with
>polymorphic smart pointers on non-polymorphic hierarchies?

Perhpas not, but the "downcasting" behaviour is crucial to
shared_ptr<void>. See
http://www.boost.org/libs/smart_ptr/sp_techniques.html#pvoid
Of course, shared_ptr<void> is a specialization anyway, so perhaps the
special behaviour could be limited to it.

>Unrelatedly, the effects clause for the shared_ptr destructor in the April
>TR1 draft makes no mention of downcasting the stored B* to a D* before
>using delete on it. Shouldn't it, since that would change the behavior of
>delete from undefined to well defined? Also, on the implementation side,
>I'm scratching my head trying to figure out how a type specified only
>during construction can be made available during destruction.

Via function template specialization pointers. e.g. something like:

template<class RealType, class Deleter>
static void do_delete(T* ptr, void* deleter)
{
Deleter* realDeleter = static_cast<Deleter*>(deleter);
RealType* realPtr = static_cast<RealType*>(ptr);
(*deleter)(realPtr);
}

shared_ptr just stores one of those, assigned by the constructor:
m_deleter = &do_delete<B, A, delete_deleter>;
//all deleters have fixed sig void (*)(T*,void*)

The only way
>I can think of is to use a virtual function during invocation of the
>shared_ptr destructor, and that seems rather heavyweight. I could check
>the Boost implementation, but I'm kind of hoping that somebody can tell me
>how one member function of a class can communicate a type to a separate
>member function of the class such that the second member function can use
>that type in a cast... Or maybe I'm just thinking about this completely
>incorrectly. Regardless, I'd love to be enlightened.

The pointer to function trick is a great one, also employed in
boost::function and probably elsewhere. It's a bit like hand coding a
vtable, but you end up with much less code bloat (no need to generate
RTTI and vtables for all the deleters). I've used it in the past to
wrap C function pointer APIs with a C++ functor API.

Tom
--
C++ FAQ: http://www.parashift.com/c++-faq-lite/
C FAQ: http://www.eskimo.com/~scs/C-faq/top.html

reverse email address ""Philipp Bachmann"

unread,
Jun 9, 2004, 1:07:32 PM6/9/04
to
"Scott Meyers" <Use...@aristeia.com> wrote in message news:MPG.1b3031fe5...@news.hevanet.com...

> On Wed, 9 Jun 2004 00:37:08 +0000 (UTC), David Abrahams wrote:
> > You get a deleter that's appropriate to the pointer being passed:
> >
> > struct B { }; // no virtual dtor
> >
> > struct D
> > : B
> > {
> > // nontrivial dtor
> > ~D() { std::cout << "bye\n"; }
> > };
> >
> > shared_ptr<B> p(new D); // OK
> > std::auto_ptr<B> p(new D); // undefined behavior
>
> Also, on the implementation side,
> I'm scratching my head trying to figure out how a type specified only
> during construction can be made available during destruction. The only way
> I can think of is to use a virtual function during invocation of the
> shared_ptr destructor, and that seems rather heavyweight. I could check
> the Boost implementation, but I'm kind of hoping that somebody can tell me
> how one member function of a class can communicate a type to a separate
> member function of the class such that the second member function can use
> that type in a cast... Or maybe I'm just thinking about this completely
> incorrectly. Regardless, I'd love to be enlightened.

Basically, it works as follows:

class smart {
struct helper_base {
virtual ~helper_base(void) {}
};
template < class T > class helper {
T *t_;
public:
explicit helper(T *t) : t_(t) {}
~helper(void) { delete t_; }
}
std::auto_ptr< helper_base > h_;
public:
template< class T > explicit smart(T *t) : h_(new helper< T >(t)) {}
};

So, indeed, the destructor of "smart" resp. "std::auto_ptr< helper_base >"
calls a virtual function, but that's implicit - the trick is the virtual destructor
of "helper_base".

With "boost::shared_ptr<>", the member types are the places, where the
reference counter and the deleter function are stored, as far as I remember.

Cheers,
Philipp.

David Abrahams

unread,
Jun 9, 2004, 1:07:39 PM6/9/04
to
Use...@aristeia.com (Scott Meyers) writes:

> On Wed, 9 Jun 2004 00:37:08 +0000 (UTC), David Abrahams wrote:
>> You get a deleter that's appropriate to the pointer being passed:
>>
>> struct B { }; // no virtual dtor
>>
>> struct D
>> : B
>> {
>> // nontrivial dtor
>> ~D() { std::cout << "bye\n"; }
>> };
>>
>> shared_ptr<B> p(new D); // OK
>> std::auto_ptr<B> p(new D); // undefined behavior
>
>
> Well, sometimes you do. Then again:
>
> B* makeAB() { return new D; }
>
> shared_ptr<B> p(makeAB()); // undefined behavior, I assume

Yes, but if you follow the recommended "immediately manage all
resources with a named owner" policy you don't get into trouble.

> Is the motivation for the current design really to protect people using
> inherently dangerous practices?

There is nothing inherently dangerous about deriving from a class with
a virtual destructor. Only the lack of tools like shared_ptr has
made it dangerous.

> Do we really want to help people work with
> polymorphic smart pointers on non-polymorphic hierarchies?

Yes ;-).

It's always better to do things right where possible. But that's not
the only reason. We also want

shared_ptr<void> p(new D);

> Unrelatedly, the effects clause for the shared_ptr destructor in the April
> TR1 draft makes no mention of downcasting the stored B* to a D* before
> using delete on it. Shouldn't it, since that would change the behavior of
> delete from undefined to well defined?

I haven't looked at the TR1 text, but I'm going to answer anyway:

"No, there's no downcast; deletion is handled by the deleter."

You might read:
http://www.boost.org/libs/smart_ptr/sp_techniques.html

> Also, on the implementation side,
> I'm scratching my head trying to figure out how a type specified only
> during construction can be made available during destruction.

It's all in the deleter, my friend.

> The only way I can think of is to use a virtual function during
> invocation of the shared_ptr destructor, and that seems rather
> heavyweight.

You could call through a generated function pointer, but it amounts
to a similar, but insignificant, overhead. Memory deallocation
generally swamps the cost of any indirect call.

> I could check the Boost implementation, but I'm kind
> of hoping that somebody can tell me how one member function of a
> class can communicate a type to a separate member function of the
> class such that the second member function can use that type in a
> cast... Or maybe I'm just thinking about this completely
> incorrectly. Regardless, I'd love to be enlightened.

I think you have the basic principle correctly. The deleter is
bundled into the count object and has a virtualized function call
operator. We covered all this in the shared_ptr talk you saw me give
in Oregon... or did you miss that particular session?

--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com

---

Scott Meyers

unread,
Jun 9, 2004, 3:28:10 PM6/9/04
to
On Wed, 9 Jun 2004 17:07:39 +0000 (UTC), David Abrahams wrote:
> There is nothing inherently dangerous about deriving from a class with
> a virtual destructor. Only the lack of tools like shared_ptr has
> made it dangerous.

Uh huh. Right. So I can then expect that

shared_ptr<Base> pb(new Derived);
cout << typeid(*pb).name();

will tell me that I have a derived class object, right?

Scott

David Abrahams

unread,
Jun 9, 2004, 3:40:07 PM6/9/04
to
Use...@aristeia.com (Scott Meyers) writes:

> On Wed, 9 Jun 2004 17:07:39 +0000 (UTC), David Abrahams wrote:
>> There is nothing inherently dangerous about deriving from a class with
>> a virtual destructor. Only the lack of tools like shared_ptr has
>> made it dangerous.
>
> Uh huh. Right. So I can then expect that
>
> shared_ptr<Base> pb(new Derived);
> cout << typeid(*pb).name();
>
> will tell me that I have a derived class object, right?

Only if Base has a virtual function.

I don't see how that's relevant to your claim that not using a virtual
destructor in Base is inherently unsafe.

Nobody ever claimed that shared_ptr makes non-polymorphic objects act
as though they were polymorphic. It just handles the resource
management issue correctly.

--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com

---

Scott Meyers

unread,
Jun 10, 2004, 1:44:21 PM6/10/04
to
On Wed, 9 Jun 2004 19:40:07 +0000 (UTC), David Abrahams wrote:
> I don't see how that's relevant to your claim that not using a virtual
> destructor in Base is inherently unsafe.

It depends on what you mean by inherently unsafe. When you facilitate
something that might behave incorrectly and that is generally a sign of bad
design anyway, I'd call it inherently unsafe. In this case, we're talking
about facilitating the manipulation of a derived class object through a
base class interface lacking a virtual destructor, and in my mind, that's
generally a sign of bad design.

> Nobody ever claimed that shared_ptr makes non-polymorphic objects act
> as though they were polymorphic. It just handles the resource
> management issue correctly.

Which is one aspect of polymorphic behavior. shared_ptr is clever. It may
be useful, and for shared_ptr<void>, I'm guessing that it probably is
useful. It's in TR1, and the people who put it there have studied the
issues involved much more than I have. Still, my instinctive reaction is to
worry that it might be a little too clever. It reminds me of auto_ptr that
way.

Scott

reverse email address ""Philipp Bachmann"

unread,
Jun 10, 2004, 1:44:34 PM6/10/04
to
> class smart {
> struct helper_base {
> virtual ~helper_base(void) {}
> };
> template < class T > class helper {
> T *t_;
> public:
> explicit helper(T *t) : t_(t) {}
> ~helper(void) { delete t_; }
> }
> [...]
> };

One correction: Of course "helper<>" is derived from "helper_base",
i.e.


class smart {
struct helper_base {
virtual ~helper_base(void) {}
};

template < class T > class helper : public helper_base {


T *t_;
public:
explicit helper(T *t) : t_(t) {}
~helper(void) { delete t_; }

};
};

Sorry for the inconvenience, cheers,

Peter Dimov

unread,
Jun 10, 2004, 1:45:05 PM6/10/04
to
tom_u...@hotmail.com (tom_usenet) wrote in message news:<6cjdc0psgp3gb4frc...@4ax.com>...

[...]

> Of course, shared_ptr<void> is a specialization anyway, so perhaps the
> special behaviour could be limited to it.

It's not, actually. The only difference between shared_ptr<void> and
shared_ptr<T> is in the return type of operator*, because void& is
illegal.

Peter Dimov

unread,
Jun 10, 2004, 1:45:11 PM6/10/04
to
Use...@aristeia.com (Scott Meyers) wrote in message news:<MPG.1b3031fe5...@news.hevanet.com>...

> On Wed, 9 Jun 2004 00:37:08 +0000 (UTC), David Abrahams wrote:
> > You get a deleter that's appropriate to the pointer being passed:
> >
> > struct B { }; // no virtual dtor
> >
> > struct D
> > : B
> > {
> > // nontrivial dtor
> > ~D() { std::cout << "bye\n"; }
> > };
> >
> > shared_ptr<B> p(new D); // OK
> > std::auto_ptr<B> p(new D); // undefined behavior
>
>
> Well, sometimes you do. Then again:
>
> B* makeAB() { return new D; }
>
> shared_ptr<B> p(makeAB()); // undefined behavior, I assume

Without repeating what's already been said: the problem in your
example lies in makeAB, because (a) it's very difficult to use it
correctly, with or without shared_ptr and (b) it returns an unmanaged
resource. Nothing in shared_ptr's design encourages people to write
functions such as makeAB;

shared_ptr<B> makeAB() { return shared_ptr<B>(new D); }

would work fine.

Of course, had the designer of B made the nonvirtual destructor
protected, the "undefined behavior" shared_ptr line in your example
wouldn't have compiled.

> Is the motivation for the current design really to protect people using
> inherently dangerous practices?

In short, the motivation for the current design is to help people
write better code by increasing their options and doing the right
thing when enough information is available.

Peter Dimov

unread,
Jun 10, 2004, 1:45:16 PM6/10/04
to
Use...@aristeia.com (Scott Meyers) wrote in message news:<MPG.1b30fec54...@news.hevanet.com>...

> On Wed, 9 Jun 2004 17:07:39 +0000 (UTC), David Abrahams wrote:
> > There is nothing inherently dangerous about deriving from a class with
> > a virtual destructor. Only the lack of tools like shared_ptr has
> > made it dangerous.
>
> Uh huh. Right. So I can then expect that
>
> shared_ptr<Base> pb(new Derived);
> cout << typeid(*pb).name();
>
> will tell me that I have a derived class object, right?

Maybe, if Base has virtual functions and a protected nonvirtual
destructor. It's possible to add a 'pb.type()' extension that would
return typeid(Derived) even for non-polymorphic Bases. I've needed
something like that once or twice, but in these cases I got away with
storing the type_info const * separately, so I'm not convinced yet
that this extension is necessary.

Peter Dimov

unread,
Jun 10, 2004, 7:32:54 PM6/10/04
to
Use...@aristeia.com (Scott Meyers) wrote in message news:<MPG.1b3169b56...@news.hevanet.com>...

> On Wed, 9 Jun 2004 19:40:07 +0000 (UTC), David Abrahams wrote:
> > I don't see how that's relevant to your claim that not using a virtual
> > destructor in Base is inherently unsafe.
>
> It depends on what you mean by inherently unsafe. When you facilitate
> something that might behave incorrectly and that is generally a sign of bad
> design anyway, I'd call it inherently unsafe. In this case, we're talking
> about facilitating the manipulation of a derived class object through a
> base class interface lacking a virtual destructor, and in my mind, that's
> generally a sign of bad design.

If you had to justify that guideline, what would you say? Probably
that it's easy to accidentally delete the object via a base class
pointer and visit the undefined behavior land. But if the users are
never expected or encouraged to use 'delete' (if they are, then that's
generally a sign of bad design, if you ask me), or if the base class
destructor is not accessible, that accidental deletion doesn't seem
very likely.

Peter Dimov

unread,
Jun 11, 2004, 12:32:11 AM6/11/04
to
Use...@aristeia.com (Scott Meyers) wrote in message news:<MPG.1b2fe96e...@news.hevanet.com>...

> I just noticed the following in TR1:
>
> template<class T> class shared_ptr {
> public:
> template<class Y> explicit shared_ptr(Y * p);
> template<class Y> void reset(Y * p);
> ...
> };
>
> I don't understand why these functions are member templates. What will
> they accept that the corresponding non-template would not accept? Pointer
> conversions are implicit, so a function taking a T* will also take anything
> convertible to a T*. Why not just declare these as functions taking a T*?

Some shared_ptr history may be of interest here.

The original motivation for the member templates was that, in an
earlier version of shared_ptr that had the ordinary T* constructor,
this:

shared_ptr<B> pb = shared_ptr<D>( new D ); // [1]

was fine, even when B didn't have a virtual destructor, but this:

shared_ptr<B> pb( new D ); // [2]

was not. It wasn't very intuitive. The compiler knows that I'm passing
a D*, and it "knows" that I don't want to introduce undefined
behavior; therefore when I say [2] I must mean [1].

Of course the obvious follow-up question is why doesn't [1] eventually
result in undefined behavior, as well. And actually, in an even
earlier version, it did. Assuming that you had

shared_ptr<D> pd( new D );
shared_ptr<B> pb( pd );

if you did

pb.reset();
pd.reset();

all was fine, but if you got the order wrong:

pd.reset();
pb.reset();

it was undefined behavior. But one of the very goals of a reference
counted smart pointer is to allow you to scatter ownership around and
still expect things to be destroyed whenever they are supposed to,
without keeping track of every owner, or maintaining a set destruction
order.

In addition, this behavior is non-local, and hence, difficult to
specify. It's much better, from specification point of view, for
operations to be context-free. That is, we should be able to analyze
the example:

shared_ptr<D> pd( new D );
shared_ptr<B> pb( pd );
pd.reset();
pb.reset();

on a line by line basis, point to one particular line, and say,
there's your undefined behavior (precondition violation).

'pb.reset()' isn't a good candidate. You should always be able to
reset() a shared_ptr; it has no precondition (under the assumption
that everything up to that point was well defined, of course).

That leaves two possibilities. First,

shared_ptr<B> pb( pd );

invokes undefined behavior / is ill-formed, because B doesn't have a
virtual destructor; which means that our other example that happens to
work fine now also violates the specification.

And second, there is no undefined behavior. Either reset() order shall
work as if the two reset()s were issued in the "correct" order.

Why was the second alternative chosen? Because there are legitimate
uses for this particular pointer conversion that can be used to
improve a design, and because shared_ptr must in any case remember a
deleter that will know to actually destroy a D, not a B. That's
because the original 'pd' may have been constructed with a custom
deleter, or with a pointer that's been allocated from a different heap
(if 'pd' crossed EXE/DLL boundaries); or, at pb.reset() time, B may be
an incomplete type.

Scott Meyers

unread,
Jun 11, 2004, 12:47:20 AM6/11/04
to
On Thu, 10 Jun 2004 23:32:54 +0000 (UTC), Peter Dimov wrote:
> Use...@aristeia.com (Scott Meyers) wrote in message news:<MPG.1b3169b56...@news.hevanet.com>...
> > On Wed, 9 Jun 2004 19:40:07 +0000 (UTC), David Abrahams wrote:
> > > I don't see how that's relevant to your claim that not using a virtual
> > > destructor in Base is inherently unsafe.
> >
> > It depends on what you mean by inherently unsafe. When you facilitate
> > something that might behave incorrectly and that is generally a sign of bad
> > design anyway, I'd call it inherently unsafe. In this case, we're talking
> > about facilitating the manipulation of a derived class object through a
> > base class interface lacking a virtual destructor, and in my mind, that's
> > generally a sign of bad design.
>
> If you had to justify that guideline, what would you say? Probably
> that it's easy to accidentally delete the object via a base class
> pointer and visit the undefined behavior land.

That would certainly be my first argument.

> But if the users are
> never expected or encouraged to use 'delete' (if they are, then that's
> generally a sign of bad design, if you ask me), or if the base class
> destructor is not accessible, that accidental deletion doesn't seem
> very likely.

Either the class has virtual functions or it doesn't. If it doesn't, then we
have not only delete that can go wrong but also RTTI. We also have to wonder
about a design lacking virtuals where it makes sense to use a base class
interface on a derived class object. Any such design would warrant a very
close look, IMO.

If the class does have virtuals, then we have to ask why it doesn't also have
a virtual destructor. That's strange right from the get-to, IMO -- again,
warranting a close design review. If the purpose is to prevent deletion
through the base class interface, a protected destructor will almost certainly
suffice without sending mixed signals as regards virtual functions.

But maybe I'm just out of the loop on current design techniques. What broad
class of generally reasonable designs uses manipulation of derived class
objects through base class interfaces lacking virtual destructors? Setting
aside shared_ptr<void> (which is a different issue entirely, IMO), what
programming constructs are facilitated by shared_ptr obviating the need for a
virtual destructor in the interface of the declared pointee type? What
motivates this feature?

As an unrelated question, if shared_ptr were to be added to C++0x, would it be
reasonable to expect that auto_ptr will be augmented to offer the same
behavior as regards deletion of the pointed-to-object?

Scott

David Abrahams

unread,
Jun 11, 2004, 12:05:18 PM6/11/04
to
Use...@aristeia.com (Scott Meyers) writes:

> But maybe I'm just out of the loop on current design techniques. What broad
> class of generally reasonable designs uses manipulation of derived class
> objects through base class interfaces lacking virtual destructors? Setting
> aside shared_ptr<void> (which is a different issue entirely, IMO), what
> programming constructs are facilitated by shared_ptr obviating the need for a
> virtual destructor in the interface of the declared pointee type? What
> motivates this feature?

I don't think you need a broad class of generally reasonable designs
to justify this feature. Even one or two designs where it's useful
are enough, because it costs practically nothing in design,
implementation, or documentation complexity to handle those cases
correctly and uniformly. In fact, making a special case of
shared_ptr<void> would complicate things immensely and make the design
harder to explain.

> As an unrelated question, if shared_ptr were to be added to C++0x, would it be
> reasonable to expect that auto_ptr will be augmented to offer the same
> behavior as regards deletion of the pointed-to-object?

I doubt it. That would in principle require effectively growing
auto_ptr by at least the size of a function pointer.

--
Dave Abrahams
Boost Consulting
http://www.boost-consulting.com

---

Alberto Barbati

unread,
Jun 11, 2004, 12:06:17 PM6/11/04
to
Scott Meyers wrote:
>
> But maybe I'm just out of the loop on current design techniques. What broad
> class of generally reasonable designs uses manipulation of derived class
> objects through base class interfaces lacking virtual destructors? Setting
> aside shared_ptr<void> (which is a different issue entirely, IMO), what
> programming constructs are facilitated by shared_ptr obviating the need for a
> virtual destructor in the interface of the declared pointee type? What
> motivates this feature?
>

From what I understand (very little, to be honest), the beauty and the
strenght of the design of the shared_ptr is indeed to allow the use of
incomplete types, void being a special case. Not only that, but it also
allows very important usage patterns such as the "pimpl" pattern and the
possibility to pass shared_ptr across DLL boundaries as the destruction
is under complete control at the construction site. IMHO, these features
alone are sufficient to justify the design. I consider the fact that you
can manipulate objects lacking virtual destructors as a pleasant
side-effect, not as the main feature.

> As an unrelated question, if shared_ptr were to be added to C++0x, would it be
> reasonable to expect that auto_ptr will be augmented to offer the same
> behavior as regards deletion of the pointed-to-object?

I don't think so. Such a behaviour comes at a cost and tipical auto_ptr
usage patterns may suffer the impact of such a change. Moreover, if we
augment the auto_ptr, it would become so similar to the shared_ptr that
there would be no reason to have two distinct templates.

Just my €0.02

Alberto

Peter Dimov

unread,
Jun 11, 2004, 12:11:50 PM6/11/04
to
Use...@aristeia.com (Scott Meyers) wrote in message news:<MPG.1b32d5b96...@news.hevanet.com>...

>
> Either the class has virtual functions or it doesn't. If it doesn't, then we
> have not only delete that can go wrong but also RTTI. We also have to wonder
> about a design lacking virtuals where it makes sense to use a base class
> interface on a derived class object. Any such design would warrant a very
> close look, IMO.

OK, let's tackle the non-polymorphic base first. If you have a pointer
pb to a non-polymorphic class B, it is entirely reasonable to expect
that typeid(*pb) would be typeid(B). No surprises here. You are right
to question the design, but the fact that shared_ptr will destroy the
derived class object correctly, a behavior that falls out from general
principles and is not specifically added to support this particular
case, is not inherently dangerous.

struct B { /* no virtuals */ };
shared_ptr<B> createB();

If createB(), for its own unfathomable reasons, returns a pointer to
something derived from B, nothing bad happens. You can happily pretend
that it returned a pointer to a plain B.

> If the class does have virtuals, then we have to ask why it doesn't also have
> a virtual destructor. That's strange right from the get-to, IMO -- again,
> warranting a close design review. If the purpose is to prevent deletion
> through the base class interface, a protected destructor will almost
> certainly suffice without sending mixed signals as regards virtual functions.

Exactly. Accessible/inaccessible and virtual/non-virtual are
orthogonal. It is true that having a destructor that is both public
and non-virtual is questionable, if not outright bad. But this does
not automatically mean that having a non-virtual destructor is bad. I
can just as easily omit the other part of the guideline and claim that
having a public destructor is bad, and I'll be about as right as you
are.

public+nonvirtual == evil; the other three alternatives are perfectly
fine. If the destructor is protected, it doesn't have to be virtual.

I've heard a stronger version of this claim, that a destructor should
be either public and virtual, or protected and nonvirtual, but I don't
agree; a protected virtual destructor is also useful when it's the
only virtual function in a class that is intended to only serve as a
common base, the polymorphic equivalent of void.

A protected nonvirtual destructor does not send mixed signals WRT
virtual functions. It has a clear meaning: 'delete p' is not an
operation that this particular interface implements. Just like not
having a function named 'f' has the obvious meaning that p->f() is not
an allowed operation. C++ doesn't provide us the means to omit the
destructor entirely, so the closest we can get is to make it
inaccessible. (However COM interfaces typically don't bother, and few
people make the mistake to 'delete' them, so even a public nonvirtual
is not that bad in practice as it appears in the literature.)

(As an aside, I believe that in a better C++, the implicitly generated
destructor of an abstract class should have been protected by
default.)

To get back on topic, in order to support inaccessible destructors, no
matter whether virtual or not, shared_ptr must necessarily not try to
invoke the base class destructor, which implies that a nonvirtual base
destructor will also "work" (i.e. be ignored) just as well.

> But maybe I'm just out of the loop on current design techniques. What broad
> class of generally reasonable designs uses manipulation of derived class
> objects through base class interfaces lacking virtual destructors? Setting
> aside shared_ptr<void> (which is a different issue entirely, IMO), what
> programming constructs are facilitated by shared_ptr obviating the need for a
> virtual destructor in the interface of the declared pointee type?

To summarize the above, the focus is not on virtual. shared_ptr simply
does not look at the declared pointee type. No special effort has been
made to support nonvirtual destructors; the virtuality of the
destructor does not matter.

> What motivates this feature?

In short, and I'll try to not repeat myself too much:

shared_ptr<T> p( q );

will call 'delete q' when the last reference goes away.

shared_ptr<T> p( q, f );

will call 'f(q)' when the last reference goes away. This is
independent of T or the declared type of the last remaining shared_ptr
instance that actually triggered the deletion.

In addition to enabling 'unusual' designs (protected or private
destructors, implementation hiding via incomplete classes), this
feature has the benefit of making shared_ptr-based code more easily
reviewable. You only need to look at the places where a shared_ptr is
constructed (or reset(...), which is a shorthand for construct+swap).
If shared_ptr's behavior depended on the declared type of the last
remaining instance, such a local analysis would be impossible in
general; you'd need to locate the last instance first.

The 'review' can be automatic, too; if there are tools that can verify
whether a pointer is a valid argument to 'delete', the implementor of
shared_ptr is allowed and encouraged by the specification to use them
at construction time to check the argument for validity.

> As an unrelated question, if shared_ptr were to be added to C++0x, would it
> be reasonable to expect that auto_ptr will be augmented to offer the same
> behavior as regards deletion of the pointed-to-object?

No, my opinion is that auto_ptr is a fundamentally different smart
pointer, with a different audience. An auto_ptr<X> is used to
exclusively hold a pointer to X that is guaranteed to be 'delete'-able
by the user, which is why release() is provided. shared_ptr<X> is
opaque; it knows how and when to dispose of the object it holds, but
the user, in general, does not. The deletion strategy was supplied and
captured at the time it was created.

Jorge Rivera

unread,
Jun 11, 2004, 3:24:52 PM6/11/04
to
> I doubt it. That would in principle require effectively growing
> auto_ptr by at least the size of a function pointer.
>

But it would provide users with the samre delete smarts.
This would prevent confusion...

EG
auto_ptr<B> apb(new D());
shared_ptr<B> apb(new D());

These two should both be equally safe or unsafe, you guys decide what
the decision is. Having one safely delete a B and not the other is
asking for confusion, IMO.

JLR

Bronek Kozicki

unread,
Jun 12, 2004, 2:19:53 PM6/12/04
to
On Fri, 11 Jun 2004 16:06:17 +0000 (UTC), Alberto Barbati wrote:
>> As an unrelated question, if shared_ptr were to be added to C++0x, would it be
>> reasonable to expect that auto_ptr will be augmented to offer the same
>> behavior as regards deletion of the pointed-to-object?
>
> I don't think so. Such a behaviour comes at a cost and tipical auto_ptr
> usage patterns may suffer the impact of such a change.

this cost could be much smaller than it is in shared_ptr. Take a loot at
http://b.kozicki.pl/cpp/ext_auto_ptr_090.zip - the only cost is size of
object - it contains 4 pointers (instead of one), but there are no extra
heap allocations and virtually no CPU cost - except extra indirection in
reset() .

> Moreover, if we
> augment the auto_ptr, it would become so similar to the shared_ptr that
> there would be no reason to have two distinct templates.

opposite to shared_ptr, auto_ptr does not have real copy semantics, and
I think that it's not going to change. It would be however nice if it
has real move semantics *instead* of flawed copy semantics. I think that
there's place for improvement or new utility (move_ptr, anyone?), if
(when) proposal N1377 is accepted. But that's much different story.


B.

0 new messages