RAII
original code example
#include <iostream>
int main()
{
double den[] = {1.0, 2.0, 3.0, 4.0, 5.0};
for (int i = 0; i < 5; ++i)
{
// allocate the resource on the heap
double *en = new double(i);
// use the resource
std::cout << *en << "/" << den[i] << " = " << *en / den[i] << std::endl;
// deallocate the resource
delete en;
}
return 0;
}
Let us therefore use the principles of RAII to create a management class that calls delete automatically:
class MyInt
{
int *_p; // pointer to heap data
public:
MyInt(int *p = NULL) { _p = p; }
~MyInt()
{
std::cout << "resource " << *_p << " deallocated" << std::endl;
delete _p;
}
int &operator*() { return *_p; } // // overload dereferencing operator
};
In this example, the constructor of class MyInt takes a pointer to a memory resource. When the destructor of a MyInt object is called, the resource is deleted from memory - which makes MyInt an RAII memory management class. Also, the * operator is overloaded which enables us to dereference MyInt objects in the same manner as with raw pointers. Let us therefore slightly alter our code example from above to see how we can properly use this new construct:
Let us therefore slightly alter our code example from above to see how we can properly use this new construct:
int main()
{
double den[] = {1.0, 2.0, 3.0, 4.0, 5.0};
for (size_t I = 0; I < 5; ++i)
{
// allocate the resource on the stack
MyInt en(new int(i));
// use the resource
std::cout << *en << "/" << den[i] << " = " << *en / den[i] << std::endl;
}
return 0;
}
Let us break down the resource allocation part in two steps:
The part new int(i) creates a new block of memory on the heap and initializes it with the value of i. The returned result is the address of the block of memory.
The part MyInt en(…)calls the constructor of class MyInt, passing the address of a valid memory block as a parameter
After creating an object of class MyInt on the stack, which, internally, created an integer on the heap, we can use the dereference operator in the same manner as before to retrieve the value to which the internal raw pointer is pointing. Because the MyInt object en lives on the stack, it is automatically deallocated after each loop cycle - which automatically calls the destructor to release the heap memory. The following console output verifies this:
0/1 = 0
resource 0 deallocated
1/2 = 0.5
resource 1 deallocated
2/3 = 0.666667
resource 2 deallocated
3/4 = 0.75
resource 3 deallocated
4/5 = 0.8
resource 4 deallocated
We have thus successfully used the RAII idiom to create a memory management class that spares us from thinking about calling delete. By creating the MyInt object on the stack, we ensure that the deallocation occurs as soon as the object goes out of scope.
Unique Pointer
A unique pointer is the exclusive owner of the memory resource it represents. There must not be a second unique pointer to the same memory resource, otherwise there will be a compiler error. As soon as the unique pointer goes out of scope, the memory resource is deallocated again. Unique pointers are useful when working with a temporary heap resource that is no longer needed once it goes out of scope.
The following diagram illustrates the basic idea of a unique pointer:
image.pngIn the example, a resource in memory is referenced by a unique pointer instance sourcePtr. Then, the resource is reassigned to another unique pointer instance destPtr using std::move. The resource is now owned by destPtr while sourcePtr can still be used but does not manage a resource anymore.
A unique pointer is constructed using the following syntax:
std::unique_ptr<Type> p(new Type);
#include <memory>
void RawPointer()
{
int *raw = new int; // create a raw pointer on the heap
*raw = 1; // assign a value
delete raw; // delete the resource again
}
void UniquePointer()
{
std::unique_ptr<int> unique(new int); // create a unique pointer on the stack
*unique = 2; // assign a value
// delete is not neccessary
}
Shared Pointer
Just as the unique pointer, a shared pointer owns the resource it points to. The main difference between the two smart pointers is that shared pointers keep a reference counter on how many of them point to the same memory resource. Each time a shared pointer goes out of scope, the counter is decreased. When it reaches zero (i.e. when the last shared pointer to the resource is about to vanish). the memory is properly deallocated. This smart pointer type is useful for cases where you require access to a memory location on the heap in multiple parts of your program and you want to make sure that whoever owns a shared pointer to the memory can rely on the fact that it will be accessible throughout the lifetime of that pointer.
The following diagram illustrates the basic idea of a shared pointer:
image.png#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> shared1(new int);
std::cout << "shared pointer count = " << shared1.use_count() << std::endl;
{
std::shared_ptr<int> shared2 = shared1;
std::cout << "shared pointer count = " << shared1.use_count() << std::endl;
}
std::cout << "shared pointer count = " << shared1.use_count() << std::endl;
return 0;
}
We can see that shared pointers are constructed just as unique pointers are. Also, we can access the internal reference count by using the method use_count(). In the inner block, a second shared pointer shared2 is created and shared1 is assigned to it. In the copy constructor, the internal resource pointer is copied to shared2 and the resource counter is incremented in both shared1 and shared2. Let us take a look at the output of the code:
shared pointer count = 1
shared pointer count = 2
shared pointer count = 1
You may have noticed that the lifetime of shared2 is limited to the scope denoted by the enclosing curly brackets. Thus, once this scope is left and shared2 is destroyed, the reference counter in shared1 is decremented by one - which is reflected in the three console outputs given above.
#include <iostream>
#include <memory>
class MyClass
{
public:
~MyClass() { std::cout << "Destructor of MyClass called" << std::endl; }
};
int main()
{
std::shared_ptr<MyClass> shared(new MyClass);
std::cout << "shared pointer count = " << shared.use_count() << std::endl;
shared.reset(new MyClass);
std::cout << "shared pointer count = " << shared.use_count() << std::endl;
return 0;
}
A shared pointer can also be redirected by using the reset() function. If the resource which a shared pointer manages is no longer needed in the current scope, the pointer can be reset to manage a difference resource as illustrated in the example on the right.
Note that in the example, the destructor of MyClass prints a string to the console when called. The output of the program looks like the following:
shared pointer count = 1
Destructor of MyClass called
shared pointer count = 1
Destructor of MyClass called
After creation, the program prints 1 as the reference count of shared. Then, the reset function is called with a new instance of MyClass as an argument. This causes the destructor of the first MyClass instance to be called, hence the console output. As can be seen, the reference count of the shared pointer is still at 1. Then, at the end of the program, the destructor of the second MyClass object is called once the path of execution leaves the scope of main.
circular reference
#include <iostream>
#include <memory>
class MyClass
{
public:
std::shared_ptr<MyClass> _member;
~MyClass() { std::cout << "Destructor of MyClass called" << std::endl; }
};
int main()
{
std::shared_ptr<MyClass> myClass1(new MyClass);
std::shared_ptr<MyClass> myClass2(new MyClass);
return 0;
}
n main, two shared pointers myClass1 and myClass2 which are managing objects of type MyClass are allocated on the stack. As can be seen from the console output, both smart pointers are automatically deallocated when the scope of main ends:
Destructor of MyClass called
Destructor of MyClass called
When the following two lines are added to main, the result is quite different:
myClass1->_member = myClass2;
myClass2->_member = myClass1;
These two lines produce a circular reference. When myClass1 goes out of scope at the end of main, its destructor can’t clean up memory as there is still a reference count of 1 in the smart pointer, which is caused by the shared pointer _member in myClass2. The same holds true for myClass2, which can not be properly deleted as there is still a shared pointer to it in myClass1. This deadlock situation prevents the destructors from being called and causes a memory leak. When we use Valgrind on this program, we get the following summary:
==20360== LEAK SUMMARY:
==20360== definitely lost: 16 bytes in 1 blocks
==20360== indirectly lost: 80 bytes in 3 blocks
==20360== possibly lost: 72 bytes in 3 blocks
==20360== still reachable: 200 bytes in 6 blocks
==20360== suppressed: 18,985 bytes in 160 blocks
As can be seen, the memory leak is clearly visible with 16 bytes being marked as "definitely lost". To prevent such circular references, there is a third smart pointer, which we will look at in the following.
## The Weak Pointer
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> mySharedPtr(new int);
std::cout << "shared pointer count = " << mySharedPtr.use_count() << std::endl;
std::weak_ptr<int> myWeakPtr1(mySharedPtr);
std::weak_ptr<int> myWeakPtr2(myWeakPtr1);
std::cout << "shared pointer count = " << mySharedPtr.use_count() << std::endl;
// std::weak_ptr<int> myWeakPtr3(new int); // COMPILE ERROR
return 0;
}
Similar to shared pointers, there can be multiple weak pointers to the same resource. The main difference though is that weak pointers do not increase the reference count. Weak pointers hold a non-owning reference to an object that is managed by another shared pointer.
The following rule applies to weak pointers: You can only create weak pointers out of shared pointers or out of another weak pointer. The code on the right shows a few examples of how to use and how not to use weak pointers.
The output looks as follows:
shared pointer count = 1
shared pointer count = 1
First, a shared pointer to an integer is created with a reference count of 1 after creation. Then, two weak pointers to the integer resource are created, the first directly from the shared pointer and the second indirectly from the first weak pointer. As can be seen from the output, neither of both weak pointers increased the reference count. At the end of main, the attempt to directly create a weak pointer to an integer resource would lead to a compile error.
As we have seen with raw pointers, you can never be sure wether the memory resource to which the pointer refers is still valid. With a weak pointer, even though this type does not prevent an object from being deleted, the validity of its resource can be checked. The code on the right illustrates how to use the expired() function to do this.
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> mySharedPtr(new int);
std::weak_ptr<int> myWeakPtr(mySharedPtr);
mySharedPtr.reset(new int);
if (myWeakPtr.expired() == true)
{
std::cout << "Weak pointer expired!" << std::endl;
}
return 0;
}
Thus, with smart pointers, there will always be a managing instance which is responsible for the proper allocation and deallocation of a resource. In some cases it might be necessary to convert from one smart pointer type to another. Let us take a look at the set of possible conversions in the following.
Converting between smart pointers
#include <iostream>
#include <memory>
int main()
{
// construct a unique pointer
std::unique_ptr<int> uniquePtr(new int);
// (1) shared pointer from unique pointer
std::shared_ptr<int> sharedPtr1 = std::move(uniquePtr);
// (2) shared pointer from weak pointer
std::weak_ptr<int> weakPtr(sharedPtr1);
std::shared_ptr<int> sharedPtr2 = weakPtr.lock();
// (3) raw pointer from shared (or unique) pointer
int *rawPtr = sharedPtr2.get();
delete rawPtr;
return 0;
}
The example on the right illustrates how to convert between the different pointer types.
In (1), a conversion from unique pointer to shared pointer is performed. You can see that this can be achieved by using std::move, which calls the move assignment operator on sharedPtr1 and steals the resource from uniquePtr while at the same time invalidating its resource handle on the heap-allocated integer.
In (2), you can see how to convert from weak to shared pointer. Imagine that you have been passed a weak pointer to a memory object which you want to work on. To avoid invalid memory access, you want to make sure that the object will not be deallocated before your work on it has been finished. To do this, you can convert a weak pointer to a shared pointer by calling the lock() function on the weak pointer.
In (3), a raw pointer is extracted from a shared pointer. However, this operation does not decrease the reference count within sharedPtr2. This means that calling delete on rawPtr in the last line before main returns will generate a runtime error as a resource is trying to be deleted which is managed by sharedPtr2 and has already been removed. The output of the program when compiled with g++ thus is: malloc: *** error for object 0x1003001f0: pointer being freed was not allocated.
Note that there are no options for converting away from a shared pointer. Once you have created a shared pointer, you must stick to it (or a copy of it) for the remainder of your program.
When to use raw pointers and smart pointers?
As a general rule of thumb with modern C++, smart pointers should be used often. They will make your code safer as you no longer need to think (much) about the proper allocation and deallocation of memory. As a consequence, there will be much fewer memory leaks caused by dangling pointers or crashes from accessing invalidated memory blocks.
When using raw pointers on the other hand, your code might be susceptible to the following bugs:
- Memory leaks
- Freeing memory that shouldn’t be freed
- Freeing memory incorrectly
- Using memory that has not yet been allocated
- Thinking that memory is still allocated after being freed
With all the advantages of smart pointers in modern C++, one could easily assume that it would be best to completely ban the use of new and delete from your code. However, while this is in many cases possible, it is not always advisable as well. Let us take a look at the C++ core guidelines, which has several rules for explicit memory allocation and deallocation. In the scope of this course, we will briefly discuss three of them:
-
R. 10: Avoid malloc and free While the calls
(MyClass*)malloc( sizeof(MyClass) )
andnew MyClass
both allocate a block of memory on the heap in a perfectly valid manner, onlynew
will also call the constructor of the class andfree
the destructor. To reduce the risk of undefined behavior,malloc
andfree
should thus be avoided. -
R. 11: Avoid calling new and delete explicitly Programmers have to make sure that every call of
new
is paired with the appropriatedelete
at the correct position so that no memory leak or invalid memory access occur. The emphasis here lies in the word "explicitly" as opposed to implicitly, such as with smart pointers or containers in the standard template library. -
R. 12: Immediately give the result of an explicit resource allocation to a manager object It is recommended to make use of manager objects for controlling resources such as files, memory or network connections to mitigate the risk of memory leaks. This is the core idea of smart pointers as discussed at length in this section.
Summarizing, raw pointers created with new
and delete
allow for a high degree of flexibility and control over the managed memory as we have seen in earlier lessons of this course. To mitigate their proneness to errors, the following additional recommendations can be given:
-
A call to
new
should not be located too far away from the correspondingdelete
. It is bad style to stretch younew
/delete
pairs throughout your program with references criss-crossing your entire code. -
Calls to
new
anddelete
should always be hidden from third parties so that they must not concern themselves with managing memory manually (which is similar to R. 12).
In addition to the above recommendations, the C++ core guidelines also contain a total of 13 rules for the recommended use of smart pointers. In the following, we will discuss a selection of these:
-
R. 20 : Use unique_ptr or shared_ptr to represent ownership
-
R. 21 : Prefer unique_ptr over std::shared_ptr unless you need to share ownership
Both pointer types express ownership and responsibilities (R. 20). A unique_ptr
is an exclusive owner of the managed resource; therefore, it cannot be copied, only moved. In contrast, a shared_ptr
shares the managed resource with others. As described above, this mechanism works by incrementing and decrementing a common reference counter. The resulting administration overhead makes shared_ptr
more expensive than unique_ptr
. For this reason unique_ptr
should always be the first choice (R. 21).
-
R. 22 : Use make_shared() to make shared_ptr
-
R. 23 : Use make_unique() to make std::unique_ptr
The increased management overhead compared to raw pointers becomes in particular true if a shared_ptr
is used. Creating a shared_ptr
requires (1) the allocation of the resource using new and (2) the allocation and management of the reference counter. Using the factory function make_shared
is a one-step operation with lower overhead and should thus always be preferred. (R.22). This also holds for unique_ptr
(R.23), although the performance gain in this case is minimal (if existent at all).
But there is an additional reason for using the make_...
factory functions: Creating a smart pointer in a single step removes the risk of a memory leak. Imagine a scenario where an exception happens in the constructor of the resource. In such a case, the object would not be handled properly and its destructor would never be called - even if the managing object goes out of scope. Therefore, make_shared
and make_unique
should always be preferred. Note that make_unique
is only available with compilers that support at least the C++14 standard.
- R. 24 : Use weak_ptr to break cycles of shared_ptr
We have seen that weak pointers provide a way to break a deadlock caused by two owning references which are cyclicly referring to each other. With weak pointers, a resource can be safely deallocated as the reference counter is not increased.
The remaining set of guideline rules referring to smart pointers are mostly concerning the question of how to pass a smart pointer to a function. We will discuss this question in the next concept.
网友评论