Most C++ programmers known more or less about std::shared_ptr, but what is its limitations?

C++ standard library smart pointer std::shared_ptr is based on the RAII and reference counting, an ingenious way to manange memory than C. As long as be careful with the circular reference (not 100% to avoid them virtually), you can always coding without too much concerns.

Supposed you need to manage a DAG, which is very common in organizing software architecture. Usually, DAG would be used along with Observer Pattern. Since one need to response to changes or events from its dependencies. It has a bidirectional link between one node and its successor.

DAG
DAG

It’s okay in a programming with GC, but in C++, there is usually a strong reference point (solid arrow in the chart) to its successor and at the same time, a weak reference point to the predecessor. (dash arrow in the chart) in order to avoid cycling referencing.

Saying that you want to create two objects with this kind of relationship as the fowlling chart, you may allocate one successor and observe it immediately after it being created in constructor, which is straightforward.

DAG
cycling reference

The code is like this


struct Node : public std::enable_shared_from_this<Node> {
  std::vector<std::weak_ptr<Node>> subscribers;
  void observe(std::shared_ptr<Node> sub) {
    // ... do some check

    sub->subscribers.push_back(this->shared_from_this());
    // ...
  }
};

struct Bar : public Node {};

struct Foo : public Node {
  std::shared_ptr<Node> object2;
  Foo(std::shared_ptr<Bar> observed ) {
    object2 = observed;
    observe(std::move(observed));
  }
};

int main() { auto object1 = std::make_shared<Foo>(std::make_shared<Bar>); }

If you run the code, it will throw std::bad_weak_ptr and terminate the program. The reason is that the object2 is not constructed yet when the shared_from_this is called in the constructor of Foo. So the weak reference is not constructed yet. That’s the limitation of std::shared_ptr when it comes to this case. You may even notice that using auto object1 = std::make_shared<Foo>() to create a shared object needs two steps rather than one. The first step is construct the object itself, and the second step, creating shared_ptr<T> wrapper and putting the object into it.

It’s so often that observing another object in one object’s constructor. You definitely could achieve it using std::shared_ptr, but it’s verbose when you need to deal with massive kind of this DAG network. So when your system heavily relies on this kind of DAG, you need another smart pointer to help you out.

That’s what intrusive smart pointer does. It’s a smart pointer that doesn’t allocate memory for the reference count, but put the reference count in the object itself. It’s intrusive because it needs to modify the object itself to add the reference count. It’s a trade-off between memory and performance. It’s more efficient than std::shared_ptr in terms of memory usage and performance. But it’s less flexible than std::shared_ptr because it needs to modify the object itself. Usually it’s not a problem because when you need it, it means that you system is designed on purpose, the cost of instrusive modification is such a small part compared to the whole system.

The intrusive pointer don’t support weak reference counting by default. Thinking a weak pointer means that when when the strong reference count is 0 and the weak reference count is not 0, the object should be destroyed according to the weak reference counting semantic. But also according to the definition of intrusive, if the object is destroyed, the counters in the object should be destroyed as well. So you cannot put the counter inside the object directly if you want to support weak reference counting. You need to put the counter outside the object, and the object should be able to access the counter. It’s a little bit tricky, but it’s doable. You just put the strong/weak counting part all together out of the object itself and point to it.

ref_ptr
smart pointer layout

The interface of RefCnt is straight forward and intuitive.


class RefCnt{
  public:
  virtual void ref() = 0;
  virtual void deref() = 0;
  virtual void ref_weak() = 0;
  virtual void deref_weak() = 0;
  virtual size_t ref_count() = 0;
  virtual size_t ref_weak_count() = 0;
};

You can implement of your own RefCnt by inheriting from it. In most senarios, you can using final keyword and give the most-concret instance to eliminate the cost of virtual call.

Here is the implementation.