Worst problems with new
and delete
:
- Proper pairing of new and delete : Every dynamically allocated object that is created with new must be followed by a manual deallocation at a "proper" place in the program. If the programer forgets to call delete (which can happen very quickly) or if it is done at an "inappropriate" position, memory leaks will occur which might clog up a large portion of memory.
- Correct operator pairing : C++ offers a variety of
new
/delete
operators, especially when dealing with arrays on the heap. A dynamically allocated array initialized withnew[]
may only be deleted with the operatordelete[]
. If the wrong operator is used, program behavior will be undefined - which is to be avoided at all cost in C++. - Memory ownership : If a third-party function returns a pointer to a data structure, the only way of knowing who will be responsible for resource deallocation is by looking into either the code or the documentation. If both are not available (as is often the case), there is no way to infer the ownership from the return type.
To put it briefly: Smart pointers were introduced in C++ to solve the above mentioned problems by providing a degree of automatic memory management: When a smart pointer is no longer needed (which is the case as soon as it goes out of scope), the memory to which it points is automatically deallocated. When contrasted with smart pointers, the conventional pointers we have seen so far are often termed "raw pointers".
In essence, smart pointers are classes that are wrapped around raw pointers. By overloading the ->
and *
operators, smart pointer objects make sure that the memory to which their internal raw pointer refers to is properly deallocated. This makes it possible to use smart pointers with the same syntax as raw pointers. As soon as a smart pointer goes out of scope, its destructor is called and the block of memory to which the internal raw pointer refers is properly deallocated. This technique of wrapping a management class around a resource has been conceived by Bjarne Stroustroup and is called Resource Acquisition Is Initialization (RAII). Before we continue with smart pointers and their usage let us take a close look at this powerful concept.
The RAII is a widespread programming paradigm, that can be used to protect a resource such as a file stream, a network connection or a block of memory which need proper management.
Acquiring and releasing resources
In most programs of reasonable size, there will be many situations where a certain action at some point will necessitate a proper reaction at another point, such as:
- Allocating memory with
new
ormalloc
, which must be matched with a call todelete
orfree
. - Opening a file or network connection, which must be closed again after the content has been read or written.
- Protecting synchronization primitives such as atomic operations, memory barriers, monitors or critical sections, which must be released to allow other threads to obtain them.
The following table gives a brief overview of some resources and their respective allocation and deallocation calls in C++:
A general usage pattern common to the above examples looks like the following:
However, there are several problems with this seemingly simple pattern:
- The program might throw an exception during resource use and thus the point of release might never be reached.
- There might be several points where the resource could potentially be released, making it hard for a programmer to keep track of all eventualities.
- We might simply forget to release the resource again.
The major idea of RAII revolves around object ownership and information hiding: Allocation and deallocation are hidden within the management class, so a programmer using the class does not have to worry about memory management responsibilities. If he has not directly allocated a resource, he will not need to directly deallocate it - whoever owns a resource deals with it. In the case of RAII this is the management class around the protected resource. The overall goal is to have allocation and deallocation (e.g. with new
and delete
) disappear from the surface level of the code you write.
RAII can be used to leverage - among others - the following advantages:
- Use class destructors to perform resource clean-up tasks such as proper memory deallocation when the RAII object gets out of scope
- Manage ownership and lifetime of dynamically allocated objects
- Implement encapsulation and information hiding due to resource acquisition and release being performed within the same object.
In the following, let us look at RAII from the perspective of memory management. There are three major parts to an RAII class:
- A resource is allocated in the constructor of the RAII class
- The resource is deallocated in the destructor
- All instances of the RAII class are allocated on the stack to reliably control the lifetime via the object scope
RAII management class example:
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
};
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;
}
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.
Quiz : What would be the major difference of the following program compared to the last example?
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 heap
MyInt *en = new MyInt(new int(i));
// use the resource
std::cout << **en << "/" << den[i] << " = " << **en / den[i] << std::endl;
return 0;
}
ANSWER: The destructor of MyInt
would never be called, hence causing a memory leak with each loop iteration.
Since C++11, there exists a language feature called smart pointers, which builds on the concept of RAII and - without exaggeration - revolutionizes the way we use resources on the heap. Let’s take a look.
C++11 has introduced three types of smart pointers, which are defined in the header of the standard library:
- The unique pointer
std::unique_ptr
is a smart pointer which exclusively owns a dynamically allocated resource on the heap. There must not be a second unique pointer to the same resource. - The shared pointer
std::shared_ptr
points to a heap resource but does not explicitly own it. There may even be several shared pointers to the same resource, each of which will increase an internal reference count. As soon as this count reaches zero, the resource will automatically be deallocated. - The weak pointer
std::weak_ptr
behaves similar to the shared pointer but does not increase the reference counter.
Prior to C++11, there was a concept called std::auto_ptr
, which tried to realize a similar idea. However, this concept can now be safely considered as deprecated and should not be used anymore.
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:
In 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);
Unique Pointer & Raw Pointer
#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
}
Example of unique pointer:
#include <iostream>
#include <memory>
#include <string>
class MyClass
{
private:
std::string _text;
public:
MyClass() {}
MyClass(std::string text) { _text = text; }
~MyClass() { std::cout << _text << " destroyed" << std::endl; }
void setText(std::string text) { _text = text; }
};
int main()
{
// create unique pointer to proprietary class
std::unique_ptr<MyClass> myClass1(new MyClass());
std::unique_ptr<MyClass> myClass2(new MyClass("String 2"));
// call member function using ->
myClass1->setText("String 1");
// use the dereference operator *
*myClass1 = *myClass2;
// use the .get() function to retrieve a raw pointer to the object
std::cout << "Objects have stack addresses " << myClass1.get() << " and " << myClass2.get() << std::endl;
return 0;
}
Summing up, the unique pointer allows a single owner of the underlying internal raw pointer. Unique pointers should be the default choice unless you know for certain that sharing is required at a later stage. We have already seen how to transfer ownership of a resource using the Rule of Five and move semantics. Internally, the unique pointer uses this very concept along with RAII to encapsulate a resource (the raw pointer) and transfer it between pointer objects when either the move assignment operator or the move constructor are called. Also, a key feature of a unique pointer, which makes it so well-suited as a return type for many functions, is the possibility to convert it to a 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:
Example 1:
#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;
}
Output:
shared pointer count = 1
shared pointer count = 2
shared pointer count = 1
Example 2:
#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;
}
Output:
shared pointer count = 1
Destructor of MyClass called
shared pointer count = 1
Destructor of MyClass called
Problems of shared pointers
When the following two lines are added to main, the result is quite different:
#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);
myClass1->_member = myClass2; // Problems
myClass2->_member = myClass1; // Problems
return 0;
}
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.
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 below shows a few examples of how to use and how not to use weak pointers.
#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;
}
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.
Validity of weak pointer
As we have seen with raw pointers, you can never be sure whether 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;
}
The example below illustrates how to convert between the different pointer types.
#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; // Runtime error
return 0;
}
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.
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.
Let us consider the following recommendation of the C++ guidelines on smart pointers:
R. 30 : Take smart pointers as parameters only to explicitly express lifetime semantics
A function that does not manipulate the lifetime or ownership should use raw pointers or references instead. A function should take smart pointers as parameter only if it examines or manipulates the smart pointer itself.
The following examples are pass-by-value types that lend the ownership of the underlying object:
void f(std::unique_ptr<MyObject> ptr)
void f(std::shared_ptr<MyObject> ptr)
void f(std::weak_ptr<MyObject> ptr)
Passing smart pointers by value means to lend their ownership to a particular function f
. In the above examples 1-3, all pointers are passed by value, i.e. the function f
has a private copy of it which it can (and should) modify.
#include <iostream>
#include <memory>
class MyClass
{
private:
int _member;
public:
MyClass(int val) : _member{val} {}
void printVal() { std::cout << ", managed object " << this << " with val = " << _member << std::endl; }
};
void f(std::unique_ptr<MyClass> ptr)
{
std::cout << "unique_ptr " << &ptr;
ptr->printVal();
}
int main()
{
std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>(23);
std::cout << "unique_ptr " << &uniquePtr;
uniquePtr->printVal();
f(std::move(uniquePtr));
if (uniquePtr)
uniquePtr->printVal();
return 0;
}
Output:
unique_ptr 0x7ffeefbff710, managed object 0x100300060 with val = 23
unique_ptr 0x7ffeefbff6f0, managed object 0x100300060 with val = 23
The output nicely illustrates the copy / move operation. Note that the address of unique_ptr differs between the two calls while the address of the managed object as well as of the value are identical. This is consistent with the inner workings of the move constructor, which we overloaded in a previous section. The copy-by-value behavior of f()
creates a new instance of the unique pointer but then switches the address of the managed MyClass
instance from source to destination. After the move is complete, we can still use the variable uniquePtr
in main but it now is only an empty shell which does not contain an object to manage.
When passing a shared pointer by value, move semantics are not needed. As with unique pointers, there is an underlying rule for transferring the ownership of a shared pointer to a function:
R.34: Take a shared_ptr parameter to express that a function is part owner
Consider the example on the right. The main difference in this example is that the MyClass
instance is managed by a shared pointer. After creation in main()
, the address of the pointer object as well as the current reference count are printed to the console. Then, sharedPtr
is passed to the function f()
by value, i.e. a copy is made. After returning to main, pointer address and reference counter are printed again. Here is what the output of the program looks like:
#include <iostream>
#include <memory>
void f(std::shared_ptr<MyClass> ptr)
{
std::cout << "shared_ptr (ref_cnt= " << ptr.use_count() << ") " << &ptr;
ptr->printVal();
}
int main()
{
std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>(23);
std::cout << "shared_ptr (ref_cnt= " << sharedPtr.use_count() << ") " << &sharedPtr;
sharedPtr->printVal();
f(sharedPtr);
std::cout << "shared_ptr (ref_cnt= " << sharedPtr.use_count() << ") " << &sharedPtr;
sharedPtr->printVal();
return 0;
}
shared_ptr (ref_cnt= 1) 0x7ffeefbff708, managed object 0x100300208 with val = 23
shared_ptr (ref_cnt= 2) 0x7ffeefbff6e0, managed object 0x100300208 with val = 23
shared_ptr (ref_cnt= 1) 0x7ffeefbff708, managed object 0x100300208 with val = 23
Throughout the program, the address of the managed object does not change. When passed to f()
, the reference count changes to 2. After the function returns and the local shared_ptr
is destroyed, the reference count changes back to 1. In summary, move semantics are usually not needed when using shared pointers. Shared pointers can be passed by value safely and the main thing to remember is that with each pass, the internal reference counter is increased while the managed object stays the same.
Without giving an example here, the weak_ptr
can be passed by value as well, just like the shared pointer. The only difference is that the pass does not increase the reference counter.
With the above examples, pass-by-value has been used to lend the ownership of smart pointers. Now let us consider the following additional rules from the C++ guidelines on smart pointers:
R.33: Take a unique_ptr& parameter to express that a function reseats the widget
and
R.35: Take a shared_ptr& parameter to express that a function might reseat the shared pointer
Both rules recommend passing-by-reference, when the function is supposed to modify the ownership of an existing smart pointer and not a copy. We pass a non-const reference to a unique_ptr
to a function if it might modify it in any way, including deletion and reassignment to a different resource.
Passing a unique_ptr
as const
is not useful as the function will not be able to do anything with it: Unique pointers are all about proprietary ownership and as soon as the pointer is passed, the function will assume ownership. But without the right to modify the pointer, the options are very limited.
A shared_ptr
can either be passed as const or non-const reference. The const should be used when you want to express that the function will only read from the pointer or it might create a local copy and share ownership.
Lastly, we will take a look at passing raw pointers and references. The general rule of thumb is that we can use a simple raw pointer (which can be null) or a plain reference (which can not be null), when the function we are passing will only inspect the managed object without modifying the smart pointer. The internal (raw) pointer to the object can be retrieved using the get()
member function. Also, by providing access to the raw pointer, you can use the smart pointer to manage memory in your own code and pass the raw pointer to code that does not support smart pointers.
When using raw pointers retrieved from the get()
function, you should take special care not to delete them or to create new smart pointers from them. If you did so, the ownership rules applying to the resource would be severely violated. When passing a raw pointer to a function or when returning it (see next section), raw pointers should always be considered as owned by the smart pointer from which the raw reference to the resource has been obtained.
With return values, the same logic that we have used for passing smart pointers to functions applies: Return a smart pointer, both unique or shared, if the caller needs to manipulate or access the pointer properties. In case the caller just needs the underlying object, a raw pointer should be returned.
Smart pointers should always be returned by value. This is not only simpler but also has the following advantages:
- The overhead usually associated with return-by-value due to the expensive copying process is significantly mitigated by the built-in move semantics of smart pointers. They only hold a reference to the managed object, which is quickly switched from destination to source during the move process.
- Since C++17, the compiler used Return Value Optimization (RVO) to avoid the copy usually associated with return-by-value. This technique, together with copy-elision, is able to optimize even move semantics and smart pointers (not in call cases though, they are still an essential part of modern C++).
- When returning a shared_ptr by value, the internal reference counter is guaranteed to be properly incremented. This is not the case when returning by pointer or by reference.
The topic of smart pointers is a complex one. In this course, we have covered many basics and some of the more advanced concepts. However, there are many more aspects to consider and features to use when integrating smart pointers into your code. The full set of smart pointer rules in the C++ guidelines is a good start to dig deeper into one of the most powerful features of modern C++.
First RULE of resource management: Must not leak memory, do not litter!