cpp

Introduction

When we dynamically create some data (or any other resource) and use them in other threads or parts of code, we run into the problem when to destroy the data. If we:

Therefore we should destroy the data at the right time, i.e., when they are no longer needed. However, this right time is hard to pinpoint with raw pointers, because it may depend on:

The solution to the problem is the shared-ownership semantics:

In Java or C#, a reference has the shared-ownership semantics.

std::shared_ptr

Details

Usage

The example below shows the basic usage.

#include <cassert>
#include <iostream>
#include <memory>
#include <string>
#include <utility>

using namespace std;

struct A
{
  string m_text;

  A(const string &text): m_text(text)
  {
    cout << "ctor: " << m_text << endl;
  }

  ~A()
  {
    cout << "dtor: " << m_text << endl;
  }
};

int main (void)
{
  // sp takes the ownership.
  shared_ptr<A> sp(new A("A1"));
  assert(sp);

  // We make sp manage a new object.  A1 is destroyed.
  sp.reset(new A("A2"));

  {
    // We copy-initialize the ownership.
    shared_ptr<A> sp2(sp);
    assert(sp);
    assert(sp2);

    shared_ptr<A> sp3;
    // We copy-assign the ownership.
    sp3 = sp2;
    assert(sp2);
    assert(sp3);

    // Even though sp2 and sp3 go out of scope, A2 will not be
    // destroyed, because it's still being managed by sp.
  }

  {
    // We move-initialize the ownership.
    shared_ptr<A> sp2(move(sp));
    assert(!sp);
    assert(sp2);

    shared_ptr<A> sp3;
    // We move-assign the ownership.
    sp3 = move(sp2);
    assert(!sp2);
    assert(sp3);

    // A2 is destroyed, because sp3 (the sole managing object o A2)
    // goes out of scope.
  }

  // We can't release the managed data from being managed, as we are
  // able to do with unique_ptr, because we can't preempt (strip)
  // other shared_ptr objects of their ownership.

  // sp.release();

  // If we want to reset a pointer, we can use the reset function.
  sp.reset();
}

How it works

From unique_ptr to shared_ptr

We can move the ownership from unique_ptr to shared_ptr like that alright:

#include <memory>

using namespace std;

int
main()
{
  auto up = make_unique<int>();
  shared_ptr<int> sp(up.release());
}

But it’s downright better done this way:

#include <memory>
#include <utility>

using namespace std;

int
main()
{
  auto up = make_unique<int>();
  shared_ptr<int> sp(move(up));
}

We can move the ownership from an rvalue of type unique_ptr, because shared_ptr has the constructor which takes by rvalue reference an object of type std::unique_ptr. Therefore, we can create a shared_ptr object from a temporary object of type unique_ptr, e.g., returned by a function like this:

#include <memory>
#include <utility>

using namespace std;

unique_ptr<int>
factory()
{
  return make_unique<int>();
}

int
main()
{
  shared_ptr<int> sp(factory());
}

Performance

A shared_ptr object takes twice as much memory as a raw pointer, because it has two fields:

On top of this, there is the memory taken by the control data structure allocated, but it’s not a big deal, because it’s shared among the managing objects.

A pointer to the managed data could be kept in the control data structure, but then getting to the managed data would involve an extra indirect access, thwarting performance.

std::make_shared

When creating the managed data and the managing object, we can write the type of the managed data twice (and perhaps introduce bugs):

#include <memory>

using namespace std;

int
main()
{
  // We have to type int twice.
  shared_ptr<int> sp(new int);
  // Bug: constructor and destructor mismatch: int[] vs int
  shared_ptr<int[]> sp2(new int);
  // Bug: constructor and destructor mismatch: int[] vs int[5]
  shared_ptr<int> sp3(new int[5]);
}

But we can use function make_shared and write the type only once like this (which is less error-prone):

#include <memory>

using namespace std;

int
main()
{
  // We don't have to write the type twice.
  auto sp = make_shared<int>();
  // We can't mismatch the constructor and destructor.
  auto sp2 = make_shared<int[]>(5);
}

Function template make_shared takes the type of the data to manage as its template argument.

Similar to function make_unique, function make_shared creates the managed data and the managing object, and then returns the managing object. There is no performance overhead, since the function will most likely be inlined, and the constructors elided when returning the managing object.

Interestingly, make_shared allocates in one piece (i.e., with one memory allocation) the memory for the managed data and the control data structure, and then creates in place (i.e., without allocating memory) the managed data and the control data structure, which is faster than two separate memory allocations.

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.