Topic: [Defect Report] shared_ptr and nullptr


Author: Alberto Ganesh Barbati <AlbertoBarbati@libero.it>
Date: Tue, 6 Nov 2007 18:26:52 CST
Raw View
Greg Herlihy ha scritto:
>
> I think initializing a shared_ptr with a nullptr value should be equivalent
> to this code:
>
>     A* p = nullptr;
>     shared_ptr<A> ptr(p);

That's your opinion. I beg to differ and I explained why. What is your
rationale?

> Since only #1 would call the same constructor as the above code, #1 is the
> only reasonable interpretation of initializing a shared_ptr with a nullptr
> argument.

This is the only reasonable interpretation that supports your opinion.
As I disagree with your opinion, it's not the only reasonable
interpretation to me.

Regards,

Ganesh

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





Author: jgottman@carolina.rr.com (Joe Gottman)
Date: Wed, 7 Nov 2007 01:50:45 GMT
Raw View
I just realized something.  One of the major reasons for wanting to
create a shared_ptr from nullptr is to enable the following code to compile:

    shared_ptr<Foo> p = some_function();
    if (p != nullptr) do_something_with(p);


But shared_ptr's operator== is defined as follows:

template<class T, class U>
bool operator==(shared_ptr<T> const& a, shared_ptr<U> const& b);

So even if we do give shared_ptr a constructor that takes a nullptr,
will the above operator==() function get called?

We might have to define the following 4 functions:

    template <class T> bool operator==(const shared_ptr<T> &, nullptr_t);
    template <class T> bool operator!=(const shared_ptr<T> &, nullptr_t);
    template <class T> bool operator==(nullptr_t, const shared_ptr<T> &);
    template <class T> bool operator!=(nullptr_t, const shared_ptr<T> &);

Joe Gottman

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





Author: Alberto Ganesh Barbati <AlbertoBarbati@libero.it>
Date: Thu, 1 Nov 2007 10:06:37 CST
Raw View
Joe Gottman ha scritto:
>
> Proposed Resolution:
>
>    Add the following constructors to 20.6.6.2:
>
>    explicit shared_ptr(nullptr_t);

I believe that there is value in allowing this constructor for implicit
conversion. If it is explicit, it isn't very useful, IMHO. Why would one
prefer to write shared_ptr<SomeType>(nullptr) instead of the more
concise shared_ptr<SomeType>() or:

  shared_ptr<SomeType> ptr(nullptr);

instead of simply:

  shared_ptr<SomeType> ptr;

?

Moreover, I find it nice to be able to write:

  void foo(const shared_ptr<SomeType>& ptr);

  int bar()
  {
    foo(nullptr);
  }

without explicitly writing the conversion. The main reason for having
the constructor explicit is to avoid the shared_ptr take the ownership
of a regular pointer by mistake. However, in the case of a nullptr_t
there is no such danger.

If my argument has value, we may think of adding a similar non-explicit
constructor to unique_ptr<> as well.

>    template <class D> shared_ptr(nullptr_t, D d, A a);
>    template <class D, class A> shared_ptr(nullptr_t, D d, A a);

I assume there is a typo in the first one: "A a" should not be present.

Just my two eurocent,

Ganesh

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





Author: Alberto Ganesh Barbati <AlbertoBarbati@libero.it>
Date: Fri, 2 Nov 2007 09:37:53 CST
Raw View
Just a couple more remarks:

Joe Gottman ha scritto:
>
>    template <class D> shared_ptr(nullptr_t, D d);
>    template <class D, class A> shared_ptr<nullptr_t, D d, A a);
>
>      Requires: D shall be CopyConstructible. The copy constructor and
>      destructor of D shall not throw exceptions. The expression
>      d(nullptr) shall be well-formed, shall have well defined behavior,
>      and shall not throw exceptions. A shall be an allocator (20.1.2).
>      The copy constructor and destructor of A shall not throw
>      exceptions.
>      Effects: Constructs a shared_ptr object that owns deleter d.  The
>      second constructor shall use a copy of a to allocate memory for
>      internal use.
>      Postconditions: use_count() == 1 and get() == nullptr.
>      Throws: bad_alloc, or an implementation-defined exception when a
>      resource other than memory could not be obtained.
>      Exception safety: If an exception is thrown, d(nullptr) is called.

1) As the value of the (null) pointer is going to be eventually
converted to type T*, the expression that is going to be called in the
destructor will be d((T*)nullptr) rather than d(nullptr). Therefore the
expression that must be well-formed and that shall be called if an
exception is thrown should follow accordingly. This would make a
difference, for example, with this deleter:

  struct naive_deleter
  {
    template <class T>
    void operator()(T* ptr) const { delete ptr; }
  };

2) I actually like the "get() == nullptr" in the postcondition, but I
think we should keep a consistent style throughout all the clause. So
either we use the standard "get() == 0" here or we replace every "get()
== 0" to "get() == nullptr" all over the place. (For what it's worth, my
preference goes to the second option.)

Cheers,

Ganesh

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





Author: Greg Herlihy <greghe@mac.com>
Date: Fri, 2 Nov 2007 16:57:52 CST
Raw View
On 11/2/07 8:37 AM, in article UbDWi.161370$U01.1150275@twister1.libero.it,
"Alberto Ganesh Barbati" <AlbertoBarbati@libero.it> wrote:

> Just a couple more remarks:
>
> Joe Gottman ha scritto:
>>
>>    template <class D> shared_ptr(nullptr_t, D d);
>>    template <class D, class A> shared_ptr<nullptr_t, D d, A a);
>>
>>      Requires: D shall be CopyConstructible. The copy constructor and
>>      destructor of D shall not throw exceptions. The expression
>>      d(nullptr) shall be well-formed, shall have well defined behavior,
>>      and shall not throw exceptions. A shall be an allocator (20.1.2).
>>      The copy constructor and destructor of A shall not throw
>>      exceptions.
>>      Effects: Constructs a shared_ptr object that owns deleter d.  The
>>      second constructor shall use a copy of a to allocate memory for
>>      internal use.
>>      Postconditions: use_count() == 1 and get() == nullptr.
>>      Throws: bad_alloc, or an implementation-defined exception when a
>>      resource other than memory could not be obtained.
>>      Exception safety: If an exception is thrown, d(nullptr) is called.
>
> 1) As the value of the (null) pointer is going to be eventually
> converted to type T*, the expression that is going to be called in the
> destructor will be d((T*)nullptr) rather than d(nullptr).

There is nothing in the proposed shared_ptr interface that suggests that a
nullptr initializer is ever converted to any particular pointer type. After
all, becauser nullptr has its own overloaded constructor, no type deduction
is performed with the nullptr (as is performed with other pointer
arguments), so it is logical to conclude that nullptr is just a nullptr_t
type.

In fact, one reason that I suggested a shared_ptr(T*) constructor overload -
was precisely to "convert" a nullptr argument to a T* null pointer constant.
Because once this conversion is performed, shared_ptr can treat nullptr just
as it treats any other stored pointer value.

> Therefore the
> expression that must be well-formed and that shall be called if an
> exception is thrown should follow accordingly. This would make a
> difference, for example, with this deleter:
>
>   struct naive_deleter
>   {
>     template <class T>
>     void operator()(T* ptr) const { delete ptr; }
>   };

I am a somewhat embarrassed to be this na   ve, but I must admit that I just
don't see a problem with naive_deleter. Could you please elaborate? (Well,
there is a potential problem in that naive_deleter().(nullptr) will not
compile - because no type can be deduced for "T". But presumably shared_ptr
would avoid this issue by calling naive_deleter().<T*>(nullptr) explicitly.)

> 2) I actually like the "get() == nullptr" in the postcondition, but I
> think we should keep a consistent style throughout all the clause. So
> either we use the standard "get() == 0" here or we replace every "get()
> == 0" to "get() == nullptr" all over the place. (For what it's worth, my
> preference goes to the second option.)

I think it is a good idea to replace 0 with nullptr wherever appropriate. I
think that nullptr works best as a "universally-compatible" null pointer
constant. The problem with the proposed nullptr_t overloads in shared_ptr's
interface - is that nullptr_t is not appearing in the context of any
specific pointer type, but has instead become a type unto itself. Yet
shared_ptr never cared to support initialization with the old "0" null
pointer constant (and still would not support "0" even with these changes).
So why should shared_ptr now implement an entire set of routines - just
because "0" has been renamed?

One explanation might be that there is something "special" about nullptr
that requires such special treatment. But a C++ programmer should think of
nullptr_t as some new, special type - one that will require adding entire
sets of routines just to support it. Instead a C++ programmer should view
"nullptr" as the null pointer constant with a more sensible name (and
slightly better behavior) than the old null pointer constant. But otherwise
there is not all that much difference between the two.

So it seems to me that - instead of having shared_ptr declare all of these
nullptr_t overloads that elevate nullptr' status - shared_ptr should do the
opposite: treat any nullptr argument as if it were (T*) 0 argument instead.
In other words, shared_ptr should treat nullptr just as it treats any other
stored pointer value. So all that would need to be added to the Standard
would be a paragraph to that effect. For example:

"An implementation shall ensure (by unspecified means) that a nullptr
argument passed as a pointer parameter to a shared_ptr<T> method - will
produce the same observable behavior as if a (T*)nullptr argument had been
passed in its place. [Note: an implementation might overload existing
shared_ptr methods that accept a template pointer type parameter with an
method that accepts T* parameter instead. This overload would then forward
its calls to the usual method while specifying T* as the type of the nullptr
argument.]"

Greg


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





Author: AlbertoBarbati@libero.it (Alberto Ganesh Barbati)
Date: Mon, 5 Nov 2007 17:52:47 GMT
Raw View
Greg Herlihy ha scritto:
> On 11/2/07 8:37 AM, in article UbDWi.161370$U01.1150275@twister1.libero=
.it,
> "Alberto Ganesh Barbati" <AlbertoBarbati@libero.it> wrote:
>=20
>> Therefore the
>> expression that must be well-formed and that shall be called if an
>> exception is thrown should follow accordingly. This would make a
>> difference, for example, with this deleter:
>>
>>   struct naive_deleter
>>   {
>>     template <class T>
>>     void operator()(T* ptr) const { delete ptr; }
>>   };
>=20
> I am a somewhat embarrassed to be this na=C3=AFve, but I must admit tha=
t I just
> don't see a problem with naive_deleter. Could you please elaborate? (We=
ll,
> there is a potential problem in that naive_deleter().(nullptr) will not
> compile - because no type can be deduced for "T". But presumably shared=
_ptr
> would avoid this issue by calling naive_deleter().<T*>(nullptr) explici=
tly.)

Your "potential problem" is precisely the point. Suppose you write this:

  struct A {};
  struct B : A {};

  shared_ptr<A> p1(new A, naive_deleter()); // #1
  shared_ptr<A> p2(new B, naive_deleter()); // #2
  shared_ptr<A> p3(nullptr, naive_deleter()); // #3

#1 will eventually call naive_deleter::operator(A*)
#2 will eventually call naive_deleter::operator(B*)
Assuming the OP proposal, #3 will fail because T cannot be deduced while
instantiating naive_deleter::operator(T*).

Now the question is: do we actually want that or wouldn't it be
preferable to make #3 call naive_deleter::operator(A*)? Now that I think
about it in these terms, I have to admit that I can't decide.

BTW, shared_ptr can *not* call naive_deleter().<T*>(nullptr) explicitly,
because it can not know in advance that operator() is a template.
However it might call naive_deleter()((T*)nullptr)).

>> 2) I actually like the "get() =3D=3D nullptr" in the postcondition, bu=
t I
>> think we should keep a consistent style throughout all the clause. So
>> either we use the standard "get() =3D=3D 0" here or we replace every "=
get()
>> =3D=3D 0" to "get() =3D=3D nullptr" all over the place. (For what it's=
 worth, my
>> preference goes to the second option.)
>=20
> I think it is a good idea to replace 0 with nullptr wherever appropriat=
e. I
> think that nullptr works best as a "universally-compatible" null pointe=
r
> constant. The problem with the proposed nullptr_t overloads in shared_p=
tr's
> interface - is that nullptr_t is not appearing in the context of any
> specific pointer type, but has instead become a type unto itself. Yet
> shared_ptr never cared to support initialization with the old "0" null
> pointer constant (and still would not support "0" even with these chang=
es).
> So why should shared_ptr now implement an entire set of routines - just
> because "0" has been renamed?

Because it has not just been renamed. nullptr is not just a special name
for 0, the former is a pointer (with an unspecified pointee type), while
the latter is an integer (that happens to be convertible to a pointer
type in some contexts).

> One explanation might be that there is something "special" about nullpt=
r
> that requires such special treatment. But a C++ programmer should think=
 of
> nullptr_t as some new, special type - one that will require adding enti=
re
> sets of routines just to support it. Instead a C++ programmer should vi=
ew
> "nullptr" as the null pointer constant with a more sensible name (and
> slightly better behavior) than the old null pointer constant. But other=
wise
> there is not all that much difference between the two.
>=20
> So it seems to me that - instead of having shared_ptr declare all of th=
ese
> nullptr_t overloads that elevate nullptr' status - shared_ptr should do=
 the
> opposite: treat any nullptr argument as if it were (T*) 0 argument inst=
ead.
> In other words, shared_ptr should treat nullptr just as it treats any o=
ther
> stored pointer value. So all that would need to be added to the Standar=
d
> would be a paragraph to that effect. For example:
>=20
> "An implementation shall ensure (by unspecified means) that a nullptr
> argument passed as a pointer parameter to a shared_ptr<T> method - will
> produce the same observable behavior as if a (T*)nullptr argument had b=
een
> passed in its place. [Note: an implementation might overload existing
> shared_ptr methods that accept a template pointer type parameter with a=
n
> method that accepts T* parameter instead. This overload would then forw=
ard
> its calls to the usual method while specifying T* as the type of the nu=
llptr
> argument.]"
>=20

The question is if

  shared_ptr<A> ptr(nullptr);

should be equivalent to

  shared_ptr<A> ptr((A*)0); // #1

or rather to

  shared_ptr<A> ptr; // #2

This isn't trivial, because #1 might need to allocate heap memory for
the deleter and therefore might throw, while #2 won't. My personal
preference definitely goes to #2. Moreover, constructor #1 is and shall
remain explicit, yet I believe that the implicit conversion from nullptr
should be allowed.

If you don't like the extra signatures, we could stress the equivalence
by changing the signature of the default constructor instead, thus
replacing:

   shared_ptr();

with

   shared_ptr(nullptr_t =3D nullptr);

As for the two-params and three-params constructors, the argument
doesn't hold, because the deleter must be allocated anyway. I still
haven't made up my mind on those.

As for the three reset() signatures with parameters, they are defined in
terms of the constructors, so instead of adding new signatures, we could
simply replace the "old" ones with:

  template<class Y> void reset(Y p);
  template<class Y, class D> void reset(Y p, D d);
  template<class Y, class D, class A> void reset(Y p, D d, A a);

where the type of the first parameter is Y instead of Y*. However, given
that the requirements on the deleter type are going to be reduced from
CopyConstructible to MoveConstructible and that passing deleter and
allocator by value might require an unneeded copy, I would go even
further and replace all four signatures (including the one without
parameters) with just one:

  template<class... Args> void reset(Args&&... args);

defined to have the same effect as:

  shared_ptr(std::forward<Args>(args)...).swap(*this);

Just my opinion,

Ganesh

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





Author: greghe@mac.com (Greg Herlihy)
Date: Tue, 6 Nov 2007 15:58:07 GMT
Raw View


On 11/5/07 9:52 AM, in article 74RWi.162147$U01.1154008@twister1.libero.it,
"Alberto Ganesh Barbati" <AlbertoBarbati@libero.it> wrote:

> Greg Herlihy ha scritto:
>>
>> I think it is a good idea to replace 0 with nullptr wherever appropriate. I
>> think that nullptr works best as a "universally-compatible" null pointer
>> constant. The problem with the proposed nullptr_t overloads in shared_ptr's
>> interface - is that nullptr_t is not appearing in the context of any
>> specific pointer type, but has instead become a type unto itself. Yet
>> shared_ptr never cared to support initialization with the old "0" null
>> pointer constant (and still would not support "0" even with these changes).
>> So why should shared_ptr now implement an entire set of routines - just
>> because "0" has been renamed?
>
> Because it has not just been renamed. nullptr is not just a special name
> for 0, the former is a pointer (with an unspecified pointee type), while
> the latter is an integer (that happens to be convertible to a pointer
> type in some contexts).

No, "0" in C++ really does express two distinct concepts (an integer
constant and a null pointer constant). Now, whether "0" expresses both
concepts at once (the "0 duality" school of thought) or expresses either one
concept or the other - depending on its context (the "dual 0s" line of
reasoning) is a question that has long been debated. Fortunately, the
nullptr proposal itself authoritatively answers this question (and thereby
shuts the door on any future debate):

"0 is always both an integer constant and a null pointer constant, except
when it's not."

> The question is if
>
>   shared_ptr<A> ptr(nullptr);
>
> should be equivalent to
>
>   shared_ptr<A> ptr((A*)0); // #1
>
> or rather to
>
>   shared_ptr<A> ptr; // #2
>

I think initializing a shared_ptr with a nullptr value should be equivalent
to this code:

    A* p = nullptr;
    shared_ptr<A> ptr(p);

Since only #1 would call the same constructor as the above code, #1 is the
only reasonable interpretation of initializing a shared_ptr with a nullptr
argument.

Greg



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





Author: Joe Gottman <jgottman@carolina.rr.com>
Date: Wed, 31 Oct 2007 10:48:58 CST
Raw View
   Consider the following program:

   struct Base {};
   struct Derived : public Base {};
   int main() {
      shared_ptr<Base> p(nullptr); // Which constructor will be called?
      return 0;
    }

This program will fail to compile because shared_ptr uses the following
template constructor to construct itself from pointers:

template <class Y> shared_ptr(Y *);

There are two equally valid conversions for nullptr_t: Base * or Derived
*, so the conversion from nullptr_t is ambiguous.  There are similar
problems with the constructors that take a pointer and a deleter or a
pointer, a deleter and an allocator, as well as the corresponding forms
of reset().  Note that N2435 will solve this problem for constructing
from just nullptr, but not for constructors that use deleters or
allocators or for reset().

Proposed Resolution:

    Add the following constructors to 20.6.6.2:

    explicit shared_ptr(nullptr_t);
    template <class D> shared_ptr(nullptr_t, D d, A a);
    template <class D, class A> shared_ptr(nullptr_t, D d, A a);

    Add the following methods to 20.6.6.2:

    void reset(nullptr_t);
    template <class D> void reset(nullptr_t, D d);
    template <class D, class A> void reset(nullptr_t, D d, A a);


Add the following constructor definitions to 20.6.6.2.1:

    explicit shared_ptr(nullptr_t);
      Effects: Constructs an empty shared_ptr object.
      Postconditions: use_count() == 0 && get() == 0.
      Throws: nothing.


    template <class D> shared_ptr(nullptr_t, D d);
    template <class D, class A> shared_ptr<nullptr_t, D d, A a);

      Requires: D shall be CopyConstructible. The copy constructor and
      destructor of D shall not throw exceptions. The expression
      d(nullptr) shall be well-formed, shall have well defined behavior,
      and shall not throw exceptions. A shall be an allocator (20.1.2).
      The copy constructor and destructor of A shall not throw
      exceptions.
      Effects: Constructs a shared_ptr object that owns deleter d.  The
      second constructor shall use a copy of a to allocate memory for
      internal use.
      Postconditions: use_count() == 1 and get() == nullptr.
      Throws: bad_alloc, or an implementation-defined exception when a
      resource other than memory could not be obtained.
      Exception safety: If an exception is thrown, d(nullptr) is called.

Add the following method definitions to 20.6.6.2.4:

    void reset(nullptr_t);
    Effects: Equivalent to shared_ptr(nullptr).swap(*this).

    template <class D> void reset(nullptr_t, const D d)
    Effects: Equivalent to shared_ptr(nullptr, d).swap(*this).

    template <class D, class A> void reset(nullptr_t, D d, A a);
    Effects: Equivalent to shared_ptr(nullptr, d, a).swap(*this).


Joe Gottman

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