6

See this example :

#include <iostream>
#include <memory>

class Foo {
public:
    Foo()  { std::cout << "Foo()\n";  }
    ~Foo() { std::cout << "~Foo()\n"; }
};

int main(){
    auto deleter = [](Foo* p) {
        if(!p) { std::cout << "Calling deleter on nullptr\n"; }
        delete p;
    };

    std::shared_ptr<Foo> foo;
    std::cout << "\nWith non-null Foo:\n";
    foo = std::shared_ptr<Foo>(new Foo, deleter);
    std::cout << "foo is " << (foo ? "not ":"") << "null\n";
    std::cout << "use count=" << foo.use_count() << '\n';
    foo.reset();

    std::cout << "\nWith nullptr and deleter:\n";
    foo = std::shared_ptr<Foo>(nullptr, deleter);
    std::cout << "foo is " << (foo ? "not ":"") << "null\n";
    std::cout << "use count=" << foo.use_count() << '\n';
    foo.reset();

    std::cout << "\nWith nullptr, without deleter:\n";
    foo = std::shared_ptr<Foo>(nullptr);
    std::cout << "foo is " << (foo ? "not ":"") << "null\n";
    std::cout << "use count=" << foo.use_count() << '\n';
    foo.reset();
}

The output is :

With non-null Foo:
Foo()
foo is not null
use count=1
~Foo()

With nullptr and deleter:
foo is null
use count=1
Calling deleter on nullptr

With nullptr, without deleter:
foo is null
use count=0

Here we see that shared_ptr calls the contained deleter when it is initialized with nullptr and a custom deleter. It seems that, when initialized with a custom deleter, shared_ptr considers it is "owning" nullptr and thus tries to delete it when it would delete any other owned pointer. Though it does not happen when no deleter is specified.

Is this intended behavior ? If so, what is the reason behind this behavior ?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Annyo
  • 1,387
  • 9
  • 20
  • 1
    `delete(nullptr)` is, afaik, completely valid, so it makes sense that, for any value given to a `shared_ptr`, it'll call the deleter it's given, without wasting cycles checking if it's `nullptr` – Phil M Mar 26 '19 at 16:21
  • 6
    `Unlike std::unique_ptr, the deleter of std::shared_ptr is invoked even if the managed pointer is null.` [Source](http://en.cppreference.com/w/cpp/memory/shared_ptr/~shared_ptr) – Samer Tufail Mar 26 '19 at 16:23
  • related: https://stackoverflow.com/questions/27962978/boost-smart-pointer-with-custom-deleter – Richard Hodges Mar 26 '19 at 16:23
  • @PhilM That's actually not [what the standard says](http://eel.is/c++draft/util.smartptr.shared#dest-1.2), though by the as-if rule (when no deleter is given) it may do as you describe – Lightness Races in Orbit Mar 26 '19 at 16:25
  • Hmm, just realised https://stackoverflow.com/a/11164463/560648 is probably a dupe – Lightness Races in Orbit Mar 26 '19 at 16:34
  • @LightnessRacesinOrbit This question's answer also partially answers my question (that is, yes, it is intended), though I still think this is a different question. Also, it does not tell me why this behavior is intended. – Annyo Mar 26 '19 at 16:52
  • http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1450.html isn't vastly helpful in that regard – Lightness Races in Orbit Mar 26 '19 at 16:56

1 Answers1

6

tl;dr: Yes, it's intended.


This is pretty subtle.

A shared_ptr can be in two states:

Constructing a shared_ptr with a null pointer actually leads to it being not-empty! get() returning p means get() returning nullptr, but that doesn't make it empty.

Since the default deleter just does delete p, and delete nullptr is a no-op, this doesn't usually matter. But, as you have seen, you can observe this difference if you provide your own deleter.

I don't know exactly why this is. On the one hand I can see a case for preventing a deleter from being invoked in the nullptr case because one generally considers a shared_ptr(nullptr) to be "empty" (even though it technically is not); on the other hand, I can see a case for letting the deleter make this decision (with the accompanying overhead of a branch) if it wants to.

You're right to include a check for null here.


Some legalese from [util.smartptr.shared.const]:

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);

9) Requires: Construction of d and a deleter of type D initialized with std::move(d) shall not throw exceptions. The expression d(p) shall have well-defined behavior and shall not throw exceptions. A shall satisfy the Cpp17Allocator requirements (Table 34).

10) Effects: Constructs a shared_­ptr object that owns the object p and the deleter d. When T is not an array type, the first and second constructors enable shared_­from_­this with p. The second and fourth constructors shall use a copy of a to allocate memory for internal use. If an exception is thrown, d(p) is called.

11) Ensures: use_­count() == 1 && get() == p.

(Notice that there is no exemption for the case that !p.)

And from [util.smartptr.shared.dest]:

~shared_ptr();

1) 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.

Sidenote: I think the confusion between the phrases "owns an object" and "owns a pointer" in the above passages is an editorial problem.


We can also see this documented on cppreference.com's ~shared_ptr article:

Unlike std::unique_ptr, the deleter of std::shared_ptr is invoked even if the managed pointer is null.

(Please use documentation!)

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • You're an exemption. – Hatted Rooster Mar 26 '19 at 16:31
  • `template shared_ptr(Y* p, D d);` mandates that `delete p` is a valid op and by defeault `delete nullptr` is. If the custom deleter can't handle that then it doesn't make sense to pass `nullptr` to `sharedptr` – Samer Tufail Mar 26 '19 at 16:36
  • an empty `shared_ptr` may have a non-null stored pointer if the aliasing constructor was used to create it - see http://eel.is/c++draft/util.smartptr.shared#const-17 – yachoor Mar 26 '19 at 16:37
  • @SamerTufail Correct. – Lightness Races in Orbit Mar 26 '19 at 16:37
  • A null `shared_ptr` may be used to execute arbitrary code: `shared_ptr guard(static_cast(0), bind(f, x, y))` - from https://www.boost.org/doc/libs/1_69_0/libs/smart_ptr/doc/html/smart_ptr.html#techniques_using_shared_ptr_to_execute_code_on_block_exit – yachoor Mar 26 '19 at 16:39
  • Sorry, I did use the documentation, but I did not think to look at the destructor, since the deletor seemed to be called in `reset()` I only looked this page and did not see anything. I also made the wrong assumption that a `shared_ptr` pointing to nullptr was empty. I'm pretty close to accept this answer, but if you can find a reason why shared_ptr does not check if it holds nullptr, it would be perfect. (I'm just being curious there, because unique_ptr does check that) – Annyo Mar 26 '19 at 17:00
  • @Annyo I've given it a go but to be honest I don't think I'll be able to. Hopefully someone with more time can give it a better go and get luckier! – Lightness Races in Orbit Mar 26 '19 at 18:16