41

I've been thinking about using shared pointers, and I know how to implement one myself--Don't want to do it, so I'm trying std::tr1::shared_ptr,and I have couple of questions...

How is the reference counting implemented? Does it use a doubly linked list? (Btw, I've already googled, but I can't find anything reliable.)

Are there any pitfalls for using the std::tr1::shared_ptr?

Christian Rau
  • 45,360
  • 10
  • 108
  • 185
purepureluck
  • 1,271
  • 2
  • 14
  • 16
  • 12
    “Does it use a doubly linked list?” – For *what*? How would this help in reference counting? – Konrad Rudolph Feb 08 '12 at 20:14
  • 5
    How would a linked list help? Also I thought you said you knew how to implement one yourself. How `shared_ptr` does reference counting is implementation-defined so there's no answer to this question. You can always look at the source for your implementation. – Seth Carnegie Feb 08 '12 at 20:14
  • 2
    First: there is source code. Second: there is C++0x/11 so why TR1? Third: SO _is-a_ awesome community. C++ _has-a_ awesome community. And GCC wish they had it :) (half joking) – sehe Feb 08 '12 at 20:15
  • "How is the std::tr1::shared_pointer implemented?" Which implementation are you using? ;) – johnsyweb Feb 08 '12 at 20:16
  • 1
    Duplicate of [How do shared pointers work?](http://stackoverflow.com/questions/2802953/how-do-shared-pointers-work) (and see also [How does weak_ptr work?](http://stackoverflow.com/questions/5671241/how-does-weak-ptr-work)) – James McNellis Feb 08 '12 at 20:17
  • 3
    STL made a [whole episode about that](http://channel9.msdn.com/Shows/Going+Deep/C9-Lectures-Stephan-T-Lavavej-Advanced-STL-1-of-n). It's non-trivial, I'd say, though that's mainly on account of the atomic reference update and high level of abstraction. – Kerrek SB Feb 08 '12 at 20:17
  • 2
    @KonradRudolph You can implement a reference counted pointer using a linked list. Instead of storing the number of reference centrally store a linked list of all of the current pointers. When there are none left in that list you know to delete the object. I believe that it *may* have certain advantages in multithreaded code as you can write a possibly lockless version. However I've never seen it actually done so I guess its not better in practice. – jcoder May 02 '13 at 09:44
  • @jcoder I know this strategy but I’ve never heard that referred to as “reference counted” and I think that would be wrong. – Konrad Rudolph May 02 '13 at 09:49
  • @KonradRudolph I agree that naming it reference counted is a bit of a stretch. – jcoder May 02 '13 at 10:05
  • @jcoder Atomic modification of an integer is trivial. Atomic modification of doubly linked list is not. – curiousguy Jul 19 '15 at 08:31
  • If all you're intending to do is *using* shared pointers, why do you care how exactly they are implemented? – DevSolar Dec 03 '15 at 15:33

4 Answers4

66

shared_ptr must manage a reference counter and the carrying of a deleter functor that is deduced by the type of the object given at initialization.

The shared_ptr class typically hosts two members: a T* (that is returned by operator-> and dereferenced in operator*) and a aux* where aux is a inner abstract class that contains:

  • a counter (incremented / decremented upon copy-assign / destroy)
  • whatever is needed to make increment / decrement atomic (not needed if specific platform atomic INC/DEC is available)
  • an abstract virtual destroy()=0;
  • a virtual destructor.

Such aux class (the actual name depends on the implementation) is derived by a family of templatized classes (parametrized on the type given by the explicit constructor, say U derived from T), that add:

  • a pointer to the object (same as T*, but with the actual type: this is needed to properly manage all the cases of T being a base for whatever U having multiple T in the derivation hierarchy)
  • a copy of the deletor object given as deletion policy to the explicit constructor (or the default deletor just doing delete p, where p is the U* above)
  • the override of the destroy method, calling the deleter functor.

A simplified sketch can be this one:

template<class T>
class shared_ptr
{
    struct aux
    {
        unsigned count;

        aux() :count(1) {}
        virtual void destroy()=0;
        virtual ~aux() {} //must be polymorphic
    };

    template<class U, class Deleter>
    struct auximpl: public aux
    {
        U* p;
        Deleter d;

        auximpl(U* pu, Deleter x) :p(pu), d(x) {}
        virtual void destroy() { d(p); } 
    };

    template<class U>
    struct default_deleter
    {
        void operator()(U* p) const { delete p; }
    };

    aux* pa;
    T* pt;

    void inc() { if(pa) interlocked_inc(pa->count); }

    void dec() 
    { 
        if(pa && !interlocked_dec(pa->count)) 
        {  pa->destroy(); delete pa; }
    }

public:

    shared_ptr() :pa(), pt() {}

    template<class U, class Deleter>
    shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}

    template<class U>
    explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}

    shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }

    template<class U>
    shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }

    ~shared_ptr() { dec(); }

    shared_ptr& operator=(const shared_ptr& s)
    {
        if(this!=&s)
        {
            dec();
            pa = s.pa; pt=s.pt;
            inc();
        }        
        return *this;
    }

    T* operator->() const { return pt; }
    T& operator*() const { return *pt; }
};

Where weak_ptr interoperability is required a second counter (weak_count) is required in aux (will be incremented / decremented by weak_ptr), and delete pa must happen only when both the counters reach zero.

Valentin
  • 917
  • 14
  • 19
Emilio Garavaglia
  • 20,229
  • 2
  • 46
  • 63
  • 1
    Shouldn't `delete pa` happen if the count of strong references reaches zero, regardless of `weak_count`? – iFreilicht Jun 12 '14 at 23:27
  • 8
    pa is the pointer to the structure that contains the counters and the deleter. When the strong count gets to zero you have to delete the object, but the counters have to stay until also all weaks are gone: the zeroed strong counter is what makes the weaks to know they point to a destroyed object, and the weak counter is the number of weak that still need to know that. – Emilio Garavaglia Jun 14 '14 at 09:45
  • Also, could you please explain in a little bit more detail why a separate template parameter U is needed? – btshengsheng Dec 02 '15 at 13:27
  • 2
    @btshengsheng: yup! yes, it must be `pt`, fixed. The need of "U" is mostly due to the fact that what T points to may not be the entire object, but only a part of it (for example a base), and to properly delete an object a way to get the object type is needed. Now, if the object is runtime-polymorphic (has a virtual destructor), delete `pt` also causes `~U` to be called, but if it is not, there is no runtime way to go from T* to U*. But since the static type of an object is know at construction, we can keep the U type safe using a polymorphic internal data. – Emilio Garavaglia Dec 03 '15 at 15:29
  • This is a very helpful answer. Tiny nitpick/question: Within `dec()`, do you need to set `pa = nullptr` after `delete pa`? Else, within `inc()`, I don't understand how `if (pa)` is safe. [I am a bit rusty on null pointer semantics post C++-11 "Big Bang", so forgive me if `delete` now automatically sets `pa` to `null`.] – kevinarpe Dec 27 '15 at 10:48
  • 1
    @kevinarpe: In this strict sample no. `dec` is called in the dtor (so you'll never have a chance to use that value further) or in assignment (where `pa` is about to take another value) – Emilio Garavaglia Jan 02 '16 at 21:19
35

How is the reference counting implemented?

A smart pointer implementation could be deconstructed, using policy-based class design1, into :

  • Storage Policy

  • Ownership Policy

  • Conversion Policy

  • Checking Policy

included as template parameters. Popular ownership strategies include: deep copy, reference counting, reference linking, and destructive copy.

Reference counting tracks the number of smart pointers pointing to (owning2) the same object. When the number goes to zero, the pointee object is deleted3. The actual counter could be:

  1. Shared among smart pointer objects, where each smart pointer holds a pointer to the reference counter:

enter image description here

  1. Included only in an additional structure that adds an extra level of indirection the pointee object. Here the space overhead of holding a counter in each smart pointer is exchanged with slower access speed:

enter image description here

  1. Contained within the pointee object itself: intrusive reference counting. The disadvantage is that the object must be constructed a priori with facilities for counting:

    enter image description here

  2. Finally the method in your question, reference counting using doubly linked lists is called reference linking and it:

...[1] relies on the observation that you don't really need the actual count of smart pointer objects pointing to one pointee object; you only need to detect when that count goes down to zero. This leads to the idea of keeping an "ownership list" :

enter image description here

The advantage of reference linking over reference counting is that the former does not use extra free store, which makes it more reliable: Creating a reference-linked smart pointer cannot fail. The disadvantage is that reference linking needs more memory for its bookkeeping (three pointers versus only one pointer plus one integer). Also, reference counting should be a bit speedier—when you copy smart pointers, only an indirection and an increment are needed. The list management is slightly more elaborate. In conclusion, you should use reference linking only when the free store is scarce. Otherwise, prefer reference counting.

Regarding your second question:

Does it (std::shared_ptr) use a doubly linked list?

All that I could find in the C++ standard was:

20.7.2.2.6 shared_ptr creation
...
7. [ Note: These functions will typically allocate more memory than sizeof(T) to allow for internal bookkeeping structures such as the reference counts. —end note ]

Which, in my opinion, excludes doubly linked lists, as they do not contain actual count.

Your third question:

Are there any pitfalls for using the std::shared_ptr?

Reference management either counting or linking is a victim of the resource leak known as cyclic reference. Let's have an object A that holds a smart pointer to an object B. Also, object B holds a smart pointer to A. These two objects form a cyclic reference; even though you don't use any of them any more, they use each other. The reference management strategy cannot detect such cyclic references, and the two objects remain allocated forever.

Because the implementation of shared_ptr uses reference counting, cyclic references are potentially a problem. A cyclic shared_ptr chain can be broken by changing the code so that one of the references is a weak_ptr. This is done by assigning values between shared pointers and weak pointers, but a weak pointer doesn't affect the reference count. If the only pointers that point to an object are weak, the object is destroyed.


1. Each design feature with multiple implementations if formulated as policy.

2. Smart pointers similarly to pointers that point to object allocated with new, not only point to that object but also are responsible for its destruction and with the release of the memory it occupies.

3. With no further problems, if no other raw pointers are used and/or point to it.

[1] Modern C++ Design: Generic Programming and Design Patterns Applied. Andrei Alexandrescu, February 01, 2001

Ziezi
  • 6,375
  • 3
  • 39
  • 49
4

If you want to see all the gory details, you can have a look at the boost shared_ptr implementation:

https://github.com/boostorg/smart_ptr

The reference counting seems to usually be implemented with a counter and platform specific atomic increment/decrement instructions or explicit locking with a mutex (see the atomic_count_*.hpp files in the detail namespace).

Damian
  • 4,395
  • 4
  • 39
  • 67
sth
  • 222,467
  • 53
  • 283
  • 367
3

Are there any pitfalls for using the std::tr1::shared_ptr?

Yes, If you create cycles in your shared memory pointers, then the memory being managed by the smart pointer will not be recycled when the last pointer goes out of scope because there are still references to the pointer (i.e., the cycles cause the reference count to not go down to zero).

For instance:

struct A
{
    std::shared_ptr<A> ptr;
};

std::shared_ptr<A> shrd_ptr_1 = std::make_shared(A());
std::shared_ptr<B> shrd_ptr_2 = std::make_shared(A());
shrd_ptr_1->ptr = shrd_ptr_2;
shrd_ptr_2->ptr = shrd_ptr_1;

Now, even if shrd_ptr_1 and shrd_ptr_2 go out of scope, the memory they are managing is not reclaimed because the ptr member of each are pointing to each other. While this is a very naive example of such a memory cycle, it can, if you use these types of pointers without any discipline, occur in a much more nefarious and hard-to-track fashion. For instance, I could see where trying to implement a circular linked-list where each next pointer is a std::shared_ptr, if you're not too careful, could result in problems.

Ziezi
  • 6,375
  • 3
  • 39
  • 49
Jason
  • 31,834
  • 7
  • 59
  • 78
  • Not really a pitfall if you use weak_ptr appropriately though... More of something you need to be aware of. – Matt May 19 '14 at 20:42
  • @Matt `weak_ptr` is not a fix for circular dependencies. The only fix to bad design is changing the design, not using other tools. You can't just take one owning smart ptr (or "strong reference"), replace it with a non owning smart ptr (or "weak reference") and expect the program to work. – curiousguy Jul 19 '15 at 07:48
  • @curiousguy What I meant was, you can fix circular dependencies by using `weak_ptr`. So if you had a dependency where A holds a `shared_ptr` to B and B holds a `shared_ptr` to A, you could instead make B hold a `weak_ptr` to A, if B really shouldn't be owning A but needs to reference it safely. But this is in fact **"changing the design" so yes**, you can't just blindly do this and expect to fix everything. My point is, it's not a pitfall of `shared_ptr` and `shared_ptr` is actually nice because it has `weak_ptr` as a tool to avoid the circular dependencies. – Matt Jul 21 '15 at 19:11
  • @Matt Yes; but `weak_ptr` is very rarely useful (and very useful then). It is useful f.ex. when you want to keep a cache where is acceptable for items to exist in a dead state; or a in game, where a monster can exist in a list in a "not there anymore" state, etc. However, it is clearly not useful in the usual circular dependency case where you have strong "owning" links going both ways as in graph, because these links by design cannot exist in a dead state. The suggestion that "weak" ref is an easy fix for circular dependency issues is an abomination. – curiousguy Jul 22 '15 at 03:10