Search code examples
c++c++11shared-ptrunique-ptrnested

Resetting nested smart pointer of a shared_ptr to a shared_ptr (or to a unique_ptr), seeming paradox


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.


Solution

  • 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