30

OK, so first some things that might be relevant:

I'm using the Clang 3.1 compiler, in C++11 mode, with the standard library set to libc++.

I'm trying to familiarize myself with C++11, and in so doing I ran across behavior that seems odd. It may be a quirk of Clang or libc++ but I can't speak C++ standardese and I have no access to other compilers with C++11 support so I can't really check it, and I've searched the internet and Stack Overflow to the best of my ability without finding anything related...so here we go:

When using shared_ptr / unique_ptr to implement RAII for a simple resource, it seems that their behavior differs with respect to null pointers upon deletion. I realize that normally it's not necessary to delete a null pointer, but I had expected the behavior to at least match between the two STL smart pointers.

For the specific case, consider the following code:

{
    auto Deleter = [](void *){cout << "It's later!" << endl;};
    shared_ptr<void> spDoSomethingLater(nullptr, Deleter);
    unique_ptr<void, void (*)(void *)> upDoSomethingLater(nullptr, Deleter);
    cout << "It's now!" << endl;
}

I would have expected one of the following outputs from this:

a) If both deleters are called even though the pointer is null:

"It's now!"
"It's later!"
"It's later!"

b) If neither deleter is called because the pointer is null:

"It's now!"

But I observe neither of these cases. Instead, I observe:

"It's now!"
"It's later!"

Which means one but not the other of the deleters is being called. Upon further investigation, I found that the deleter for shared_ptr is called regardless of whether it holds a null value, but unique_ptr's deleter is only called if it does not hold a null value.

My questions: Is this actually the correct behavior as specified by the standard? If so, why does the specified behavior differ between the two STL types in this manner? If not, is this a bug I should report to libc++?

R. Martinho Fernandes
  • 209,766
  • 68
  • 412
  • 492
Bob Miller
  • 572
  • 3
  • 11

2 Answers2

28

The observed behavior is in accordance with the standard.

For unique_ptr, 20.7.1.2.2/2 (destructor effects) says

Effects: If get() == nullptr there are no effects. Otherwise get_deleter()(get()).

For shared_ptr, 20.7.2.2.2/1 says that the deleter should be called even if it wraps the null pointer:

Effects:

  • If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects.
  • Otherwise, if *this owns an object p and a deleter d, d(p) is called.
  • Otherwise, *this owns a pointer p, and delete p is called.

The important detail here is the expression "owns an object p". 20.7.2.2/1 says that "a shared_ptr object is empty if it does not own a pointer". 20.7.2.2.1/9 (the relevant constructor) says that it "constructs a shared_ptr object that owns the object p and the deleter d".

So as far as I can tell, that invocation technically makes the shared_ptr own the null pointer, which results in the deleter being called. Contrast this with the parameterless constructor which is said to leave the shared_ptr "empty".

Jon
  • 396,160
  • 71
  • 697
  • 768
  • 1
    Thank you for your speedy and helpful reply. I'm still unsure as to why the standard would intend this behavior to differ, but you have definitely answered the part of my question I was most concerned with. – Bob Miller Jun 22 '12 at 21:42
  • 1
    @BobMiller: Unfortunately I cannot offer any insight into why exactly it was specified to work like that. – Jon Jun 22 '12 at 21:43
  • 1
    -1 it's incorrect. the quoted text makes a fine distinction between "object" and "pointer". an "object", in the standard's meaning, is a region of memory with an associated type; it is what a pointer points to. – Cheers and hth. - Alf Jun 22 '12 at 21:47
  • 4
    @Alf: In a shared_ptr, an object `p` is anything convertible to `T*`. Check, for example, §20.7.2.2.1/9. – kennytm Jun 22 '12 at 21:49
  • 1
    @Cheersandhth.-Alf: But 20.7.2.2.1/9 which I quote clearly applies to the constructor that takes `nullptr_t` as the first argument. Isn't that correct? – Jon Jun 22 '12 at 21:51
  • 1
    @Jon: nope, but i think maybe we're onto a defect in the wording here. the wording applies to FOUR DIFFERENT constructors, listed above. two of them take a `nullptr`. the *owns* refers to the other two constructors, because one cannot own a nullpointer. ownership is for a pointed to object, and is the obligation to destroy that object. – Cheers and hth. - Alf Jun 22 '12 at 21:56
  • @KennyTM: sorry, i don't understand your comment. – Cheers and hth. - Alf Jun 22 '12 at 21:57
  • 1
    @Cheersandhth.-Alf: What about the text regarding the destructor? Point #2 says "owns an object" and point #3 says "owns a pointer". Do you suggest that the wording intentionally specifies that `delete` is called on null pointers but a user-specified deleter is *not* called on the same? – Jon Jun 22 '12 at 22:01
  • 3
    @Jon: Sorry, I was wrong. `shared_ptr p;` and `shared_ptr p(0);` do indeed have different effect, with both compilers I tried (Visual C++ and g++). Who would have thunked. I tried to remove the downvote but SO won't allow that; could you make a little edit? – Cheers and hth. - Alf Jun 22 '12 at 22:05
10

Yes it is the correct behavior.

§20.7.1.2.2[unique.ptr.single.dtor]/2:

unique_ptr destructor

Effects: If get() == nullptr there are no effects. Otherwise get_deleter()(get()).

§20.7.2.2.2[util.smartptr.shared.dest]/1:

shared_ptr destructor

Effects:

  • If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects.
  • Otherwise, if *this owns an object p and a deleter d, d(p) is called.
  • Otherwise, *this owns a pointer p, and delete p is called.

So there should be no effect since the shared_ptr is empty? Wrong, because providing the a pointer makes the shared_ptr not empty, even if the pointer is null.

§20.7.2.2.1[util.smartptr.shared.const]/8–10

shared_ptr constructors

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

Requires: p shall be convertible to T*. …

Effects: Constructs a shared_ptr object that owns the object p and the deleter d.

Postconditions: use_count() == 1 && get() == p.

This means the shared_ptr owns the nullptr. Thus the deleter will be called when the shared_ptr is destroyed.

kennytm
  • 469,458
  • 94
  • 1,022
  • 977
  • 1
    I see. Thank you also for your reply (sorry that Jon appears to have beaten you to the punch). You and Jon definitely know your stuff! – Bob Miller Jun 22 '12 at 21:46