Search code examples
c++shared-ptrsmart-pointersrace-condition

How do `shared_ptr` and `weak_ptr` avoid a leak in this case?


Typically a smart pointer (either strong or weak) will deallocate the control block when both the strong and weak refcounts reach 0.

I have a problem figuring out how this is achieved in the following scenario: Thread A holds a strong reference and Thread B holds a weak reference, both pointing to the same block.

Assuming the strong reference is in the process of being destroyed, and the strong refcount has reached 0, the reference destructor will invoke the destructor for the managed object. So far, so good.

But then it checks the weak refcount and decides based on that value, to deallocate the block.

Here is where I still see potential for a race between the weak reference in thread B and the strong reference in thread A to deallocate the block twice. The weak reference might decrease its weak refcount atomically to 0, but I don't see why the strong reference won't be able to see this 0 before the weak reference gets to deallocate, and deallocate again.


Solution

  • If I'm understanding the question correctly, you are imagining something like

    ~shared_ptr() {
       if (--strong_ref == 0) {
           destroy_object();
           if (weak_ref == 0) {
               deallocate_block();
           }
       }
    }
    ~weak_ptr() {
        if (--weak_ref == 0) {
            deallocate_block();
        }
    }
    

    which could indeed cause deallocate_block() to be called twice. Hence, it's not a correct implementation.

    The trick is to have shared_ptr construction increment (and destruction decrement) both the strong and the weak reference counts:

    ~shared_ptr() {
       if (--strong_ref == 0) {
           destroy_object();
       }
       if (--weak_ref == 0) {
           deallocate_block();
       }
    }
    

    With one shared_ptr and one weak_ptr outstanding, the weak count would be 2, not 1. When both are destroyed, deallocate_block will only be called once - by the destructor that decremented the weak count to zero.

    As an optimization, we can have all outstanding shared_ptrs for a given control block share ownership of one increment of the weak count, in which case the destructor would look like:

    ~shared_ptr() {
       if (--strong_ref == 0) {
           destroy_object();
           if (--weak_ref == 0) {
               deallocate_block();
           }
       }
    }
    

    In this setup, if there is any shared_ptr outstanding then the weak count will be (# of outstanding weak_ptrs) + 1.