cpp

Introduction

We are guaranteed that the managed data exist as long as at least one shared pointer to them exists. This guarantee, however, might be more than we need. We might want something less: the ability to check whether the managed data still exist, and use them safely if needed. We might say that we want to track the managed data without claiming an ownership, i.e., without requiring them to exist.

In C++, this functionality is provided by the weak pointer, implemented by class template std::weak_ptr. The weak pointer functionality is coupled with the shared pointer, because the weak pointer kind of shares the data, but not fully. It’s best to illustrate this functionality with an example.

Motivating example

We need a factory function, which returns a shared pointer to some (possibly large) data. The function should:

The factory function should track the created data without claiming the ownership, and reuse them if needed. Whether the data still exists depends on the callers of the function, i.e., whether they keep or destroy their shared pointers.

For this job we need weak pointers. We cover some basics before we give the implementation.

Details

std::weak_ptr

Usage

The example below shows the basic usage:

#include <cassert>
#include <iostream>
#include <memory>

using namespace std;

struct A
{
  A()
  {
    cout << "ctor\n";
  }

  void
  saysomething()
  {
    cout << "Hi!\n";
  }

  ~A()
  {
    cout << "dtor\n";
  }
};

int
main()
{
  auto sp = make_shared<A>();
  weak_ptr<A> wp(sp);

  // The assert below doesn't compile, because it would suggest the
  // same semantics as for the unique and shared pointers, that we've
  // got the managed data, and can use them (which is wrong for the
  // weak pointer).

  // assert(wp);

  // Instead we can use function 'expired' of the weak pointer, which
  // should alert us of special semantics.
  assert(!wp.expired());

  // Here the managed data exist.
  {
    shared_ptr<A> sp2(wp);
    sp2->saysomething();
    assert(sp2);
  }

  // Release the ownership from sp.  Since sp was the sole managing
  // object, the managed data are destroyed.
  sp.reset();

  // Here the managed data is gone.
  assert(wp.expired());
}

Producing the shared poiner from a weak pointer

The problem: how to safely (i.e., without the race condition) use the managed data if, using a weak pointer, we know the data exist. Even if we make sure that a weak poiner has not expired yet, we cannot use a raw pointer, because that raw pointer might dangle just a while later. Luckily, weak_ptr does not offer a way of getting a raw pointer with the dereference operator (operator *), the member access through poiner operator (operator ->), or the get function just as the unique and shared pointers do.

The solution: lock the managed data (snatch the ownership) by creating a shared pointer from the weak pointer. We can do it in two ways:

Here’s an example:

#include <cassert>
#include <iostream>
#include <memory>

using namespace std;

int
main()
{
  auto sp = make_shared<int>();
  weak_ptr<int> wp(sp);

  // Here the managed data exist.
  {
    shared_ptr<int> sp(wp);
    assert(sp);
  }

  // Here the managed data exist.
  {
    shared_ptr<int> sp = wp.lock();
    assert(sp);
  }

  // Flush the managed data.
  sp.reset();

  // Here the managed data is gone.
  try
    {
      shared_ptr<int> sp(wp);
    }
  catch (std::bad_weak_ptr &)
    {
      cout << "Caught a std::bad_weak_ptr.\n";
    }

  // Here the managed data is long gone.
  {
    shared_ptr<int> sp = wp.lock();
    assert(!sp);
  }
}

How it works

The control data structure of the shared pointer group is also used by the weak pointers, which also belong to the group, but without claiming ownership.

Just as a shared pointer, a weak pointer has a raw pointer to the control data structure.

A managing structure not only has a reference count, but also a weak count, which keeps the number of weak pointers.

We know that the managed data is destroyed, when the reference count reaches zero.

The control data structure is destroyed when both the reference count and the weak count reach zero.

The implementation of the motivating example

Here’s the implementation:

#include "factory.hpp"

#include <cassert>
#include <iostream>
#include <map>
#include <memory>

using namespace std;

struct A
{
  static int counter;

  int m_id;
  int m_unique;

  A(int id): m_id(id), m_unique(counter++)
  {
    cout << "ctor: " << m_id << ", " << m_unique << '\n';
  }

  ~A()
  {
    cout << "dtor: " << m_id << ", " << m_unique << '\n';
  }
};

int A::counter = 0;

int
main()
{
  int unique;

  {
    auto sp1 = factory<A>(1);
    auto sp2 = factory<A>(1);
    unique = sp1->m_unique;
    assert(sp1->m_unique == sp2->m_unique);
  }

  auto sp1 = factory<A>(1);
  assert(unique != sp1->m_unique);
}

Performance

A weak pointer takes twice as much memory as a raw pointer, because it has:

What do we need the raw pointer to the managed data for if we cannot access it directly? Beause it will be needed to produce a shared pointer.

As for a shared pointer, the same applies to a raw pointer: the raw pointer to the managed data could be a part of the control data structure, but getting to the managed data would be slower, because an extra indirect access would be needed.

Conclusion

Quiz

Acknowledgement

The project financed under the program of the Minister of Science and Higher Education under the name “Regional Initiative of Excellence” in the years 2019 - 2022 project number 020/RID/2018/19 the amount of financing 12,000,000 PLN.