Ownership and Smart Pointers#

Pointer and Reference#

Raw Pointer#

The raw pointer allows us to directly manipulate the memory. When using a pointer, if it not pointing to an existing object, it is a good practice to point it to nullptr.

std::cout << "PlainData pointer initialized : ";
// It is a good practice to initialize a raw pointer to nullptr.
PlainData * ptr = nullptr;
// Although nullptr will be integer 0, do not use the integer literal 0 or
// the infamous macro NULL to represent nullity.
put_ptr(std::cout, ptr) << std::endl;

nullptr is the null address, which is generally just 0x0:

PlainData pointer initialized : 0x0000000000000000

The full example code can be found in 01_raw_pointer.cpp.

Type of Null Pointer Literal#

In older C, there is a convention to use the macro NULL to for the null address (0x0). NULL should not be used with C++11 and beyond. The macro is merely a number and does not provide correct type information:

// The reason to not use 0 or NULL for the null pointer: they are not even
// of a pointer type!
static_assert(!std::is_pointer<decltype(0)>::value, "error");
static_assert(!std::is_pointer<decltype(NULL)>::value, "error");
// 0 is int
static_assert(std::is_same<decltype(0), int>::value, "error");
// int cannot be converted to a pointer.
static_assert(!std::is_convertible<decltype(0), void *>::value, "error");
// NULL is long
static_assert(std::is_same<decltype(NULL), long>::value, "error");
// long cannot be converted to a pointer, either.
static_assert(!std::is_convertible<decltype(NULL), void *>::value, "error");

C++11 defines the new nullptr pointer literal to represent the null pointer with type information (std::nullptr_t). The compiler may use the type information to perform more rigorous checks:

// Although nullptr is of type std::nullptr_t, not exactly a pointer ...
static_assert(std::is_same<decltype(nullptr), std::nullptr_t>::value, "error");
static_assert(!std::is_pointer<decltype(nullptr)>::value, "error");
// It can be converted to a pointer.
static_assert(std::is_convertible<decltype(nullptr), void *>::value, "error");
static_assert(std::is_convertible<decltype(nullptr), PlainData *>::value, "error");

Dynamic Allocation#

This is how to use a pointer to point to a memory block allocated for a class using malloc():

// Allocate memory for PlainData and get the returned pointer.
std::cout << "PlainData pointer after malloc: ";
ptr = static_cast<PlainData *>(malloc(sizeof(PlainData)));
put_ptr(std::cout, ptr) << std::endl;

The address of the allocated memory is stored in the pointer variable:

PlainData pointer after malloc: 0x00007fdd5e809800

Freeing the memory block takes the pointer:

// After free the memory, the pointer auto variable is not changed.
std::cout << "PlainData pointer after free  : ";
free(ptr);
put_ptr(std::cout, ptr) << std::endl;

Freeing does not touch the pointer variable:

PlainData pointer after free  : 0x00007fdd5e809800

Use new for the allocation:

// Use new to allocate for and construct PlainData and get the returned
// pointer.
std::cout << "PlainData pointer after new   : ";
ptr = new PlainData();
put_ptr(std::cout, ptr) << std::endl;

The allocated memory happens to be the same as that returned by malloc():

PlainData pointer after new   : 0x00007fdd5e809800

delete takes the pointer for deletion:

// After delete, the pointer auto variable is not changed, either.
std::cout << "PlainData pointer after delete: ";
delete ptr;
put_ptr(std::cout, ptr) << std::endl;

delete does not change the pointer variable either:

PlainData pointer after delete: 0x00007fdd5e809800

Reference#

A reference works very similar to a pointer, but unlike a pointer, a reference cannot be used to deallocate or destruct the object it references. In general, a reference is used just like an instance:

void manipulate_with_reference(PlainData & data)
{
    std::cout << "Manipulate with reference      : ";
    put_ptr(std::cout, &data) << std::endl;

    for (size_t it=0; it < 1024*8; ++it)
    {
        data.buffer[it] = it;
    }
    // (... more meaningful work before returning.)

    // We cannot delete an object passed in with a reference.
}
Manipulate with reference      : 0x00007fe94a808800

The full example code for using the reference can be found in 02_reference.cpp.

RAII#

When using a pointer with dynamic memory, we need to make sure the life cycle of the instance is managed correctly. That is, to instantiate the class and destroy the instance at proper places. It is oftentimes cumbersome and error-prone.

The C++ reference makes it easier when we do not need to manage the life cycle. Because a reference cannot be used to destroy an instance, when a programmer sees the use of a reference, it is clear that the lifecycle of the referenced instance is not managed with the reference. Programmers should take the advantage to make the intention of their code clear.

But there are cases that the resource life cycle needs explicit management, and references are not adequate. A better way than the manual control shown above is the technique of RAII (resource acquisition is initialization). The basic concept of RAII is to use the object life cycle to control the resource life cycle.

With RAII, we can relax the treatment of always deleting the object in the same function creating it. RAII is directly related to the concept of ownership we are introducing immediately.

Ownership#

In a practical system, memory (resource) is rarely freed immediately after allocation. The resources are usually manipulated and probably passed around multiple functions. It is not a trivial task to keep track of the life cycle and know when and where to free the resources. To help the management, the concept of ownership is introduced.

Lack of Ownership#

We will use the following example to show what is ownership (the full example code is in 03_ownership.cpp). The example uses a large data object, whose expensive overhead of frequent allocation and deallocation should be avoided.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Data
{

public:

    constexpr const static size_t NELEM = 1024*8;

    using iterator = int *;
    using const_iterator = const int *;

    Data()
    {
        std::fill(begin(), end(), 0);
        std::cout << "Data @" << this << " is constructed" << std::endl;
    }

    ~Data()
    {
        std::cout << "Data @" << this << " is destructed" << std::endl;
    }

    const_iterator cbegin() const { return m_buffer; }
    const_iterator cend() const { return m_buffer+NELEM; }
    iterator begin() { return m_buffer; }
    iterator end() { return m_buffer+NELEM; }

    size_t size() const { return NELEM; }
    int   operator[](size_t it) const { return m_buffer[it]; }
    int & operator[](size_t it)       { return m_buffer[it]; }

    bool is_manipulated() const
    {
        for (size_t it=0; it < size(); ++it)
        {
            const int v = it;
            if ((*this)[it] != v) { return false; }
        }
        return true;
    }

private:

    // A lot of data that we don't want to reconstruct.
    int m_buffer[NELEM];

}; /* end class Data */

void manipulate_with_reference(Data & data)
{
    std::cout << "Manipulate with reference: " << &data << std::endl;

    for (size_t it=0; it < data.size(); ++it)
    {
        data[it] = it;
    }
    // In a real consumer function we will do much more meaningful operations.

    // However, we cannot destruct an object passed in with a reference.
}

The memory allocation and deallocation in the example is separated in two functions. The first function construct Data:

Data * worker1()
{
    // Create a new Data object.
    Data * data = new Data();

    // Manipulate the Data object.
    manipulate_with_reference(*data);

    return data;
}

The second:

/*
 * Code in this function is intentionally made to be lack of discipline to
 * demonstrate how ownership is messed up.
 */
void worker2(Data * data)
{
    // The prerequisite for the caller to write correct code is to read the
    // code and understand when the object is alive.
    if (data->is_manipulated())
    {
        delete data;
    }
    else
    {
        manipulate_with_reference(*data);
    }
}

The example problem first constructs the Data object and uses a raw pointer to hold it:

Data * data = worker1();
std::cout << "Data pointer after worker 1: " << data << std::endl;

We see the process of construction and manipulation:

Data @0x7fb287008800 is constructed
Manipulate with reference: 0x7fb287008800
Data pointer after worker 1: 0x7fb287008800

The second worker function does something that is hard to infer from the function name:

worker2(data);
std::cout << "Data pointer after worker 2: " << data << std::endl;

It destructs the instance that the input pointer points to:

Data @0x7fb287008800 is destructed

We need to be careful that the pointer in the caller remains unchanged, although the instance is destructed:

Data pointer after worker 2: 0x7fb287008800

The second helper function has such a surprising behavior that we can only understand by reading its code, but unfortunately, few programmers have time to read code like this when they are busy implementing requested features. Thus, we call code like this hard to maintain. A consequence of such hard-to-maintain code is that programmers may run into mistake like:

// You have to read the code of worker2 to know that data could be
// destructed.  In addition, the Data class doesn't provide a
// programmatical way to detect whether or not the object is alive.  The
// design of Data, worker1, and worker2 makes it impossible to write
// memory-safe code.
delete data;
std::cout << "Data pointer after delete: " << data << std::endl;

We get a hard crash:

03_ownership(75158,0x114718e00) malloc: *** error for object 0x7f8ef9808800: pointer being freed was not allocated
03_ownership(75158,0x114718e00) malloc: *** set a breakpoint in malloc_error_break to debug

What Is Ownership#

The above example shows the problem of lack of ownership. “Ownership” isn’t officially a language construct in C++, but is a common concept in many programming language for dynamic memory management.

To put it simply, when the object is “owned” by a construct or piece of code, it is assumed that it is safe for the piece of code to use that object. The ownership assures the life of the object, and the object is not destructed when it is owned by someone. It also means that the owner is responsible for making sure the object gets destructed when it should be.

As we observed in the above example code, there is no way for us to let the code know the ownership, and it is unsafe to use the data object after worker2() is called. The way C++ handles the situation is to use smart pointers.

Smart Pointers#

(Modern) C++ provides smart pointers to help manage object life cycles. Since C++11, STL provides two smart pointers: unique_ptr (unique pointer) and shared_ptr (shared pointer). They have different use cases. When using a unique pointer, the pointed object may have at most one unique pointer. But if using shared pointers, the pointed object may have any number of shared pointers. Only one type of smart point may be used at a time. If an object is managed by unique pointer, it may not be used with shared pointer, and vice versa.

In other words, unique_ptr should be used when there can only be one owner of the pointed object, and shared_ptr allows the pointed object to have more than one owner.

Unique Pointer#

We start with unique_ptr because it is lighter-weight. A unique pointer may take the same number of bytes of a raw pointer, and used as a drop-in replacement with a raw pointer.

static_assert
(
    sizeof(Data *) == sizeof(std::unique_ptr<Data>)
  , "unique_ptr should take only a word"
);

The full example code is in 04_unique.cpp. In the new example, we still have 2 worker functions, but they change to use unique pointers.

First worker: returns a unique pointer#
std::unique_ptr<Data> worker1()
{
    // Create a new Data object.
    std::unique_ptr<Data> data = std::make_unique<Data>();

    // Manipulate the Data object.
    manipulate_with_reference(*data);

    return data;
}
Second worker: consumes a unique pointer#
void worker2(std::unique_ptr<Data> data)
{
    if (data->is_manipulated())
    {
        data.reset();
    }
    else
    {
        manipulate_with_reference(*data);
    }
}

The first worker function is called and it returns a unique pointer:

std::unique_ptr<Data> data = worker1();
std::cout << "Data pointer after worker 1: " << data.get() << std::endl;

The results are the same as raw pointer:

Data @0x7fee5a008800 is constructed
Manipulate with reference: 0x7fee5a008800
Data pointer after worker 1: 0x7fee5a008800

There can be at most one unique pointer per object. Thus, we need to move the returned pointer to worker2.

worker2(std::move(data));
std::cout << "Data pointer after worker 2: " << data.get() << std::endl;

The second worker detects that the input data are manipulated, and exercises its right to destruct the object. Because the input pointer was moved into the function, when we try to get the address after worker2, we get null:

Data @0x7fee5a008800 is destructed
Data pointer after worker 2: 0x0

Note

Unique pointer does not have copy constructor and copy assignment operator defined. Trying to copy the pointer object:

worker2(data);

results in compilation error:

04_unique.cpp:97:13: error: call to implicitly-deleted copy constructor of 'std::unique_ptr<Data>'
    worker2(data);
            ^~~~
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:2518:3: note: copy constructor is implicitly deleted because
      'unique_ptr<Data, std::__1::default_delete<Data> >' has a user-declared move constructor
  unique_ptr(unique_ptr&& __u) _NOEXCEPT
  ^
04_unique.cpp:79:36: note: passing argument to parameter 'data' here
void worker2(std::unique_ptr<Data> data)
                                   ^
1 error generated.

Although it is unnecessary, in the end we delete the unique pointer again.

data.reset();
std::cout << "Data pointer after delete: " << data.get() << std::endl;
Execution result of the final deletion#
Data pointer after delete: 0x0

Shared Pointer#

Unlike unique_ptr, shared_ptr allows multiple owners. It maintains a reference counter to achieve the multiple ownership. When a shared pointer object is constructed, the counter increments. When the pointer object (note, not the pointed object) is destructed, the counter decrements. When the counter decrements from 1, the pointed object gets destructed.

std::shared_ptr provides use_count() function for showing the reference counts. This reference counting technique is commonplace for managing ownership, and it appears in many other libraries and languages. The reference counter requires a lot of additional memory, and a shared pointer is always larger than a raw pointer:

static_assert
(
    sizeof(Data *) < sizeof(std::shared_ptr<Data>)
  , "shared_ptr uses more than a word"
);

Note

The additional memory of the std::shared_ptr pointer is not directly used for storing the reference count.

We will use the shared pointer in our 2-worker example. The full example code is in 05_shared.cpp.

First worker: returns a shared pointer#
std::shared_ptr<Data> worker1()
{
    // Create a new Data object.
    std::shared_ptr<Data> data = std::make_shared<Data>();

    std::cout << "worker 1 data.use_count(): " << data.use_count() << std::endl;

    // Manipulate the Data object.
    manipulate_with_reference(*data);

    return data;
}
Second worker: consumes a shared pointer#
void worker2(std::shared_ptr<Data> data)
{
    std::cout << "worker 2 data.use_count(): " << data.use_count() << std::endl;

    if (data->is_manipulated())
    {
        data.reset();
    }
    else
    {
        manipulate_with_reference(*data);
    }
}

Call the first worker function to get the returned shared pointer:

std::shared_ptr<Data> data = worker1();
std::cout << "Data pointer after worker 1: " << data.get() << std::endl;

The first worker function constructs the data object and shows the reference count. The caller also shows the memory address of the managed object:

Data @0x7ffbac500018 is constructed
worker 1 data.use_count(): 1
Manipulate with reference: 0x7ffbac500018
Data pointer after worker 1: 0x7ffbac500018

The shared pointer allows more than one owner. A copy of the shared pointer object is given to the second worker function. The caller and the callee simultaneously own the data object:

worker2(data);
std::cout << "Data pointer after worker 2: " << data.get() << std::endl;

The second worker prints the reference count, and the caller shows the address:

worker 2 data.use_count(): 2
Data pointer after worker 2: 0x7ffbac500018

After the two workers are complete, we destroy the data object in the caller:

std::cout << "main data.use_count(): " << data.use_count() << std::endl;
data.reset();
std::cout << "Data pointer after reset from outside: " << data.get() << std::endl;
std::cout << "main data.use_count(): " << data.use_count() << std::endl;

The data object is destructed after the last shared pointer releases the ownership:

main data.use_count(): 1
Data @0x7ffbac500018 is destructed
Data pointer after reset from outside: 0x0
main data.use_count(): 0

Warning

Only use a shared pointer when it is absolutely necessary. The reference counting is much more expensive than it looks.

When writing C++ code, the rule of thumb is to use smart pointers as much as possible, but start with the unique pointer. A unique pointer forces a developer to think clearly about whether or not multiple owners are necessary.

More on Shared Pointer#

Use of shared pointers is usually tricky. At the first glance, shared pointers allow multiple ownership and seemingly solve all problems of object life cycles. We must not be tricked by the misunderstandings.

In this section, some common guidelines and caveats of using shared pointers will be introduced.

Exclusively Manage Data Object#

Sometimes we know a big resource (our Data class) must not be constructed and destructed frequently, and should be shared among multiple consumers. It should be managed by a shared pointer. The overhead of reference counting is negligible compare to other operations or we simply have to tolerate. In this case, we do not want anyone to directly call the Data constructor:

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

We want to allow only the construction of std::shared_ptr<Data>:

// We want this to work:
std::shared_ptr<Data> sptr1(new Data);
// Or this:
std::shared_ptr<Data> sptr2 = std::make_shared<Data>();

To achieve what we want, we need to solve the problem that , if new Data is forbidden, std::shared_ptr<Data>(new Data) is forbidden too. How can we only turn off the first but not the second?

(The full example code is in 01_fully.cpp.)

Private Constructor (Non-Ideal)#

One idea is to use private constructor:

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

The above design makes the following line work:

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

It is because the constructor of Data is called in the static member function make(). The member function is inside the class Data, and can access the private constructor.

When trying to construct the shared pointer using new, the following code fails to compile:

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

It is because the constructor is private and cannot be called outside of class Data.

It works nice, until we start to consider std::make_shared<Data>(). The function template std::make_shared is useful 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.

However, the use of the private constructor forbids the following code from working:

std::shared_ptr<Data> ret = std::make_shared<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 scene. Simply making friend with that function template may or may not work.

Passkey Pattern#

A sound approach is to use the passkey pattern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Data
{
private:
    class ctor_passkey {};
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 design prohibits calling the constructor:

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

The constructor is deleted:

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, while the compiler shows much more verbose messages:

In file included from 01_fully.cpp:1:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/iostream:37:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/ios:215:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/__locale:14:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/string:504:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/string_view:175:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/__string:57:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/algorithm:643:
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:4398:5: error: static_assert failed due to requirement
      'is_constructible<Data>::value' "Can't construct object in make_shared"
    static_assert(is_constructible<_Tp, _Args...>::value, "Can't construct object in make_shared");
    ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
01_fully.cpp:95:17: note: in instantiation of function template specialization 'std::__1::make_shared<Data>' requested here
    data = std::make_shared<Data>();
                ^
In file included from 01_fully.cpp:1:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/iostream:37:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/ios:215:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/__locale:14:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/string:504:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/string_view:175:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/__string:57:
In file included from /Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/algorithm:643:
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:2201:46: error: call to deleted constructor of 'Data'
  __compressed_pair_elem(__value_init_tag) : __value_() {}
                                             ^
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:2294:42: note: in instantiation of member function
      'std::__1::__compressed_pair_elem<Data, 1, false>::__compressed_pair_elem' requested here
      : _Base1(std::forward<_U1>(__t1)), _Base2(std::forward<_U2>(__t2)) {}
                                         ^
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:3567:12: note: in instantiation of function template specialization
      'std::__1::__compressed_pair<std::__1::allocator<Data>, Data>::__compressed_pair<std::__1::allocator<Data>, std::__1::__value_init_tag>'
      requested here
        :  __data_(_VSTD::move(__a), __value_init_tag()) {}
           ^
/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/memory:4405:26: note: in instantiation of member function
      'std::__1::__shared_ptr_emplace<Data, std::__1::allocator<Data> >::__shared_ptr_emplace' requested here
    ::new(__hold2.get()) _CntrlBlk(__a2, _VSTD::forward<_Args>(__args)...);
                         ^
01_fully.cpp:95:17: note: in instantiation of function template specialization 'std::__1::make_shared<Data>' requested here
    data = std::make_shared<Data>();
                ^
01_fully.cpp:22:5: note: 'Data' has been explicitly marked deleted here
    Data() = delete;
    ^
2 errors generated.

Now we return to our 2-worker example:

First worker: obtain the shared pointer from the factory method#
std::shared_ptr<Data> worker1()
{
    // Create a new Data object.
    std::shared_ptr<Data> data;
    data = Data::make();
    std::cout << "worker 1 data.use_count(): " << data.use_count() << std::endl;

    // Manipulate the Data object.
    manipulate_with_reference(*data);

    return data;
}
Second worker: consumes the shared pointer#
void worker2(std::shared_ptr<Data> data)
{
    std::cout << "worker 2 data.use_count(): " << data.use_count() << std::endl;

    if (data->is_manipulated())
    {
        data.reset();
    }
    else
    {
        manipulate_with_reference(*data);
    }
}

Call the first worker function:

std::shared_ptr<Data> data = worker1();
std::cout << "Data pointer after worker 1: " << data.get() << std::endl;

The Data object is constructed and returned with a shared pointer:

Data @0x7fb36ad00018 is constructed
worker 1 data.use_count(): 1
Manipulate with reference: 0x7fb36ad00018
Data pointer after worker 1: 0x7fb36ad00018

Call the second worker function:

worker2(data);
std::cout << "Data pointer after worker 2: " << data.get() << std::endl;

The Data object is properly sent to the second worker:

worker 2 data.use_count(): 2
Data pointer after worker 2: 0x7fb36ad00018

After the worker functions, destroy the Data object:

data.reset();
std::cout << "Data pointer after reset from outside: " << data.get() << std::endl;
std::cout << "main data.use_count(): " << data.use_count() << std::endl;

The object is properly destructed:

Data @0x7fb36ad00018 is destructed
Data pointer after reset from outside: 0x0
main data.use_count(): 0

Get Shared Pointer from inside Object#

Occasionally we get the Data object without the managing shared pointer object, but still want to return the ownership to the caller.

Never Recreate from Raw Pointer#

A wrong way to do it is to recreate the shared pointer object using the raw pointer, e.g., the following example. (The full code of the example is in 02_duplicate.cpp.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Data
{
public:
    Data * get_raw_ptr()
    {
        // Returning raw pointer discards the ownership management.
        return this;
    }

    std::shared_ptr<Data> get_shared_ptr()
    {
        // XXX: Recreate a shared_ptr will duplicate the reference counter, and
        // later results into 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 results in double free in the caller.

To see the problem, first create the Data object:

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

The created object is held in a shared pointer:

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

Then we call the problematic function get_shared_ptr():

// This is the problematic call that creates an ill-formed shared pointer.
std::shared_ptr<Data> holder2 = data->get_shared_ptr();
std::cout << "a bad shared pointer is created" << std::endl;

We get the pointer:

a bad shared pointer is created

Release the original shared pointer:

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

The Data object is destructed:

Data @0x7faaf0d00018 is destructed
data.use_count() after data.reset(): 0

Now, release the second, ill-formed shared pointer:

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

It can never reach the last line, since releasing the pointer results into double free:

holder2.use_count(): 1
Data @0x7faaf0d00018 is destructed
02_duplicate(76813,0x10d1c7e00) malloc: *** error for object 0x7faaf0d00018: pointer being freed was not allocated
02_duplicate(76813,0x10d1c7e00) malloc: *** set a breakpoint in malloc_error_break to debug

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. The full code of the example is in 03_fromthis.cpp. It requires two things:

  1. Inherit the Data class from enable_shared_from_this.

  2. Call the inherited member function shared_from_this().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 we 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 not double free any more.

Avoid Circular Reference#

Circular (or cyclic) reference means two objects contain pointers that point to each other. The circle does not need to have only two objects. It may contain three or more objects. The circular reference is not a problem when none of the pointers owns other objects. It becomes a problem when the pointers are smart pointers, and specifically the shared pointer. Here we use the simplest case of the 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 full code is in 04_cyclic.cpp.) The circular reference creates a memory leak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 {};
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;
};
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;
Data @0x7f8f48d00018 is constructed
data.use_count(): 2
child.use_count(): 2

(Here we create two weak_ptr objects. The weak pointers can access the reference count of the pointed objects of the shared pointers but do not own the objects. They will be used for peaking the counts.)

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;

There is still one reference to Data remaining:

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!

Use Weak Pointer to Work around#

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Child
  : public std::enable_shared_from_this<Child>
{
private:
    class ctor_passkey {};
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;
};

(The full code is in 05_weak.cpp.)

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;

Because now Child does not own Data, both the reference count to the Data and Child objects reduce to 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.

Reminder: Avoid Weak Pointer#

Using weak pointers to break circular reference should only be considered as a workaround, rather than a full resolution. We sometimes need it since the reference circle may not be as obvious as it is in our example. For example, there may be 3 or 4 levels of references in the cycle. Weak pointers have a similar interface to shared pointers. When we are troubleshooting resource-leaking issues, replacing std::shared_ptr with std::weak_ptr can work as a quick-n-dirty fix.

The right treatment is to sort out the ownership. It’s not easy when the system is complex. The rule of thumb is that, as we mentioned earlier, you should avoid using a shared pointer unless you really need it. And most of the time the need appears in a higher-level and heavy-weight container, rather than the lower-level small objects. For small objects, we should try to limit the life cycle and use raw pointers or just the stack.

Exercises#

  1. Write code so that when std::unique_ptr is destructed, the object it points to doesn’t destruct.

  2. Create vectors of 1,000,000 elements of (i) raw pointers, (ii) unique_ptr, and (iii) shared_ptr, respectively, and measure difference of the performance.

  3. Compare the runtime performance between shared_ptr(new Type) and make_shared<Type>. Explain why there is a difference of performance.

References#