Neuroborus
Neuroborus

Reputation: 55

What variables does weak_ptr hold?

I understand about what methods are available and what they are. Please, describe private section of weak_ptr class or give example of some custom weak_ptr code. I can't understand by std::weak_ptr implementation.

Upvotes: 1

Views: 356

Answers (2)

curiousguy
curiousguy

Reputation: 8298

In order to understand the linked shared-weak pair, you must first imagine a pure shared "smart pointer" without any "weak reference" feature. For efficiency imagine it's an intrusive owning reference counted pointer: the counter is made part of the managed object.

Back to the implementation of the "weak reference" feature: we need an object that can detect whether a referent is still alive and bind a real (strong) reference to it (atomically). The measurement of "is still alive" needs to be done on a reference count that exists at all time (until it isn't ever needed, when there are zero references, strong or weak), so the "weak reference" needs to be able to keep that counter alive. So the "weak reference" is an owning (strong) reference to an intrusively counted data structure that keeps the reference count of an external counted data structure, the managed object of type T for a weak_ptr<T>.

The implementation must destroy the managed object of type T exactly when there are no more real (strong) references (shared_ptr), so it must have an atomic counter for that purpose. Note that it isn't technically have to be some kind of atomic integer but an atomic<int> happens to be a simple way to get that effect. The operations are:

  • owner creation by copy of other owner (shared_ptr to shared_ptr): atomically increment the count
  • owner destruction and test: atomically decrement and test if last owner (that is count is zero)
  • owner creation from weak reference (weak_ptr to shared_ptr): atomically test if count is non zero, then increase and indicate success, otherwise no modification to count and indicate failure

All these operations are possible on an atomic variable. They all involve an atomic RMW (read modify write) which is often a lot more costly than a simple read or write. The cost is however less if the location is available in the local L1d cache and not bouncing between CPUs.

The other count is for the intrusively counted object: the count of weak reference must be separate as it can be non zero even when the object of type T has been destroyed.

The only operations on that count of the management data structure are increment and decrement-and-test, as a zero value can never be reached for a stable object: zero is for an object that is being destroyed.

Of course as usually only one word can be atomically manipulated and each counter is usually a word, as an half word might overflow in some cases, but a word cannot (as you don't have memory to create 2 ** 31 objects, or even 2 ** 63 for a 64 bits machines); so there is a constraint on the modifications of the counters. A simple solution is that the intrusive reference count doesn't keep track of changes of strong owners, which is already well kept in one counter and doesn't need to be replicated in the other counter. The intrusive counter serves only for the last strong reference to T and for the weak references to T (the weak_ptr here), which are also strong references to the management data structure.

So there are two atomic counters:

  • the strong reference count: to determine when T is destroyed (and its memory deallocated, except when the allocation is shared with the management data as with make_shared
  • the (weak reference plus last strong reference) count: which indicates how many references exist, all strong references counting as one

Other variants might be possible, and if you abandon thread safety you can get more creative (you can imagine a linked list of owners), but these might not be more efficient.

Upvotes: 1

Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385325

An unintrusive shared pointer implementation usually contains a pointer to some dynamically-allocated "state", which counts how many references there are to the original object.

When a shared pointer is copied, the copy gets the same pointer to the same "state", and the count inside the "state" is incremented to indicate that there are now two shared pointers sharing the resource.

When a shared pointer is destroyed, it decrements the counter to indicate that there is now one fewer pointer sharing the resource. If this results in the counter reading zero, the resource is destroyed.

A weak pointer also has a pointer to this "state", but it doesn't increment or decrement the counter. When asked, it will construct a shared pointer using the same state, but only if the count is not zero. If the count is zero, the last shared pointer already destroyed the resource, and we can't get access to it any more.

What's interesting is that you also need logic like this to control the lifetime of the "state" object. :) (I'd imagine that's implemented using a second counter, which both shared_ptr and weak_ptr do increment, but don't quote me on that.)

(your data)         (ref. counters)
     ║                    ║
[resource]             [state]
  ┆  │ │                │ │ │
  ┆  │ └─[shared_ptr]───┘ │ │
  ┆  └───[shared_ptr]─────┘ │
  └┄┄┄┄┄┄┄[weak_ptr]────────┘

Of course, what the private section of any particular std::weak_ptr implementation exactly looks like is up to the person who wrote it.

Incidentally, the diagram kind of shows why you shouldn't construct a shared_ptr from a raw pointer, if you suspect that the resource it points to may already be managed by shared_ptr(s) elsewhere: you'd get a second, unrelated "state" object, your counters will be wrong, and your resource may be destroyed prematurely (and will definitely be destroyed twice, if such a notion exists), causing mayhem.

Upvotes: 2

Related Questions