HOW TO USE SMART POINTERS IN C
2021-11-16
In this guide we will see smart pointers in C++ as a replacement
to the old, unsafe, C-like pointers. I will assume that you have some
knowledge of modern C++(i.e., \( \geq \) C++11) and of object oriented programming.
What Is a Smart Pointer?
A smart pointer is an object that simulates a normal pointer while ensuring the program to be free of memory leaks. It achieves that by providing an automatic memory management system that deletes an object if no longer in use. In order to act like a normal pointer, smart pointers need to provide a method for both dereferencing (*
) and indirection(->
).
Smart pointers are defined into the standard library of C++ in the %<memory>% header.
How Does Smart Pointers work?
Smart pointers follow the RAII idiom(Resource Acquisition Is Initialization); that is, ensuring that the object acquisition occurs when the object is being declared. In order to do that, smart pointers take the ownership of any resource allocated on the heap, when the ownership count of an object reach zero(for example, when a pointer reach the end of the scope), the resource is being automatically terminated.Why Should I Use Smart Pointers?
The main reason to use smart pointers is that they prevent some of the most common bugs in both C and C++: memory leaks, dangling pointers and any other memory related problem. If you think that you are not affected by this kind of issues, take a look at this excellent Microsoft article. In particular, consider the following statement:The solutions to this problem are essentially the following:>[…] ~70% of the vulnerabilities Microsoft assigns a CVE each year continue to be memory safety issues[…]-- Microsoft
- Choose garbage collected languages such as Go or Java(not convenient when working on low level applications);
- Stick to unsafe C code and try to avoid this kind of bugs(also not convenient and practically impossible);
- Choose a new way(such as smart pointers)or a new language(such as Rust).
Three Different Smart Pointers
Modern C++ provides three different ways to define a smart pointer:-
std::unique_ptr
: Bind exactly one owner to the pointed resources. This means that declaring another smart pointer to the same pointer will cause a compilation error. The resource pointed by aunique_ptr
is automatically being removed after the pointer goes out of scope; -
std::shared_ptr
: Allows multiple owners to the same object. In this case multiple pointers can point to the same resource, to keep count of how many owners a given resource has,shared_ptr
s keep an internal counter that it is either incremented or decremented each time a new pointer is declared or deleted. The allocated object will not be terminated until all owners have gone out of scope; -
std::weak_ptr
: Another kind of smart pointer to be used along withshared_ptr
s.weak_ptrs
give access to a resource owned by one or moreshared_ptrs
but they do not alter the ownership count. That is, when all theshared_ptrs
are gone out of scope, the resource will be terminated even if there are one or moreweak_ptr
pointed to that location. The newweak_ptr
value will be marked as expired.weak_ptr
can be used to observe an object without interfere with the ownership count.
std::unique_ptr
Without smart pointers, to declare a pointer to an object, we would write:
#include <iostream>
#include <string>
// Compile with: g++ foo.cpp -Wall -Wextra -Werror -std=c++14
class Object {
public:
Object(const unsigned int num, const std::string val) {
this->num = num;
this->val = val;
}
std::string print() { return this->val + ": " + std::to_string(this->num); }
private:
unsigned int num;
std::string val;
};
int main() {
Object *obj = new Object(0, "zero"); // Declare a new object
std::cout << obj->print() << std::endl; // Do something with the object
delete obj; // Manually delete the object
return 0;
}
with smart objects, this becomes
// [...]
int main() {
std::unique_ptr<Object> obj = std::make_unique<Object>(0, "zero"); // Declare a new object
std::cout << obj->print() << std::endl; // Do something with the object
return 0;
} // obj will be terminated here
If we try to declare a new smart pointer that points to the same location, i.e.
// [...]
int main() {
std::unique_ptr<Object> obj = std::make_unique<Object>(0, "zero"); // Declare a new object
std::unique_ptr<Object> obj2 = obj; // New smart pointer
std::cout << obj->print() << std::endl; // Do something with the object
return 0;
} // obj will be terminated here
we will get the same error from the compiler:
sp.cpp:21:29: error: call to implicitly-deleted copy constructor of 'std::unique_ptr<Object>'
std::unique_ptr<Object> obj2 = obj;
Resources initialized with unique_ptr
pointer must have one owner only.
The only thing we can do in these cases is to move the
ownership to another smart pointer. That is:
// [...]
int main() {
std::unique_ptr<Object> obj = std::make_unique<Object>(0, "zero"); // Declare a new object
std::unique_ptr<Object> obj2 = std::move(obj); // Move ownership to this pointer
// obj is no longer available
std::cout << obj2->print() << std::endl; // Do something with the object
return 0;
} // obj2 will be terminated here
Do note however, after moving the ownership to obj2
,
the old
obj is no longer available. In fact,
invoking it will cause a segmentation fault exception by the operating system.
std::shared_ptr
let's now discuss aboutshared_ptrs
. As we described before, this kind
of smart pointer allows multiple referencing to the same resource.
After the number of owners of an object reaches zero, the resource is
automatically terminated. let's see an example:
// [...]
int main() {
std::shared_ptr<Object> obj = std::make_shared<Object>(0, "zero");
// Prints out the number of owners
std::cout << "Current owner(s): " << obj.use_count() << std::endl;
{
std::shared_ptr<Object> obj2 = obj;
std::cout << "Current owner(s): " << obj.use_count() < std::endl;
} // obj2 will be terminated here
std::cout < "Current owner(s): " < obj.use_count() < std::endl;
std::cout << obj->print() << std::endl; // Do something with the object
return 0;
} // obj will be terminated here
Which produces:
$> ./a.out
Current owner(s): 1
Current owner(s): 2
Current owner(s): 1
zero: 0
This is what happens in the previous code:
- We first declare a new shared pointer, the owner's count is equal to one;
- In the inner scope, we declare a new shared pointer to the same object, the owner's count is now equal to two;
-
The inner scope ends, the
owner's count of
obj
decrease to one, so it will not be terminated; -
The outer scope ends, so owner's count of
obj
decrease to zero, the resource will now be terminated.
std::weak_ptr
Finally, let's seeweak_ptrs
. This kind of smart pointer does not
alter the ownership count of an object, instead it will only provide
access to a shared pointer resource. Let's see an example:
// [...]
int main() {
std::weak_ptr<Object> w_ptr; // Declare a new weak_ptr
{
std::shared_ptr<Object> obj = std::make_shared<Object>(0, "zero");
// Prints out owners count
std::cout << "Owner(s) count(before w_ptr): " << obj.use_count() << std::endl;
// Owner's count does not change, even after last assignment
w_ptr = obj;
std::cout << "Owner(s) count(after w_ptr): " << obj.use_count() << std::endl;
} // obj goes out of scope, owner's count is zero, so the resource is terminated
if(w_ptr.expired())
std::cout << "The weak pointer is now invalid" << std::endl;
return 0;
}
which produces
$> ./a.out
Owner(s) count(before w_ptr): 1
Owner(s) count(after w_ptr): 1
The weak pointer is now invalid
The previous code does the following:
-
Create a new empty
weak_ptr
; -
In the inner scope, declare a new
shared_ptr
, the owner's count increase to 1; -
Assign the same resource to
w_ptr
, the owner's count does not change; - The inner scope ends, so the owner's count drops to zero, the resource is being terminated;
-
If we now try to check if
w_ptr
is expired, we get a true result.
Compilation flags
All the previous examples were compiled using the following flags:
-Wall -Wextra -Werror -std=c++14
Some of these features, were not available prior to C++14.