C++ - Smart Pointers
C++ smart pointers let us get rid of the explicit deletion of dynamic memory.
In this post, we want to see what are smart pointers and how we can properly
use them.
The new C++ library provides three types of smart pointers defined in
memory header file:
- Shared pointer
- Unique pointer
- Weak pointer. We don't cover weak pointers in this post.
Shared Pointer
C++ provides a class template called shared_ptr. We specify the type of object
that our pointer refers to as a template parameter.
std::shared_ptr<MyClass> myPointer;
The shared_ptr class overrides * and -> operators to make a shared pointer
object looks like a normal (raw) pointer. Thus, using * we can deference the
pointer, and using -> we can access members of the underlying object.
->mem() function is the same as *.
When we define a shared pointer like above, the shared pointer holds a null
pointer. The safer way is to use the make_shared template function.
This function constructs an actual object and returns a shared pointer to it.
Thus, we are sure that our smart pointer is referring to an actual object
instead of a null pointer.
std::shared_ptr<MyClass> myPointer = std::make_shared<MyClass>(/* constructor parameters */);
or we can take advantage of auto:
auto myPointer = std::make_shared<MyClass>(/* constructor parameters */);
We can also initialize a shared pointer with new:
std::shared_ptr<MyClass> myPointer (new MyClass(/* constructor parameters */));
Reference Counting
Each shared pointer keeps track of the number of other shared pointers
referring to the same object that it is referring to. You can think of it as
ALL shared pointers referring to the same object share this number called the
reference count. Note that the reference count is assigned to the
underlying object. When a new pointer joins the group of shared pointer
referring to this object, the reference counter increases, and when one
pointer leaves the group this number decreases. When the last shared pointer
leaves the group, i.e., there is no shared pointer referring to the underlying
object anymore, the object is destroyed.
When does the reference count increase?
- When a shared pointer is used to initialize another shared pointer.
- When a shared pointer is used on the right-hand-side of an assignment.
- When a shared pointer is passed by value to a function.
- When a shared pointer is returned by value from a function.
When does the reference count decrease?
- When a shared pointer is used on the left-hand-side of an assignment.
- When a shared pointer goes out of its scope.
Is shared_ptr the same as Java objects?
Although thinking of shared_ptr as Java objects might be useful to understand
and work with it, C++ shared pointers and Java objects are different. Java is
a managed language. The out-of-scope objects are deleted by the garbage
collector periodically. Thus, Java classes cannot have destructors, and
there is no way for the program to determine when its object must be
destroyed. On the other hand, when using shared pointers in C++, we know for
sure, that the destructor of the underlying class is called
immediately when the last shared_ptr is destroyed.
No mixed access!
Although we can initialize a shared_pointer from a raw pointer, it is very
important to avoid using the raw pointer after using it to initialize a
shared pointer, because as soon as we create a shared pointer out of a raw
pointer, we have given up our control over the lifetime of the object, so
accessing the raw pointer is dangerous.
For example, if I have the following code:
MyClass* myRawPointer = new MyClass();
std::shared_ptr<MyClass> myPointer (myRawPointer);
myPointer = std::make_shared<MyClass>();
The object is deleted at the third line and myRawPointer is undefined
now.
get() Function
The smart pointers have a get function that returns a raw pointer to the
underlying object. the get function must be only used to give access to the
raw pointer to a code that only works with the raw pointers (e.g. a function
that receives a raw pointer as argument). We may face this situation while
working with a legacy code that does not use smart pointers.
There are 2 important things we have to check when we use the get()
function:
- No deletion: We must not delete the pointer that get() function returns. Thus, if we give the raw pointer to a function, we must make sure that the function does not delete this raw pointer.
- No initialization: We must use the raw pointer return by the get() function to initialize another smart pointer, as this will mess up the reference counting for the underlying object and may cause deleting it at the wrong time.
Exceptions
One of the reasons that we don't use exceptions in C++ as much as we use in
Java is the memory leak due to exceptions. If we create a dynamic object using
a new operator, we know that we have to delete it. However, if we leave
the scope of the object due to an exception, we may not delete it and there
might be no way to delete it at all. Using smart pointers we can use
exceptions safely because smart pointers manage their memory correctly even
when we leave the scope prematurely due to an exception.
Deleter
By default, a shared pointer assumes the object that it is referring to a
created dynamically--using the new operator. Thus, it executes the
delete operator on the underlying raw pointer when it is the last
shared pointer for that object and it is being destroyed. Now suppose we
create a shared pointer out of a statically allocated object. For example:
MyClass obj;
std::shared_ptr<MyClass> myPointer (&obj);
Here, the shared pointer cannot call delete on the underlying pointer, as the
delete can only be called for dynamically allocated objects created by the new
operator.
Why would I even need to create a shared_ptr out of a statically allocated
object?
Sometimes, we have classes that do not have proper destructors or require the
user to explicitly free resources. We want to make sure we call a routine when
the object is destroyed. For that, we can use shared pointers with a
deleter.
First, we define a deleter. The deleter must receive a pointer to the
underlying object:
void myClassDeleter(MyClass* obj) {
//free resources here
}
Then, we register the deleter at the initialization of the shared
pointer.
{
MyClass obj;
std::shared_ptr<MyClass> myPointer (&obj, myClassDeleter);
//use obj here
}
An Example: A vector of shared pointers
#include <iostream>
#include<memory>
#include <vector>
class MyClass {
private:
int id;
public:
MyClass (int id_) : id(id_) {}
~MyClass () {
std::cout << "Destructor for " << id << std::endl;
}
int getId () {return id;}
};
int main (int argc, char** argv) {
std::vector<std::shared_ptr<MyClass>> vec;
//Different ways to push this vector.
vec.push_back(std::shared_ptr<MyClass>(new MyClass(1)));
vec.push_back(std::make_shared<MyClass>(2));
vec.emplace_back(new MyClass(3));
auto second = vec[1]; //reference count for 2 will become 2
vec.clear(); //reference count for the 1 and 3 become 0
//so they will be deleted here.
std::cout<<"Right before exit" << std::endl;
return 0;
}
The output will be:
Destructor for 1
Destructor for 3
Right before exit
Destructor for 2
Other Operations
Swapping underlying pointers:
swap(sharedPtr1, sharedPtr2)
smartPtr1.swap(sharedPtr2)
Note: You CANNOT swap a shared pointer with a unique
pointer.
Return the number of the shared pointer pointing to the same object as
sharedPtr:
sharedPtr.use_count()
Return true if sharedPtr is the only shared pointer pointing to the object:
sharedPtr.unqiue()
Reset the shared pointer to null or a different raw pointer:
sharedPtr.reset() //sets sharedPtr to null
sharedPtr.reset(rawPtr)
sharedPtr.reset(rawPtr, deleterFunction)
In summary, shared pointers are easy to understand and use. Just
remember:
- Don't use raw and shared pointers to the same object at the same time. Once you created a shared pointer out of a raw pointer, don't access the raw pointer anymore.
- Be careful when using get() function; don't delete the returned raw pointer and don't use it to initialize another shared pointer.
Unique Pointer
A unique pointer, as the name suggests, is unique, i.e., unlike shared
pointers, we cannot have two unique pointers referring to the same underlying
object. Thus, unique pointers don't have copy constructors or the
assignment operator.
We can define and initialize a unique pointer in two ways:
-
std::unique_ptr<MyClass> uniquePtr1;
-
std::unique_ptr<MyClass> uniquePtr2 (new MyClass(1));
We don't have assignment operator for unique pointers. Then, how can we
assign any object to the first form of definition?
Using reset, std:move, or swap functions (as we will see below) we can assign
an object to the already defined unique pointer.
release() Function
An important function of the unique pointer is the release function. As
the name suggests, the release function releases the ownership of the
underlying object owned by the unique pointer and sets the unique pointer to
nullptr. The release function returns a raw pointer to the underlying
object.
Important: It is very important to capture the raw pointer returned by
the release function. Otherwise, we will have a memory leak, as we won't have
any access to the underlying object, so we won't have any way to delete
it.
Usually, we use the release function to take ownership of a unique pointer and
assign it to another smart pointer using the constructor or the reset()
function. Alternatively, we can store it in a raw pointer, but in that case,
we are responsible for deleting it once our program is done using the
object.
std::unique_ptr<MyClass> uniquePtr1 (new MyClass(1));
std::unique_ptr<MyClass> uniquePtr2 (uniquePtr1.release()); //ownership is transfered to uniquePtr2
std::unique_ptr<MyClass> uniquePtr3;
uniquePtr3.reset(uniquePtr2.release()); //ownership is transfered to uniquePtr3
Other than release and reset, we can transfer the ownership with swap() and std::move() as well:
std::unique_ptr<MyClass> uniquePtr4 = std::move(uniquePtr3); //ownership is transfered to unqiuePtr4
std::unique_ptr<MyClass> uniquePtr5;
swap(uniquePtr4, uniquePtr5); //ownership is transfered to unqiuePtr5
uniquePtr5.swap(uniquePtr4); //ownership is transfered to unqiuePtr4 again.
Note: The std::move() function sets the original pointer to null.
Passing a unique_ptr to a Function
Passing shared pointers is straightforward; when we pass by value, we simply
copy the shared pointer. So the reference pointer increases by one, and then
when the function returns the reference count decreases by one. For unique
pointers, however, we don't have a copy constructor. Thus, we cannot pass a
unique pointer by value the way we can pass shared pointers. We have two
options to pass a unique pointer to a function:
- Pass by reference.
- Move the pointer using std::move() function.
void printId_value (std::unique_ptr<MyClass> ptr) {
std::cout << ptr->getId() << std::endl;
}
void printId_reference (std::unique_ptr<MyClass>& ptr) {
std::cout << ptr->getId() << std::endl;
}
int main (int argc, char** argv) { std::unique_ptr<MyClass> uniquePtr (new MyClass(1));
printId_reference(uniquePtr);
printId_value(std::move(uniquePtr));
if (!uniquePtr)
std::cout << "uniquePtr is null" << std::endl;
return 0;
}
Output:
1
1
Destructor for 1
uniquePtr is null
Returning a unique_ptr from a Function
Suppose we have a function that returns a unique pointer. We can define and initialize a unique pointer in its body and return it. Simple.
std::unique_ptr<MyClass> giveMeAUniquePtr (int id) {
std::unique_ptr<MyClass> p (new MyClass(1));
return p;
}
int main (int argc, char** argv) {
std::cout << giveMeAUniquePtr(1)->getId() << std::endl;
return 0;
}
Output:
1 Destructor for 1
But don't we call copy constructor on the return?! We said no copy
constructor for unique pointers.
This is the only exception that we can assume a unique pointer has a copy
constructor. The compiler is smart enough to know that pointer is about to be
destroyed. Thus, it actually moves the object here.
Deleter for Unique Pointers
We saw above how using a deleter we can override the default behavior of a
shared pointer to delete its object. We can do the same for a unique pointer
with a little difference; we have to specify the type of the deleter function
as a template parameter for our unique pointer.
void myClassDeleter(MyClass* obj) {
auto id = obj->getId();
delete obj;
std::cout<< "Object with id=" << id << " deleted by my custom deleter." << std::endl;
}
int main (int argc, char** argv) {
std::unique_ptr<MyClass, decltype(myClassDeleter)*> uniquePtr (new MyClass(1), myClassDeleter);
return 0;
}
Output:
Destructor for 1 Object with id=1 deleted by my custom deleter.
What is the benefit of specifying the type of the deleter as a template
parameter?
This way, the compiler knows the type of the deleter at the compile time. It is more efficient than finding the type at run-time
and making an indirect call. On the other hand, with a shared pointer, we can change the deleter at the run-tie. Thus, unique pointer is more efficient,
while shared pointer is more flexible regarding deleters.
In summary, use a unique pointer when you want to make sure there is
only one pointer referring to an object.
- You can pass a unique pointer either by reference or std::move() to a function.
- You can simply return a unique pointer from a function as if it has a copy constructor.
- If you want a custom deleter, you have to specify its type as a template parameter.
Comments
Post a Comment