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:
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:
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 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.