Smart pointer is a wrapper class around a raw pointer providing automatic memory management and few additional functions. Pointers are one of the reason C++ is considered a strong engineering language because they allow close control of system resources, in this case, memory. Pointers allow direct control of memory at runtime, a process technically called dynamic memory allocation.
Raw pointers review
Pointers in general are special types of variables that contains the address of other variables.
Raw pointers provides nothing more than just that.
Example of raw pointer use in C++:
#include <iostream>
using namespace std;
int main() {
int age = 22;
int *pAge = &age;
std::cout<<*pAge<<std::endl;
return 0;
}
Reasons Introduction of Smart Pointers
The freedom given to the engineers on system resources often lead to shooting oneself in the foot. In attempt to reduce the chances, C++ 11 bundled smart pointers in the memory header of the Standard Library to reduce chances of:
1. Dangling pointers
When a pointer exists after its usage is over and the memory it pointed to deleted, it retains the memory address unless explicitly reassigned to nullptr by the programmer. These pointers pointing to non-existent variables indefinitely are called dangling pointers.
2. Multiple deallocations
Occur when you deallocate an already deallocated (and hence dangling) pointer for a second time using either delete or delete[] in C++, is another recipe for disaster.
3. Memory Leaks
Memory leak is a technical term used to describe continuous reduction of memory space due to old structures holding on to memory resources "forever". This is critical issue in older C++ or legacy systems using raw pointers, raw pointers require the user to explicitly manage memory. It's unbelievably easy to forget releasing memory. Consequently, there is progressive reduction in usable space.
4. Allocation/Deallocation mismatch
Both dynamic array and raw pointers are initialized using "new" keyword and after passing them around in the program, there may be a mismatch in what memory is released because of different syntax. Dynamic arrays use delete [] while regular raw pointers uses delete.
Types of Smart Pointers
Smart pointers are implemented as class templates and are in memory header of the Standard Library.
They have ownership semantic dictating who owns an object and when should the ownership end.
Here is an example of how smart pointers are used:
#include <iostream>
#include <memory>
using namespace std;
int main() {
std::unique_ptr<int> age {new int {8}};
cout<<*age;
return 0;
}
There are three types of smart pointers in modern C++. They are mainly different on ownership type
(as suggested in their names) and subsequently how they handle variable lifetime.
- unique_ptr
- shared_ptr
- weak_ptr
1. unique_ptr pointer
This type of smart pointers is used to enable a pointer to exclusively own whatever it points to. It is not possible to have another pointer pointing to the same variable/ have a copy of unique_ptr. Only one unique_ptr can own a resource at a time. Ownership, however, can be transferred using std::move().
#include <iostream>
#include <memory>
int main() {
auto age {std::make_unique<int>(22)};
std::cout<<*age<<std::endl;
return 0;
}
NOTE: Using make_unique is the recommended way of creating smart pointers and enable us to use auto keyword. It's less verbose compared to "new"-based definition and also prevent nuanced cases of memory leaks.
The example above cannot convince anybody on the need for unique_ptrs though, some cases like polymorphic pointers (in dynamic binding ) would but that has classes as prerequisite and therefore out of this scope.
2. shared_ptr pointers
This type of smart pointers are used for shared ownership of whatever is pointed to in the freestore/heap. It succeeds in avoiding deallocation issues by keeping count of references to the variable. The count is then reduced when a shared_ptr is destroyed and finally, memory is released when count is back to zero.
#include <iostream>
#include <memory>
int main() {
auto age {std::make_shared<int>(22)};
std::cout<<*age<<std::endl; //22
auto age2 {std::make_shared<int>()};
age2 = age; //assignment of pointer
std::cout<<*age2<<std::endl; //22
std::shared_ptr<int> age3;
age3 = age2; //assignment of pointer
std::cout<<*age3<<std::endl; //22
std::shared_ptr<int> age4 {age3}; //initialized with another shared_ptr, = auto age4 {age3}
std::cout<<*age4<<std::endl; //22
return 0;
}
The use of auto and std::make_shared<T>() combo is preferred to std::shared_ptr <T> pointer_name {new T {value}} because it makes the time expensive allocation more efficient. To make a shared_ptr share a value, you initialize one with another or assign new with older shared_ptr. The process increases the reference count therefore, both pointers have to be destroyed /reset for the variable memory to be released.
3. weak_ptr pointers
weak_ptr are non-owning pointers that are used as link to to shared_ptr by containing the same address but does not increment the reference count. It therefore cannot stop the object it points to from being destroyed. Dangling pointers again? No. Addresses held by weak pointers cannot be accessed directly but through a shared_ptr. The use case of weak pointers are limited to avoidance of reference cycles suffered by shared_ptr. Reference cycle is a scenario where a shared_ptr in one object points to another object with another shared_ptr also pointing to the first object. It's impossible to destroy either objects then leading to memory leak. Weak pointers can be converted to shared pointers using lock() method of the class.
#include <iostream>
#include <memory>
struct Obj {
std::shared_ptr next;
std::weak_ptr prev; // to breaks circular reference
~Obj() { std::cout << "Obj destroyed"<<std::endl; }
};
int main() {
auto obj1 = std::make_shared<Obj>();
auto obj2 = std::make_shared<Obj>();
obj1->next = obj2;
obj2->prev = obj1;
std::cout << "Objects Linked."<<std::endl;
}
Smart Pointers Methods
Smart pointers are implemented as class templates and therefore can have other members alongside the wrapped raw pointer. There are methods used to manage the underlying pointer via the dot notation. Also, some methods are for general smart pointers while others are conveniently tied to particular types.
General Smart Pointer Methods
get()
The get() method of smart pointers returns the address of the variable pointed to i.e a raw pointer. It is mostly used in interfacing with legacy codes using raw pointers.
#include <iostream>
#include <memory>
int main() {
auto age {std::make_shared<int>(22)};
std::cout<<*age<<std::endl; //22
int *age2 = age.get();
std::cout<<*age2<<std::endl; //22
return 0;
}
NOTE: The indirection operator is overloaded such that the following statements have similar
effect:
*smart_pointer
*(smart_pointer.get())
The first statements shows an indirection operator used on an object and the last one, on a raw pointer.
reset()
reset() is used to change what the smart pointer is pointing to. If no argument is passed, it changes the address it points to nullptr. In this case, the pointer obviously still exist but points to nothing and the initial address is released. The other scenario is is passing a new value. While for unique_ptr, the former address is deleted and it points to a new thing, for smart_ptr, the reference count is decremented but the variable still exists for other shared_ptrs if any.
#include <iostream>
#include <memory>
int main() {
auto age {std::make_shared<int>(22)};
std::cout<<*age<<std::endl; //22
age.reset();
std::cout<<age<<std::endl; //0
age.reset(new int{23});
std::cout<<*age<<std::endl; //23
return 0;
}
Smart Pointers Specific to Type
release()
release() is a unique_ptr method that turns a pointer into a raw pointer. release() alliterates with reset() but closer to get() in terms of use. Both reset() and release() changes the address but in different ways. While reset() only change address to nullptr when no argument is passed or nullptr explicitly passed, release always reset address of the smart pointer to nullptr.
#include <iostream>
#include <iostream>
#include <memory>
int main() {
auto age {std::make_unique<int>(22)};
std::cout<<*age<<std::endl; //22
int *age2 = age.release();
std::cout<<*age2<<std::endl; //22
delete age2;
age2 = nullptr; //prevents dangling pointer
return 0;
}
While reset() can be very useful for legacy support, it is a recipe for chaos. Not only does it introduce mix of raw and smart pointers, it also relinquish the responsibility of deallocating the memory. Again, if you fail to capture the released pointer by a raw pointer, you will never release the memory during the run, memory leak.
use_count()
This is a shared_ptr method used to get the current number of shared pointers pointing to the same address.
#include <iostream>
#include <memory>
int main() {
auto age {std::make_shared<int>(22)};
std::cout<<*age<<std::endl; //22
std::cout<<age.use_count()<<std::endl; //1
return 0;
}
lock()
#include <iostream>
#include <memory>
int main() {
auto age {std::make_shared<int>(22)};
std::cout<<*age<<std::endl; //22
std::weak_ptr age2 = age;
age2 = age;
std::cout<<*age2.lock()<<std::endl; //22
return 0;
}
YOU MIGHT ALSO LIKE
Arrays and Smart Pointers
Arrays in C++ have to be initialized or have constant expressions as their size. That means you either give very large size or you will be in trouble. You can use smart pointers to create arrays then assign values later.
#include <iostream>
#include <memory>
int main() {
auto ages {new int [5]};
for(size_t i {}; i < 5; i++){
ages[i] = i * 2;
}
for(size_t i {}; i < 5; i++){
std::cout<< ages[i] <<std::endl;
}
delete ages; // a hassle we would avoid by using std::make_unique<int []>(5)
ages = nullptr;
return 0;
}
As a rule, any time you use new to allocate memory, ensure it's paired with delete to avoid leaking memory. Notice that we did not use the indirection operator. This is because ages[i] uses implicit indirection because array indexing is defined as *(ages + i). You may have noticed that this is exactly the reason for introduction of std::vector<T> though. We encourage you to use std::vectors when dealing with dynamic arrays.
Conclusion
We have discussed what smart pointers are, their relationship with raw pointers, shortcomings of raw pointers, different types of smart pointers and their use cases. From now henceforth, you're expected to always use smart pointers instead of the error-prone raw ponters.