Ownership and Lifetime

Everything is simple until there are too many things to handle at once. So we scope. A scope is a namespace (e.g., a function). We can also use multi-level scopes. Scopes break the namespace into smaller ones to help us manage the cognitive context.

It sounds all good, but causes a new problem: what do we do when an object outlives the scope in which it was created?

That is why we need ownership. When an object is owned, it stays alive. When its last owner releases it, it gets destroyed.

Lack of Ownership

When nobody owns an object, nothing is responsible for destroying it: it may leak or get destroyed on a guess. Let us see how ownership works by observing the problem introduced by the lack of ownership.

A raw pointer records an address and nothing else, so it does not carry any information about ownership. It does not say whether the pointed-to object is alive, and it does not say who is responsible for destroying it. Code written against raw pointers therefore encodes that responsibility nowhere but in the reader’s memory:

Data * data = alloc_function(); // allocates and returns
op_function(data); // may or may not delete, depending on the code inside
delete data; // correct or a double free; the caller here cannot tell

Because the raw pointer carries no ownership information, there is no way for the third line to know whether data was already deleted in op_function().

Note

solvcon follows a convention for raw pointers: when a function receives a raw pointer that is not null, someone else owns the object. If it is null, there is no object.

A reference (Type &) is a similar case, but it cannot be null: it must refer to an existing object owned by someone else.

Unique Pointer for Simple Ownership

Raw pointers and references are not sufficient to manage object lifetime.

If we consider a simple scenario that allows at most one owner for an object, std::unique_ptr can be used to manage the ownership. The smart pointer makes it straight-forward to manage the object lifetime using RAII (resource acquisition is initialization), tying the lifetime of a resource to the lifetime of an object.

The overhead of std::unique_ptr is usually zero. If an object is not shared among multiple owners, or you only want to make sure it gets release at the end of a scope, std::unique_ptr is usually the right tool.

C++ carries such guarantees with RAII (resource acquisition is initialization), tying the lifetime of a resource to the lifetime of an object, and spells the ownership out with smart pointers: std::unique_ptr where there is exactly one owner, std::shared_ptr where there are several.

Shared Ownership

If an object should be shared among multiple owners, std::shared_ptr should be used. It offers shared ownership with a reference count. The object is destroyed when the last owner releases it.

Conceptually, shared ownership is a superset of unique ownership, but we must not be fooled into using a std::shared_ptr when the code does not require shared ownership. It costs much more overhead than a raw pointer or a unique pointer.

To make shared ownership possible, a std::shared_ptr needs to actually manage two objects: the referenced object and a control block for the reference counts. That takes two memory allocations instead of one. The additional memory allocation is a cost, and sometimes a significant one.

Always think twice before using a shared pointer. If you create a lot of small objects, it is not the right scenario for shared pointers, because each object will need to pay for the overhead. Shared pointers should be used to manage objects that are large and have long lifetimes.

A memory buffer is in exactly that position.

Exclusively Use Shared Pointers

There are a lot of caveats in using a shared pointer. The first one is the issue with the std::shared_ptr constructor.

The std::shared_ptr constructor accepts a raw pointer and takes ownership of the object it points to. But the raw pointer stays with the caller, and nothing stops it from being given to another shared pointer.

Shared Pointer Double Free

To see the problem, first new an object (Data) and hand the pointer to the constructor of the shared pointer:

Data * raw_pointer = new Data;
std::shared_ptr<Data> sptr1(raw_pointer);
std::cout << "sptr1.use_count(): " << sptr1.use_count() << std::endl;

Then the object is held in the shared pointer:

Data @0x5e9b5b1012b0 is constructed
sptr1.use_count(): 1

Then we give the same raw pointer to a second shared pointer:

std::shared_ptr<Data> sptr2(raw_pointer);
std::cout << "sptr2.use_count(): " << sptr2.use_count() << std::endl;

The reference count is 1 rather than 2, because the second shared pointer creates its own reference counter and does not know about the first one:

sptr2.use_count(): 1

Release the first shared pointer:

sptr1.reset();
std::cout << "sptr1.use_count() after sptr1.reset(): " << sptr1.use_count() << std::endl;

Its reference count drops to zero, so the Data object is destructed, although sptr2 still points to it:

Data @0x5e9b5b1012b0 is destructed
sptr1.use_count() after sptr1.reset(): 0

Now release the second shared pointer:

std::cout << "sptr2.use_count(): " << sptr2.use_count() << std::endl;
sptr2.reset();  // This line crashes with double free.
// This line never gets reached since the above line causes double free and
// crash.
std::cout << "sptr2.use_count() after sptr2.reset(): "
          << sptr2.use_count() << std::endl;

It can never reach the last line, since releasing the pointer destructs the object again and results in double free:

sptr2.use_count(): 1
Data @0x5e9b5b1012b0 is destructed
double free or corruption (!prev)
Aborted (core dumped)

To avoid the problem, we do not want anyone to directly call the Data constructor:

// We want to forbid it.
Data * raw_pointer = new Data;

Then it will be reasonably hard to write code that constructs two shared pointers from the same raw pointer.

Wrong Approach: Private Constructors

You could think that private constructors may solve the issue. Here I explain how private constructors work with shared pointers, and then why it is not the solution.

Here is a class with a private constructor for shared pointers:

class Data
{
private:
    // A private constructor.
    Data() {}
public:
    static std::shared_ptr<Data> make()
    {
        std::shared_ptr<Data> ret(new Data);
        return ret;
    }
};

By making the constructor private, it is only possible to construct the Data object from a member function. Then we implement the static member function make() to call the constructor and return the shared pointer:

class Data
{
public:
    static std::shared_ptr<Data> make()
    {
        std::shared_ptr<Data> ret(new Data);
        return ret;
    }
};

The static member function make() is a factory function to create std::shared_ptr<Data>:

std::shared_ptr<Data> data = Data::make();

Because the constructor of Data is private, the following code fails to compile:

std::shared_ptr<Data> data(new Data);

You will see messages like:

private_ctor.cpp:18:36: error: 'Data::Data()' is private within this context
   18 |     std::shared_ptr<Data> data(new Data);
      |                                    ^~~~
private_ctor.cpp:7:5: note: declared private here
    7 |     Data() {}
      |     ^~~~

It works nicely, except it prevents us from using the function template std::make_shared<Data>(). The inability to use the function template is why using private constructors is a wrong approach.

Try writing this line:

std::shared_ptr<Data> ret = std::make_shared<Data>();

The error messages will be like (in g++):

bits/stl_construct.h: In instantiation of 'void std::_Construct(Data*)':
bits/alloc_traits.h:661       required from 'std::allocator_traits::construct'
bits/shared_ptr_base.h:604    required from 'std::_Sp_counted_ptr_inplace'
bits/shared_ptr_base.h:971    required from 'std::__shared_count'
bits/shared_ptr_base.h:1712   required from 'std::__shared_ptr'
bits/shared_ptr.h:464         required from 'std::shared_ptr'
bits/shared_ptr.h:1009        required from 'std::make_shared<Data>()'
private_make.cpp:18           required from here
bits/stl_construct.h:119: error: 'Data::Data()' is private within this context
  119 |       ::new((void*)__p) _Tp(std::forward<_Args>(__args)...);
      |       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private_make.cpp:7: note: declared private here
    7 |     Data() {}
      |     ^~~~

It is because the function template std::make_shared is not inside class Data, and cannot access the private constructor! Using friend sometimes works, but it depends on how std::make_shared is implemented. The template does a lot of things behind the scenes. Simply making friends with that function template may or may not work.

The function template std::make_shared is desired because it allocates the Data object along with its reference counter. The reference counter of a shared pointer must be dynamically allocated because it is shared among all shared pointer instances. The Data object also needs to be dynamically allocated. Without std::make_shared, two dynamic allocations will be used instead of one, and it is a lot of overhead when we have many Data objects.

Good Approach: Passkey Pattern

A sound approach is to use the passkey pattern:

class Data
{
private:
    class ctor_passkey
    {
        ctor_passkey() {}
        friend class Data;
    };
public:
    static std::shared_ptr<Data> make()
    {
        std::shared_ptr<Data> ret = std::make_shared<Data>(ctor_passkey());
        return ret;
    }
    Data() = delete;
    Data(ctor_passkey const &) {}
    // TODO: Copyability and moveability should be considered, but we leave
    // them for now.
};

The default constructor should be deleted to prevent the following from working:

data = std::shared_ptr<Data>(new Data);

The compiler error messages:

01_fully.cpp:91:38: error: call to deleted constructor of 'Data'
    data = std::shared_ptr<Data>(new Data);
                                     ^
01_fully.cpp:22:5: note: 'Data' has been explicitly marked deleted here
    Data() = delete;
    ^

The use of the function template std::make_shared:

data = std::make_shared<Data>();

is forbidden for the same reason:

bits/stl_construct.h: In instantiation of 'void std::_Construct(Data*)':
bits/alloc_traits.h:661       required from 'std::allocator_traits::construct'
bits/shared_ptr_base.h:604    required from 'std::_Sp_counted_ptr_inplace'
bits/shared_ptr_base.h:971    required from 'std::__shared_count'
bits/shared_ptr_base.h:1712   required from 'std::__shared_ptr'
bits/shared_ptr.h:464         required from 'std::shared_ptr'
bits/shared_ptr.h:1009        required from 'std::make_shared<Data>()'
01_fully.cpp:95               required from here
bits/stl_construct.h:119: error: use of deleted function 'Data::Data()'
  119 |       ::new((void*)__p) _Tp(std::forward<_Args>(__args)...);
      |       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
01_fully.cpp:22: note: declared here
   22 |     Data() = delete;
      |     ^~~~

Then you only have the constructor that takes the passkey argument: Data(ctor_passkey const &). The internal passkey class has a private constructor that can be accessed from the Data class by the added friendship:

    class ctor_passkey
    {
        ctor_passkey() {}
        friend class Data;
    };

As such, the factory function Data::make() can call the passkeyed constructor through the function template std::make_shared():

    static std::shared_ptr<Data> make()
    {
        std::shared_ptr<Data> ret = std::make_shared<Data>(ctor_passkey());
        return ret;
    }

This completely manages the object by using shared pointers. You always need to construct Data by calling the factory function, and always get a shared pointer std::shared_ptr<Data> rather than a raw pointer Data *.

Get Shared Pointer from inside Object

Occasionally we get the Data object without the shared pointer (std::shared_ptr<Data>) object itself, but still want to return the shared pointer to the caller. For example, we may need to call a helper function that requires the shared pointer.

A wrong way to do it is to recreate the shared pointer object using the raw pointer (this):

class Data
{
public:
    Data * get_raw_ptr()
    {
        // Returning raw pointer discards the ownership management.
        return this;
    }

    std::shared_ptr<Data> get_shared_ptr()
    {
        // XXX: Recreating a shared_ptr will duplicate the reference counter,
        // and later result in double free.
        return std::shared_ptr<Data>(this);
    }
};

The above function get_shared_ptr() naively creates a duplicate std::shared_ptr<Data> object, and will result in double free in the caller, just like what we saw in Shared Pointer Double Free.

Enable Shared Pointer from This

The right way to get a new shared pointer from inside a shared-pointer-managed object is to use the class template std::enable_shared_from_this. It requires two things:

  1. Inherit the Data class from enable_shared_from_this.

  2. Call the inherited member function shared_from_this().

class Data
  : public std::enable_shared_from_this<Data>
{
public:
    std::shared_ptr<Data> get_shared_ptr()
    {
        // This is the right way to get the shared pointer from within the
        // object.
        return shared_from_this();
    }
};

With the change, get_shared_ptr() will not result in double free. To show it, first create the Data object:

std::shared_ptr<Data> data = Data::make();
std::cout << "data.use_count(): " << data.use_count() << std::endl;

It is held in a shared pointer with unity reference count:

Data @0x7fc5bed00018 is constructed
data.use_count(): 1

Now we call the corrected get_shared_ptr():

std::shared_ptr<Data> holder2 = data->get_shared_ptr();
std::cout << "data.use_count() after holder2: " << data.use_count() << std::endl;

The reference count is correctly increased to 2:

data.use_count() after holder2: 2

Release the first shared pointer:

data.reset();
std::cout << "data.use_count() after data.reset(): "
          << data.use_count() << std::endl;

The reference count of the original pointer becomes 0 (!):

data.use_count() after data.reset(): 0

But don’t worry, it is because the nullified shared pointer does not have access to the reference counter of the original Data object anymore. The object is still there since we do not see the destruction message.

Now release the second shared pointer:

std::cout << "holder2.use_count() before holder2.reset(): "
          << holder2.use_count() << std::endl;
holder2.reset();
std::cout << "holder2.use_count() after holder2.reset(): "
          << holder2.use_count() << std::endl;

The Data object is correctly destructed, and the reference count is correct:

holder2.use_count() before holder2.reset(): 1
Data @0x7fc5bed00018 is destructed
holder2.use_count() after holder2.reset(): 0

There is no double free any more.

Avoid Circular Reference

Circular (or cyclic) reference means a chain of objects pointing in a circle. One object is sufficient to form a circle (self-referencing).

The circular reference is not a problem when no pointer owns other objects. It becomes a problem when the pointers are smart pointers that manage object lifetime.

Here we use a circle formed by two objects to demonstrate the problem. The following code has two objects (Data and Child) pointing to each other using a shared pointer. The circular reference creates a memory leak:

class Data
  : public std::enable_shared_from_this<Data>
{
public:
    std::shared_ptr<Child>   child() const { return m_child; }
    std::shared_ptr<Child> & child()       { return m_child; }
private:
    std::shared_ptr<Child> m_child;
};

class Child
  : public std::enable_shared_from_this<Child>
{
private:
    class ctor_passkey
    {
        ctor_passkey() {}
        friend class Child;
    };
public:
    Child() = delete;
    Child(std::shared_ptr<Data> const & data, ctor_passkey const &)
      : m_data(data) {}
    static std::shared_ptr<Child> make(std::shared_ptr<Data> const & data)
    {
        std::shared_ptr<Child> ret =
            std::make_shared<Child>(data, ctor_passkey());
        data->child() = ret;
        return ret;
    }
private:
    std::shared_ptr<Data> m_data;
};

Create the Data and Child objects:

std::shared_ptr<Data> data = Data::make();
std::shared_ptr<Child> child = Child::make(data);
std::cout << "data.use_count(): " << data.use_count() << std::endl;
std::cout << "child.use_count(): " << child.use_count() << std::endl;

The reference counts are 2:

Data @0x7f8f48d00018 is constructed
data.use_count(): 2
child.use_count(): 2

To peek at the reference counts, create two weak_ptr objects. They can access the reference counts without owning the objects.

std::weak_ptr<Data> wdata(data);
std::weak_ptr<Child> wchild(child);

Release the shared pointer to the Data object:

data.reset();
std::cout << "wdata.use_count() after data.reset(): "
          << wdata.use_count() << std::endl;
std::cout << "wchild.use_count() after data.reset(): "
          << wchild.use_count() << std::endl;

The Child object still owns the Data object, so wdata has a reference count of 1:

wdata.use_count() after data.reset(): 1
wchild.use_count() after data.reset(): 2

Release the shared pointer to the Child object:

child.reset();
std::cout << "wdata.use_count() after child.reset(): "
          << wdata.use_count() << std::endl;
std::cout << "wchild.use_count() after child.reset(): "
          << wchild.use_count() << std::endl;

There is still one reference to Child remaining:

wdata.use_count() after child.reset(): 1
wchild.use_count() after child.reset(): 1

Oops. The Data and Child objects will never go away!

Weak Pointers Provide a Workaround

In the above demonstration we use weak pointers to get the reference count without increasing it. The weak pointer can also be used to break the circular reference. In the following example, the Child object replaces std::shared_ptr with std::weak_ptr to point to Data:

class Child
  : public std::enable_shared_from_this<Child>
{
private:
    class ctor_passkey
    {
        ctor_passkey() {}
        friend class Child;
    };
public:
    Child() = delete;
    Child(std::shared_ptr<Data> const & data, ctor_passkey const &)
      : m_data(data) {}
    static std::shared_ptr<Child> make(std::shared_ptr<Data> const & data)
    {
        std::shared_ptr<Child> ret =
            std::make_shared<Child>(data, ctor_passkey());
        data->child() = ret;
        return ret;
    }
private:
    // Replace shared_ptr with weak_ptr to Data.
    std::weak_ptr<Data> m_data;
};

Like the previous example, the Data and Child objects are created:

std::shared_ptr<Data> data = Data::make();
std::shared_ptr<Child> child = Child::make(data);
std::cout << "data.use_count(): " << data.use_count() << std::endl;
std::cout << "child.use_count(): " << child.use_count() << std::endl;

std::weak_ptr<Data> wdata(data);
std::weak_ptr<Child> wchild(child);

The two objects are linked to each other:

Data @0x7fe6f8500018 is constructed
data.use_count(): 1
child.use_count(): 2

Release the reference to the Child object from the controlling program:

child.reset();
std::cout << "wdata.use_count() after child.reset(): "
          << wdata.use_count() << std::endl;
std::cout << "wchild.use_count() after child.reset(): "
          << wchild.use_count() << std::endl;

Now Child does not own Data, both the reference counts to the Data and Child objects are 1:

wdata.use_count() after child.reset(): 1
wchild.use_count() after child.reset(): 1

Then release the reference to the Data object from the controlling program:

data.reset();
std::cout << "wdata.use_count() after data.reset(): " << wdata.use_count() << std::endl;
std::cout << "wchild.use_count() after data.reset(): " << wchild.use_count() << std::endl;

The Data object is correctly destructed:

Data @0x7fe6f8500018 is destructed
wdata.use_count() after data.reset(): 0
wchild.use_count() after data.reset(): 0

The circular reference is broken.