Cippi maintains the garden.
First, what is a resource? A resource is something that you have to manage. That means you have to acquire and release it because resources are limited, or you have to protect it. You can have only a limited amount of memory, sockets, processes, or threads; only one process can write a shared file or one thread can write a shared variable at one point in time. If you don’t follow the protocol, many issues are possible.
If you think about resource management, it all boils down to one critical point: ownership. What I like in particular about modern C++ is that we can directly express our intention about ownership in code.
Local objects: The C++ run time, as the owner, automatically manages the lifetime of these resources. The same holds for global objects or members of a class. The guidelines call them scoped objects.
References: I’m not the owner. I only borrowed the resource that cannot be empty.
Raw pointers: I’m not the owner. I only borrowed the resource that can be empty. I must not delete the resource.
std::unique_ptr
: I’m the exclusive owner of the resource. I may explicitly release the resource.
std::shared_ptr
: I share the resource with other shared_ptr
s and release the resource if I’m the last owner. I may explicitly release my share of the ownership.
std::weak_ptr
: I’m not the owner of the resource, but I may temporarily become a shared owner of the resource by using the member function std::weak_ptr.lock()
.
Although this section has six rules, only two of them, RAII and scoped objects, are original. Two of them are already part of other sections:
R.2: In interfaces, use raw pointers to denote individual objects (only) (see I.13: Do not pass an array as a single pointer)
R.6: Avoid non-const
global variables (see I.2: Avoid non-const
global variables)
The remaining four about the semantics of pointers and references extend existing rules.
The first general rule is idiomatic for C++: RAII. RAII stands for Resource Acquisition Is Initialization. The C++ standard library systematically relies on RAII.
The idea of RAII is simple. You create a kind of proxy object for your resource. The constructor of the proxy acquires the resource, and the destructor of the proxy releases the resource. The central idea of RAII is that the C++ run time is the owner of this proxy as a local object and, therefore, of the resource. When the proxy object as a local object goes out of the scope, the destructor of the proxy is automatically called. Consequently, we get deterministic destruction behavior in C++.
RAII is heavily used in the C++ ecosystem. Examples of RAII are the containers of the Standard Template Library (STL), smart pointers, and locks. Containers take care of their elements, smart pointers take care of their memory, and locks take care of their mutexes.
The following class ResourceGuard
models RAII.
1 // raii.cpp 2 3 #include <iostream> 4 #include <new> 5 #include <string> 6 7 class ResourceGuard { 8 public: 9 explicit ResourceGuard(const std::string& res):resource(res){ 10 std::cout << "Acquire the " << resource << "." << '\n'; 11 } 12 ~ResourceGuard(){ 13 std::cout << "Release the "<< resource << "." << '\n'; 14 } 15 private: 16 std::string resource; 17 }; 18 19 int main() { 20 21 std::cout << '\n'; 22 23 ResourceGuard resGuard1{"memoryBlock1"}; 24 25 std::cout << "\nBefore local scope" << '\n'; 26 { 27 ResourceGuard resGuard2{"memoryBlock2"}; 28 } 29 std::cout << "After local scope" << '\n'; 30 31 std::cout << '\n'; 32 33 34 std::cout << "\nBefore try-catch block" << '\n'; 35 try { 36 ResourceGuard resGuard3{"memoryBlock3"}; 37 throw std::bad_alloc(); 38 } 39 catch (const std::bad_alloc& e) { 40 std::cout << e.what(); 41 } 42 std::cout << "\nAfter try-catch block" << '\n'; 43 44 std::cout << '\n'; 45 46 }
ResourceGuard
is the guard that manages its resource. In this case, the resource is a simple string. ResourceGuard
creates the resource in its constructor (lines 9–11) and releases the resource in its destructor (lines 12–14). It does its job very reliably. The creation and the releasing of the resource is only indicated in the constructor and in the destructor.
The C++ run time calls the destructor of resGuard1
(line 23) exactly at the end of the main
function (line 46). The lifetime of resGuard2
(line 27) already ends in line 28. Therefore, the C++ run time calls the destructor once more. Even the throwing of the exception std::bad_alloc
does not affect the reliability of resGuard3
(line 36). Its destructor is called at the end of the try block (lines 35–38).
Figure 7.1 displays the lifetime of the objects.
Figure 7.1 Resource Acquisition Is Initialization
and
Both rules generalize the ownership aspect of the rule about passing pointers or references to functions and the rule about when to return a pointer (T*
) or an lvalue reference (T&
) from a function. The critical question for pointers and references is, Who is the owner of the resource? If you are not the owner but just borrowed it, you must not delete the resource.
The rule about scoped objects is probably the most important rule for resource management in order to make it simple. If possible, use a scoped object.
A scoped object is an object with its scope. That may be a local object, a global object, or a member of a class. The C++ run time takes care of the scoped objects. There is no memory allocation and deallocation involved, and you cannot get a std::bad_alloc
exception.
Why is the following example bad?
void f(int n) { auto* p = new Gadget{n}; // ... delete p; }
There is no need to create Gadget
on the heap. It costs time, and it is error prone. You may forget to deallocate the memory, or an exception may happen before the delete call. In the end, you have a memory leak. Just use a local object and you are safe by design.
void f(int n) { Gadget g{n}; // ... }
Maybe you are a little bit bewildered? The C++ Core Guidelines have only four rules for allocation and deallocation. Three of the four rules are about smart pointers. In the end, the essence of this section is that you should use smart pointers, which are the topic of the following section.
Before I dive into the four rules, let me give you a little background that is necessary for understanding the rules. Creating an object in C++ with new consists of two steps.
Allocate the memory for the object.
Construct the object into the allocated memory.
operator new
or operator new []
is the first step; the constructor is the second step.
The same strategy applies to the destruction but the other way around. First, the destructor (if any) is called, and then the memory is deallocated with operator delete
or operator delete []
.
What is the difference between new
and malloc
, or delete
and free
? The C functions malloc
and free
do only half of the job. malloc
allocates the memory, and free
deallocates the memory. Neither does malloc
invoke the constructor nor does free
invoke the destructor.
This means if you use an object that was just created via malloc
, your program has undefined behavior.
// mallocVersusNew.cpp #include <iostream> #include <string> struct Record { explicit Record(const std::string& na): name(na) {} std::string name; }; int main() { Record* p1 = static_cast<Record*>(malloc(sizeof(Record))); // (1) std::cout << p1->name << '\n'; auto p2 = new Record("Record"); // (2) std::cout << p2->name << '\n'; }
I allocate memory only for the Record
object (1). The result is that the output p1->name
call in the following line is undefined behavior. Undefined behavior just means that you cannot make any assumption about the behavior of the program. On repeated runs, I got no output; the expected output, which is an empty string; and a core dump. See Figure 7.2.
Figure 7.2 Undefined behavior causes a core dump
In contrast, the call (2) invokes the constructor.
You should keep this rule in mind. The emphasis in this rule lies in the word explicitly because using smart pointers or containers of the STL gives you objects that implicitly use new
and delete
.
For example, here are a few variations to create std::unique_ptr
and std::shared_ptr
.
std::unique_ptr<int> uniq1(new int(2011)); // (1) std::unique_ptr<int> uniq2 = std::make_unique<int>(2014); std::shared_ptr<int> shar1(new int(2011)); // (1) std::shared_ptr<int> share2 = std::make_shared<int>(2014);
If you don’t know which version you should prefer, the rules “R.22: Use make_shared()
to make shared_ptr
s” and “R.23: Use make_unique()
to make unique_ptr
s” give you the definite answer.
You cannot entirely avoid the calls in (1). When you want to create a std:::unique_ptr
or a std::shared_ptr
, which shouldn’t use the destructor of the underlying type, the following syntax is obligatory:
std::shared_ptr<int> shar1(new int(2011), MyIntDeleter());
Immediately give the result of an explicit resource allocation to a manager object |
The C++ community loves acronyms. For memory allocation, there is a special name for this rule: NNN. NNN stands for No Naked New and means the result of a memory allocation should be given to a manager object. This manager object could be a std::unique_ptr
or a std::shared_ptr
. Of course, this rule has a broader context. For example, containers of the STL know how to take care of their elements, or locks know how to take care of their mutexes.
When you don’t follow these rules, the danger of undefined behavior lurks.
// standaloneAllocation.cpp // Bad: because of double free #include <iostream> #include <memory> struct MyInt{ explicit MyInt(int myInt):i(myInt) {} ~MyInt() { std::cout << "Goodbye from " << i << '\n'; } int i; }; int main() { std::cout << '\n'; MyInt* myInt = new MyInt(2011); std::unique_ptr<MyInt> uniq1 = std::unique_ptr<MyInt>(myInt); std::unique_ptr<MyInt> uniq2 = std::unique_ptr<MyInt>(myInt); std::cout << '\n'; }
The class MyInt
displays in its destructor the value of the member attribute i_
. The issue starts with the standalone allocation (MyInt* myInt = new MyInt(2011
)). Either uniq1
or uniq2
is the owner of myInt
, but not both. Due to the two owners, two deal-locations of the memory happen, which is undefined behavior. See Figure 7.3.
Figure 7.3 Two owners with std::unique_ptr
Two deallocations of myInt
happen at the end of the main
function. The first deallocation via the handle is fine, but the second causes undefined behavior. The value of the member attribute i_
is 0 in the second case.
When using std::make_unique
, you avoid the risk of double-free problems:
int main() { std::cout << '\n'; std::unique_ptr<MyInt> uniq = std::make_unique<int>(2011); std::cout << '\n'; }
Perform at most one explicit resource allocation in a single expression statement |
This rule is a little bit tricky.
void func(std::shared_ptr<Widget> sp1, std::shared_ptr<Widget> sp2) { ... } func(std::shared_ptr<Widget>(new Widget(1)), std::shared_ptr<Widget>(new Widget(2)));
This function call is not exception safe and may, therefore, result in a memory leak. Why? The reason is that four operations must be performed to initialize both shared pointers.
Allocate memory for Widget(1)
.
Construct Widget(1)
.
Allocate memory for Widget(2)
.
Construct Widget(2)
.
Up to C++14, the compiler is free to first allocate the memory for Widget(1)
and Widget(2)
and then construct both. From the optimization perspective, this makes much sense because one memory allocation of two Widget
s is very likely faster than two allocations of one Widget
. This means the following instructions could happen:
Allocate memory for Widget(1)
.
Allocate memory for Widget(2)
.
Construct Widget(1)
.
Construct Widget(2)
.
If one of the constructors throws an exception, the memory of the other object is not automatically freed and we get a memory leak.
It’s easy to overcome this issue by using the factory function std::make_shared
for creating a std::shared_ptr
.
func(std::make_shared<Widget>(1), std::make_shared<Widget>(2));
std::make_shared
guarantees that the function call has no effect if an exception is thrown. The analogous function std::make_unique
for creating a std::unique_ptr
gives the same guarantee.
From the library perspective, the smart pointers were the most important addition to the C++11 standard. The C++ Core Guidelines have more than ten rules related to std::unique_ptr
, std::shared_ptr
, and std::weak_ptr
. The rules for smart pointers boil down to two categories: the basic usage of smart pointers as owners and smart pointers as function parameters.
I assume in this section a basic familiarity with smart pointers. If you want to know all the details, read the documentation for std::unique_ptr
, std::shared_ptr
, and std::weak_ptr
.
For completeness, I also include std::weak_ptr
in this rule. Modern C++ has three smart pointers for expressing three different kinds of ownership.
std::unique_ptr
: exclusive owner
std::shared_ptr
: shared owner
std::weak_ptr
: non-owning reference to an object that is managed by a std::shared_ptr
A std::unique_ptr
is the exclusive owner of its resource. A std::unique_ptr
cannot be copied, only moved.
auto uniquePtr1 = std::make_unique<int>(1998); auto uniquePtr2(std::move(uniquePtr1));
In contrast, a std::shared_ptr
shares ownership. If you copy or copy assign a shared pointer, the reference counter is increased; if you delete or reset a shared pointer, the reference counter is decreased. If the reference counter becomes zero, the underlying resource will be deleted.
auto sharedPtr1 = std::make_shared<int>(1998) // reference count 1 auto sharedPtr2(sharedPtr1); // reference count 2
A std::weak_ptr
is not a smart pointer. It has a reference to an object that is managed by a std::shared_ptr
. Its interface is quite limited and doesn’t allow the transparent access on the underlying resource. By using the member function lock
on a std::weak_ptr
, you can create a std::shared_ptr
from a std::weak_ptr
.
auto sharedPtr1 = std::make_shared<int>(1998) // reference count 1 std::weak_ptr<int> weakPtr1(sharedPtr1); // reference count 1 auto sharedPtr2 = weakPtr1.lock(); // reference count 2
Prefer |
The std::unique_ptr
should always be your first choice if you need a smart pointer. A std::unique_ptr
is per design as fast and as memory efficient as a raw pointer.
This observation does not hold for a std::shared_ptr
. A std::shared_ptr
needs to manage its reference counter and allocate extra memory for maintaining the control block. The control block is necessary to manage the lifetime of the controlled object. The std::shared_ptr
shines when you need shared ownership. In this case, allocating the shared resource only once may spare memory and time.
Don’t use a std::shared_ptr
for convenience reasons because you want to copy it. A std::unique_ptr
cannot be copied, but it can be moved.
1 // moveUniquePtr.cpp 2 3 #include <algorithm> 4 #include <iostream> 5 #include <memory> 6 #include <utility> 7 #include <vector> 8 9 void takeUniquePtr(std::unique_ptr<int> uniqPtr) { 10 std::cout << "*uniqPtr: " << *uniqPtr << '\n'; 11 } 12 13 int main() { 14 15 std::cout << '\n'; 16 17 auto uniqPtr1 = std::make_unique<int>(2011); 18 19 takeUniquePtr(std::move(uniqPtr1)); 20 21 auto uniqPtr2 = std::make_unique<int>(2014); 22 auto uniqPtr3 = std::make_unique<int>(2017); 23 24 std::vector<std::unique_ptr<int>> vecUniqPtr {}; 25 vecUniqPtr.push_back(std::move(uniqPtr2)); 26 vecUniqPtr.push_back(std::move(uniqPtr3)); 27 vecUniqPtr.push_back(std::make_unique<int>(2020)); 28 29 std::cout << '\n'; 30 31 std::for_each(vecUniqPtr.begin(), vecUniqPtr.end(), 32 [](std::unique_ptr<int>& uniqPtr) { 33 std::cout << *uniqPtr << '\n'; 34 }); 35 36 std::cout << '\n'; 37 38 }
The function takeUniquePtr
(line 9) takes a std::unique_ptr
by value. The key observation is that you have to move the std::unique_ptr
inside. The same argument holds for the std::vector<std::unique_ptr<int>>
(line 24). std::vector
, like all containers of the standard template, uses copy semantics. The container wants to own its elements but copying a std::unique_ptr
is not possible. std::move
solves this issue (lines 25 and 26). Directly constructing the std::unique_ptr
is also possible (line 27). You can apply an algorithm such as std::for_each
on the std::vector
<std::unique_ptr<int>>
(line 31) if no copy semantics is used internally.
Finally, Figure 7.4 shows the output of the program.
Figure 7.4 Moving a std::unique_ptr
and
There are two reasons to prefer std::make_unique
to std::unique_ptr
and to prefer std::make_shared
to std::shared_ptr
.
The first reason is exception safety. Read the details in the previous rule “R.13: Perform at most one explicit resource allocation in a single expression statement.”
The second reason holds only for std::shared_ptr
.
auto sharPtr1 = std::shared_ptr<int>(new int(1998)); auto sharPtr2 = std::make_shared<int>(1998);
When you call std::shared_ptr<int>(new int(1998)
), two memory allocations are involved: one allocation for new int(1998)
and the second for the control block of the std::shared_ptr
. Memory allocation is expensive. Therefore, you should avoid it. std::make_shared<int>(1998)
makes out of two memory allocations one and is, therefore, faster. Additionally, the allocated object (new int(1998)
) and the control block are next to each other and can, therefore, be accessed faster.
You get cyclic references of std::shared_ptr
if the std::shared_ptr
s reference each other. For example, a doubly linked list creates cycles. If you implement the links with std::shared_ptr
, your reference counter never becomes zero, and you end up with a memory leak. Here is a short example.
There are two cycles in Figure 7.5: first, between the mother and her daughter; second, between the mother and her son. The subtle difference is, however, that the mother references her daughter with a std::weak_ptr
. So there’s a std::shared_ptr
cycle between mother and son keeping both objects alive, while there is no std::shared_ptr
cycle between mother and daughter, which allows the daughter to be deleted.
Figure 7.5 Cycles of smart pointers
If you don’t like images, here is the corresponding source code.
1 // cycle.cpp 2 3 #include <iostream> 4 #include <memory> 5 6 struct Son; 7 struct Daughter; 8 9 struct Mother { 10 ~Mother() { 11 std::cout << "Mother gone" << '\n'; 12 } 13 void setSon(const std::shared_ptr<Son> s) { 14 mySon = s; 15 } 16 void setDaughter(const std::shared_ptr<Daughter> d) { 17 myDaughter = d; 18 } 19 std::shared_ptr<Son> mySon; 20 std::weak_ptr<Daughter> myDaughter; 21 }; 22 23 struct Son { 24 explicit Son(std::shared_ptr<Mother> m): myMother(m) {} 25 ~Son() { 26 std::cout << "Son gone" << '\n'; 27 } 28 std::shared_ptr<Mother> myMother; 29 }; 30 31 struct Daughter { 32 explicit Daughter(std::shared_ptr<Mother> m): myMother(m) {} 33 ~Daughter() { 34 std::cout << "Daughter gone" << '\n'; 35 } 36 std::shared_ptr<Mother> myMother; 37 }; 38 39 int main() { 40 41 std::shared_ptr<Mother> m = std::make_shared<Mother>(); 42 std::shared_ptr<Son> s = std::make_shared<Son>(m); 43 std::shared_ptr<Daughter> d = std::make_shared<Daughter>(m); 44 m->setSon(s); 45 m->setDaughter(d); 46 47 }
At the end of the main
function, the lifetime of the mother, the son, and the daughter ends. Or to say it the other way around: mother, son, and daughter go out of scope, and therefore, the destructor of the class
Mother
(lines 10–12), Son
(lines 25–27), and Daughter
(lines 33–35) should automatically be invoked.
“Should” because only the destructor of the daughter is called. See Figure 7.6.
Figure 7.6 Cycles of smart pointers
Due to the cycle of std::shared_ptr
s between the mother and the son, the reference counter is always greater than zero and the destructor is not automatically invoked. That observation is not true for the mother and the daughter. If the daughter goes out of scope, she is automatically deleted.
The remaining rules in this section answer the question, How should a function take smart pointers as parameters? Should the parameter be a std::unique_ptr
or a std::shared_ptr
? Should the argument be taken by const
or by reference? You should perceive these rules for smart pointers as function parameters as a refinement of the more general previous rules for the parameter passing of function parameters: See Parameter Passing: In and Out and Parameter Passing: Ownership Semantics in Chapter 4.
Before I dive into the rules, Table 7.1 presents an overview first.
Table 7.1 Smart pointers as function parameters
Function signature |
Semantics |
Rule |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The table has five rules. Two rules for using smart pointers as parameters are still missing. First, we have to answer the question of when to use smart pointers as function parameters. Second, there are dangers involved if a function takes its parameters by reference.
Let’s answer the first question: When should smart pointers be used as function parameters?
R.30 | Take smart pointers as parameters only to explicitly express lifetime semantics |
If you pass a smart pointer as a parameter to a function, and in this function, you use only the underlying resource of the smart pointer, you are doing something wrong. In this case, you should use a raw pointer or a reference as a function parameter because you don’t need the lifetime semantics of a smart pointer.
Let me give you an example showing the quite sophisticated lifetime management of a smart pointer.
1 // lifetimeSemantic.cpp 2 3 #include <iostream> 4 #include <memory> 5 6 using std::cout; 7 8 void asSmartPointerGood(std::shared_ptr<int>& shr) { 9 cout << "asSmartPointerGood \n"; 10 cout << " shr.use_count(): " << shr.use_count() << '\n'; 11 shr.reset(new int(2011)); 12 cout << " shr.use_count(): " << shr.use_count() << '\n'; 13 cout << "asSmartPointerGood \n"; 14 } 15 16 void asSmartPointerBad(std::shared_ptr<int>& shr) { 17 cout << "asSmartPointerBad(sharedPtr2) \n"; 18 *shr += 19; 19 } 20 21 int main() { 22 23 cout << '\n'; 24 25 auto sharedPtr1 = std::make_shared<int>(1998); 26 auto sharedPtr2 = sharedPtr1; 27 cout << "sharedPtr1.use_count(): " << sharedPtr1.use_count() 28 << '\n'; 29 cout << '\n'; 30 31 asSmartPointerGood(sharedPtr1); 32 33 cout << '\n'; 34 35 cout << "*sharedPtr1: " << *sharedPtr1 << '\n'; 36 cout << "sharedPtr1.use_count(): " << sharedPtr1.use_count() 37 << '\n'; 38 cout << '\n'; 39 40 cout << "*sharedPtr2: " << *sharedPtr2 << '\n'; 41 cout << "sharedPtr2.use_count(): " << sharedPtr2.use_count() 42 << '\n'; 43 cout << '\n'; 44 45 asSmartPointerBad(sharedPtr2); 46 cout << "*sharedPtr2: " << *sharedPtr2 << '\n'; 47 48 cout << '\n'; 49 50 }
Let me start with the good case for a std::shared_ptr
. The reference counter at line 27 is 2 because I used the shared pointer sharedPtr1
to initialize sharedPtr2
. Let’s have a closer look at the invocation of the function asSmartPointerGood
(line 8). In line 10, the reference count of shr
is 2, and then it becomes 1 in line 12. What happened in line 11? I reset shr
to the new resource: new
int(2011)
. Consequently, both the shared pointer sharedPtr1
and sharedPtr2
are immediately owners of different resources. You can observe the behavior in Figure 7.7.
Figure 7.7 Lifetime semantics of smart pointers
When you invoke reset
on a shared pointer sharedPtr
, a sophisticated workflow happens under the hood:
If you invoke reset
without an argument on sharedPtr
, the reference counter is decreased by one. Afterward, sharedPtr
is not an owner anymore.
If you invoke reset
with an argument and the reference counter is at least 2, you get two independent shared pointers owning different resources.
If you invoke reset
with or without an argument and the reference counter becomes 0, the resource is released.
The semantics of the argument of asSmartPointerBad(std::shared_ptr<int>& shr)
suggests that you might reseat the smart pointer in the method, but the method does not have any intent to do so.
So the user of your method is pushed into the wrong direction.
This magic is overkill if you are only interested in the underlying resource of the shared pointer; therefore, a raw pointer or a reference is the right kind of parameter for the function asSmartPointerBad
(line 16).
std::unique_ptr
There are two rules regarding std::unique_ptr
parameters:
R.32: Take a unique_ptr<widget>
parameter to express that a function assumes ownership of a Widget
R.33: Take a unique_ptr<widget>&
parameter to express that a function reseats the Widget
Here are the two corresponding function signatures:
void sink(std::unique_ptr<Widget>
) void reseat(std::unique_ptr<Widget>
&)
std::unique_ptr<Widget> When a function takes ownership of a Widget
, you should take the std::unique_ptr<Widget>
by value. The consequence is that the caller has to move the std::unique_ptr<Widget>
.
// uniqPtrMove.cpp #include <memory> #include <utility> struct Widget { explicit Widget(int) {} }; void sink(std::unique_ptr<Widget> uniqPtr) { // do something with uniqPtr, then dispose of it } int main() { auto uniqPtr = std::make_unique<Widget>(1998); sink(std::move(uniqPtr)); // OK sink(uniqPtr); // ERROR }
The call sink(std::move(uniqPtr))
is fine, but the call sink(uniqPtr)
breaks because you cannot copy a std::unique_ptr
. When your function only wants to use a Widget
, it should take its parameter, according to the previous rule “R.30: Take smart pointers as parameters only to explicitly express lifetime semantics, by pointer or by reference.”
std::unique_ptr<Widget>& Sometimes a function wants to reseat a Widget
. In this case, you should pass the std::unique_ptr<Widget>
by a non-const
reference.
// uniqPtrReference.cpp #include <memory> #include <utility> struct Widget{ Widget(int) {} }; void reseat(std::unique_ptr<Widget>& uniqPtr) { uniqPtr.reset(new Widget(2003)); // do something with uniqPtr } int main() { auto uniqPtr = std::make_unique<Widget>(1998); reseat(std::move(uniqPtr)); // ERROR reseat(uniqPtr); // OK }
Now the call reseat(std::move(uniqPtr))
fails because you cannot bind an rvalue to a non-const
lvalue reference. This error does not hold for the call in the following line: reseat(uniqPtr)
. An lvalue can be bound to an lvalue reference. By the way, the uniqPtr.reset(new Widget(2003))
generates a new Widget(2003)
and destructs the old Widget(1998)
.
Two of the three rules for std::shared_ptr
are repetitions; therefore, I will not bother you with details.
std::shared_ptr
There are three rules about parameters of type std::shared_ptr
:
Here are the relevant function signatures for std::shared_ptr
:
void share(std::shared_ptr<Widget>
); void reseat(std::shared_ptr<Widget>
&); void mayShare(conststd::shared_ptr<Widget>
&);
Let’s look at each function signature in isolation. What does this mean from the function perspective?
void share(std::shared_ptr<Widget>):
I’m a shared owner of the Widget
during the lifetime of the function. At the beginning of the function, I increase the reference counter; at the end of the function, I decrease the reference counter; therefore, the Widget
stays alive as long as I use it.
void reseat(std::shared_ptr<Widget>&):
I’m not a shared owner of the Widget
because I do not change the reference counter. I have no guarantee that the Widget
stays alive during the execution of the function, but I can reseat the resource.
void mayShare(const std::shared_ptr<Widget>&):
I only borrow the resource. I cannot extend the lifetime of the resource, nor can I reseat the resource. Honestly, it would be best if you used a pointer (Widget*
) or a reference (Widget&
) as a parameter instead because there is no added value in using a const std::shared_ptr<Widget>&
as a parameter.
Do not pass a pointer or reference obtained from an aliased smart pointer |
First of all, the title of this rule may be misleading. An aliased smart pointer (reference to a smart pointer) is a smart pointer, of which you are not the owner. Violating this rule often ends in a dangling pointer.
The code snippet exemplifies the problem.
void oldFunc(Widget* wid){ // do something with wid } void shared(std::shared_ptr<Widget>& shaPtr){ oldFunc(*shaPtr); // do something with shaPtr } auto globShared = std::make_shared<Widget>(2011); ... shared(globShared);
globShared
is a globally shared pointer. The function shared
takes its argument by reference. Therefore, the reference counter of shaPtr
as the aliased smart pointer is not increased and the function share
does not extend the lifetime of Widget(2011)
. The issue begins with the call oldFunc(*shaPtr)
. oldFunc
accepts a pointer to the Widget
; therefore, oldFunc
has no guarantee that the Widget
stays alive during its execution. oldFunc
only borrows the Widget
.
The cure is simple. You have to ensure that the reference count of globShared
is increased before the call to the function oldFunc
.
Pass the std::shared_ptr
by value to the function shared
:
void shared(std::shared_ptr<Widget> shaPtr) {
oldFunc(*shaPtr);
// do something with shaPtr
}
Make a copy of the shaPtr
in the function shared
:
void shared(std::shared_ptr<Widget>
& shaPtr) {
auto keepAlive = shaPtr;
oldFunc(*shaPtr);
// do something with keepAlive or shaPtr
}
Let me formulate the cure as a straightforward rule: You should access a shared resource only if you actually hold a share in its ownership.
The same reasoning also applies to std::unique_ptr
, but there is no simple cure because you cannot copy a std::unique_ptr
.
The general rules for resource management have a strong overlap with the existing rules regarding functions and interfaces (see Chapter 4, Functions).
The guidelines addressing smart pointers as function parameters are a refinement of the previous rules regarding the parameter passing of function parameters: See Parameter Passing: In and Out and Parameter Passing: Ownership Semantics in Chapter 4.