1

I know the object managed by a std::shared_ptr is not deleted by reset() unless it is the only shared_ptr that manages it at that point. I know that when there are multiple shared_ptrs managing the same object, changes to the managed object’s value are reflected through all shared_ptrs that point to it, while changes to any of these shared_ptr’s value (not its managed object’s value) caused by reset()ting it (i.e. changing the shared_ptr from one that points to the original managed object to one that points to nothing or something else) does not change the other shared_ptrs’ values (i.e. they all still point to the original managed object, and the original managed object still exists):

#include <memory>
#include <vector>
#include <iostream>
using namespace std;
int main() {
    vector<shared_ptr<int>> vec{ make_shared<int>(5) };
    shared_ptr<int> sptr(vec[0]);
    ++ *sptr;
    cout << *vec[0] << endl; // 6
    vec[0].reset();
    vec.pop_back();
    cout << *sptr << endl;   // 6
}

But that logic is lost to me when using two levels of indirection. Given a class named Wrapper and a shared_ptr<shared_ptr<Wrapper>> and any number of other shared_ptr<shared_ptr<Wrapper>>s initialized to the prior, why does this configuration allow a reset() called on any inner shared_ptr to effectively reset() all other inner shared_ptrs?

My guess is: the managed object of any of the outer shared_ptrs is the inner shared_ptr (not the Wrapper) and changes to the value of the inner shared_ptr (by reset()ting the inner shared_ptr, which changes the value of the inner shared_ptr from one that points to a Wrapper instance to one that points to nothing) is reflected throughout all outer shared_ptrs, effectively causing all outer shared_ptrs to lose indirect management over the Wrapper instance, thereby deleting the Wrapper instance.

But by the same logic, isn’t resetting one of the inner pointers only going to cause that particular inner pointer to lose management over the Wrapper? Given that all the other outer pointers point to inner pointers of their own (i.e. the ones that were constructed with them), wouldn’t those outer ones continue to have indirect management over the Wrapper, since resetting one inner pointer doesn’t change the Wrapper’s value, which should still be accessible by the other inner pointers? It’s a paradox to me.

If resetting one inner pointer effectively resets all of them, then that means the inner pointers' use_count() was 1 right before the reset(). The only way I thought multiple shared_ptrs can appear to manage the same object while keeping use_count() at 1 would be through illusion: they manage different objects (i.e. objects at different addresses) that have the same value. I tested this by making an int wrapper named Wrapper, whose only data members are the wrapped int and a static instance_count that keeps track of the number of Wrapper instances that currently exist.

struct Wrapper {
    Wrapper(int par = 0) : num(par) { ++instance_count; }
    Wrapper(const Wrapper& src) : num(src.num) { ++instance_count; }
    ~Wrapper() { --instance_count; }
    int num;
    static int instance_count;
};
int Wrapper::instance_count = 0;

int main() {
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_1(
        make_shared<shared_ptr<Wrapper>>(
            make_shared<Wrapper>(Wrapper(5))
        )
    );
                                                            // - Output -
    cout << Wrapper::instance_count << endl;                // 1
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_2(dual_ptr_1);
    cout << Wrapper::instance_count << endl;                // 1
    cout << dual_ptr_1->use_count() << endl;                // 1
    cout << dual_ptr_2->use_count() << endl;                // 1
    cout << dual_ptr_1.use_count() << endl;                 // 2
    cout << dual_ptr_2.use_count() << endl;                 // 2
    // note that above, the '->' operator accesses
    // inner ptr while '.' operator is for outer ptr
    cout << (*dual_ptr_1)->num << endl;                     // 5
    cout << (*dual_ptr_2)->num << endl;                     // 5
    dual_ptr_2->reset();
    cout << Wrapper::instance_count << endl;                // 0
    cout << dual_ptr_1->use_count() << endl;                // 0
    cout << dual_ptr_2->use_count() << endl;                // 0
    cout << dual_ptr_1.use_count() << endl;                 // 2
    cout << dual_ptr_2.use_count() << endl;                 // 2
}

Apparently there were 2 inner pointers that point to 1 Wrapper object; the inner pointers' use_count was at most 1 (prior to destruction); the Wrapper class's instance_count was at most 1 (prior to destruction); and the indirectly managed Wrapper object was accessible through both outer pointers (which means neither outer pointer got move-constructed by the other); and resetting one inner pointer effectively reset all of them; so I still don't understand the seeming paradox.

I'm also asking the same questions in this post about the case in which the above code has the inner shared_ptrs replaced by unique_ptrs, the inner make_shared replaced by make_unique, and the use_count() commented out for the inner pointers (because unique_ptrs lack that method), which gives the same output. That's a seeming paradox to me because the unique_ptrs don't seem unique here.

CodeBricks
  • 1,771
  • 3
  • 17
  • 37
  • 1
    I'm not sure what you were expecting, `dual_ptr_1` and `dual_ptr_2` both share ownership of the single inner `shared_ptr` , which in turn manages a single instance of `Wrapper`. `dual_ptr_2->reset();` destroys the single `Wrapper` instance so you're left with shared ownership of an empty `shared_ptr`. – user657267 Nov 05 '14 at 02:30
  • As everyone else is saying. Think of it like this: `Wrapper * w = new Wrapper(); Wrapper ** a = &w; Wrapper ** b = &w; delete *a; *a = nullptr;` <-- Would you be surprised that `*b` is now null? – cdhowie Nov 05 '14 at 03:31

2 Answers2

3

Given a class named Wrapper and a shared_ptr<shared_ptr<Wrapper>> and any number of other shared_ptr<shared_ptr<Wrapper>>s initialized to the prior, why does this configuration allow a reset() called on any inner shared_ptr to effectively reset() all other inner shared_ptrs?

There are no other inner shared_ptrs, you have a single instance of the contained object, i.e.

dual_ptr_1
          \
           --> shared_ptr --> Wrapper
          /
dual_ptr_2

And not

dual_ptr_1 --> shared_ptr 
                         \
                          --> Wrapper
                         /
dual_ptr_2 --> shared_ptr 

After your call to dual_ptr_2->reset(); this changes to

dual_ptr_1
          \
           --> shared_ptr --> (empty)
          /
dual_ptr_2
user657267
  • 20,568
  • 5
  • 58
  • 77
1

The accepted answer shows with a diagram what happens in the OP code: two outer shared_ptrs point to the same inner shared_ptr, which points to the Wrapper object. (I refer to the diagrams in the unedited version of the accepted answer; it hasn’t been edited at the time of my answer here.) The accepted answer has another diagram, which shows what the OP expected to happen but didn’t happen, which I refer to as:

Case A – two outer pointers that point to different inner pointers that point to the same Wrapper object (refer to accepted answer for diagram).

Here’s code that causes Case A:

int main() {
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_1(
        make_shared<shared_ptr<Wrapper>>(make_shared<Wrapper>(5))
    );
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_2(
        make_shared<shared_ptr<Wrapper>>(*dual_ptr_1)
    );
    cout << dual_ptr_1.use_count() << endl;  // 1
    cout << dual_ptr_2.use_count() << endl;  // 1
    cout << dual_ptr_1->use_count() << endl; // 2
    cout << dual_ptr_2->use_count() << endl; // 2
}

I refer to dual_ptr_1 as the first outer pointer and the shared_ptr to which it points as the first inner pointer. I refer to dual_ptr_2 as the second outer pointer and the shared_ptr to which it points as the second inner pointer. The two outer pointers point to different inner pointers. The second outer pointer was not copy-constructed or assigned to the first outer pointer, so either outer pointer’s use_count is 1. The second outer pointer does not point to the first inner pointer, but rather a nameless inner pointer that's copy-constructed from the first inner pointer. While the second outer pointer is still managing the second inner pointer, the latter’s namelessness doesn’t cause the latter to go out of scope. The second inner pointer points to the same Wrapper as the first inner pointer, because the second inner pointer was copy-constructed from the first inner pointer. Because of this shared_ptr copy-construction, either inner pointer’s use_count is 2. Each inner pointer must be reset() to nothing or something else, assigned to something else, or go out of scope in order for the Wrapper to be destroyed (the inner pointers don’t both need to go through the same operation as long as each one goes through at least one of them).

Here’s another one, Case B – same diagram as Case A, but with a buggy implementation and different console output:

int main() {
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_1(
        make_shared<shared_ptr<Wrapper>>(make_shared<Wrapper>(5))
    );
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_2(
        make_shared<shared_ptr<Wrapper>>(&(**dual_ptr_1))
    ); // (*)
    cout << dual_ptr_1.use_count() << endl;  // 1
    cout << dual_ptr_2.use_count() << endl;  // 1
    cout << dual_ptr_1->use_count() << endl; // 1
    cout << dual_ptr_2->use_count() << endl; // 1
} // <- Double free runtime error at closing brace.

// Replacing line (*) with:
// shared_ptr<shared_ptr<Wrapper>> dual_ptr_2(
//     new shared_ptr<Wrapper>(&(**dual_ptr_1))
// );
// has much the same effect, possibly just without compiler optimization.

Case B is a buggy variation of Case A, where the difference in Case B is that the second inner pointer is constructed from a raw pointer to the Wrapper object rather than copy-constructed or assigned from the first inner pointer. Because of this, either inner pointer’s use_count stays at 1 (instead of 2) even though they both point to the same address. So, each of these inner pointers behave as if it is the only one that manages the Wrapper object. A double free runtime error occurs at main()’s closing brace, because the last inner pointer to go out of scope is trying to free memory already freed by the previous inner pointer's going out of scope.

Here's Case C - two outer pointers point to different inner pointers that point to different Wrapper objects:

int main() {
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_1(
        make_shared<shared_ptr<Wrapper>>(make_shared<Wrapper>(5))
    );
    shared_ptr<shared_ptr<Wrapper>> dual_ptr_2(
        make_shared<shared_ptr<Wrapper>>(make_shared<Wrapper>(**dual_ptr_1))
    );
    cout << dual_ptr_1.use_count() << endl;  // 1
    cout << dual_ptr_2.use_count() << endl;  // 1
    cout << dual_ptr_1->use_count() << endl; // 1
    cout << dual_ptr_2->use_count() << endl; // 1
}

Although the Wrapper objects have the same value, they are different objects and they are at different addresses. The second inner pointer's Wrapper object was copy constructed from the first inner pointer's Wrapper object.

CodeBricks
  • 1,771
  • 3
  • 17
  • 37