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:
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:
Inherit the Data class from enable_shared_from_this.
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.