SmartPointers from C++11 onwards
Amit Nadiger
Polyglot(Rust??, C++ 11,14,17,20, C, Kotlin, Java) Android TV, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Engineering management.
In C++, pointers are a powerful feature that allows you to manipulate memory directly, but they can be difficult to use correctly. Improper use of pointers can lead to a variety of problems, including memory leaks, dangling pointers, and undefined behavior.
Smart pointers were introduced in C++ to help alleviate these problems by providing a higher-level abstraction for memory management. Smart pointers are classes that wrap raw pointers and manage their lifetime automatically, based on a set of rules that ensure safe memory management.
The need for smart pointers arises from the fact that it can be difficult to manually manage the lifetime of objects that are created dynamically with new. For example, if you create an object with new and then forget to delete it, you have a memory leak. On the other hand, if you delete the object twice, you have undefined behavior.
Smart pointers help to solve these problems by taking care of the memory management automatically. For example, a shared_ptr ensures that the memory it points to is deleted when the last shared_ptr pointing to it goes out of scope. Similarly, a unique_ptr ensures that the memory it points to is deleted when the unique_ptr itself goes out of scope , anyways Iet's discuss about it later.
Overall, the use of smart pointers in C++ helps to improve code safety, simplify memory management, and reduce the likelihood of memory-related bug.
To fix this issue, C++11 introduced the concept of smart pointers, which includes:
These new pointers provide more robust memory management while avoiding the pitfalls of the auto pointer and solve the problem of managing memory dynamically. Smart pointers are objects that behave like pointers but with additional features that provide automatic memory management.
Shared Pointers
A shared pointer is a smart pointer that allows multiple pointers to refer to the same dynamically allocated object. The shared pointer keeps track of the number of references to the object and automatically deletes the object when the last shared pointer that references it goes out of scope.
To use a shared pointer, you can declare a shared pointer object and initialize it with a new object, like this:
std::shared_ptr<int> ptr = shared_ptr<int> ptr(new int(101));
//OR
std::shared_ptr<int> ptr = std::make_shared<int>(101) ; // Recommended.
In this example, a new integer object is dynamically allocated with the value 101, and a shared pointer object is created and initialized to point to the new object.
To create a new shared pointer that refers to the same object, you can use the copy constructor or the assignment operator:
//shared_ptr<int> ptr1(new int(101)); // Not recommanded
shared_ptr<int> ptr1 = make_shared<int>(101); // Recommended.
shared_ptr<int> ptr2 = ptr1; // using copy constructor
shared_ptr<int> ptr3;
ptr3 = ptr1; // using assignment operator
In this example, three shared pointers are created, and all of them refer to the same dynamically allocated integer object.
Advantages of Shared Pointers
Shared pointers have several advantages over raw pointers, including:
Disadvantages of Shared Pointers
Shared pointers also have some disadvantages, including:
When to use shared pointers:
APIs provided by shared pointer:
Unique Pointers:
Unique Pointers are introduced in C++11 to replace the auto_ptr. Unique Pointers provide exclusive ownership semantics, which means that only one Unique Pointer can own the dynamically allocated memory at any given time. Unique Pointers provide a mechanism for custom deleters. Unique Pointers cannot be copied but can be moved. Unique Pointers are used when there is a single owner of the dynamically allocated memory.
Advantages of Unique Pointers:
Disadvantages of Unique Pointers:
When to use Unique pointers:
Example:
#include <iostream>
#include <memory>
using namespace std;
int main() {
//unique_ptr<int> uptr(new int(10)); //Not Recommended.
unique_ptr<int> uptr = make_unique<int>(10); // Recommended.
cout << "The value of the pointer is: " << *uptr << endl;
*uptr = 20;
cout << "The new value of the pointer is: " << *uptr <<endl;
return 0;
}
In the above example, we create a Unique Pointer to dynamically allocated memory that stores an integer value of 10. We access the value using the dereference operator (*). Then we change the value of the dynamically allocated memory to 20. When the program exits, the Unique Pointer automatically deallocates the memory.
APIs provided by unique_ptr:
Weak Pointers:
Weak pointers are a type of smart pointer that provides a non-owning, weak reference to an object that is managed by a shared pointer. Weak pointers are used to break circular references between objects managed by shared pointers.
The main advantage of weak pointers is that they allow you to safely reference an object managed by a shared pointer without extending the lifetime of the object. This can be useful when you need to reference an object in a callback or event handler, for example.
Weak pointers are created from shared pointers using the weak_ptr constructor. You can use the lock() method on a weak pointer to obtain a shared pointer to the managed object, but the lock() method will return an empty shared pointer if the object has already been deleted.
Here is an example of using a weak pointer:
#include <iostream>
#include <memory>
udsing namespace std;
class MyClass {
public:
MyClass() { cout << "MyClass constructor" << endl; }
~MyClass() {cout << "MyClass destructor" << endl; }
};
int main() {
shared_ptr<MyClass> sharedPtr = make_shared<MyClass>();
weak_ptr<MyClass> weakPtr = sharedPtr;
if (!weakPtr.expired()) {
shared_ptr<MyClass> sharedPtr2 = weakPtr.lock();
cout << "Managed object still exists" << endl;
} else {
cout << "Managed object has been deleted" << endl;
}
sharedPtr.reset();
if (!weakPtr.expired()) {
shared_ptr<MyClass> sharedPtr2 = weakPtr.lock();
cout << "Managed object still exists" << endl;
} else {
cout << "Managed object has been deleted" << endl;
}
return 0;
}
/*
amit@DESKTOP-9LTOFUP:~/OmPracticeC++$ ./a.out
MyClass constructor
Managed object still exists
MyClass destructor
Managed object has been deleted
*/
In this example, we create a shared pointer to a MyClass object and then create a weak pointer to the same object. We use the expired() method to check if the managed object still exists, and then use the lock() method to obtain a shared pointer to the managed object.
When the shared pointer is reset, the managed object is deleted and the weak pointer's expired() method will return true.
领英推荐
Advantages of Weak Pointers:
Disadvantages of Weak Pointers:
Despite these disadvantages, weak pointers are a useful tool for managing object lifetimes in C++ and can help prevent memory leaks and improve code reliability.
When to use weak pointers:
APIs provided by weak_ptr:
std::weak_ptr is used to avoid circular references between std::shared_ptr objects. It allows a std::shared_ptr to be safely used without preventing the managed object from being deleted when no std::shared_ptr is referencing it anymore.
Few more commonly used functions and operations associated with smart pointers in C++:
Below examples demonstrate above functions:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class MyClass {
public:
MyClass(const int& value, const string& str) : _value(value), _str(str) {
cout << "Constructor called for MyClass(" << value << ", " << str << ")" << endl;
}
void Print() const {
cout << "MyClass object: " << _value << ", " << _str << endl;
}
private:
int _value;
string _str;
};
int main() {
// Shared pointer example
shared_ptr<MyClass> sptr1 = make_shared<MyClass>(10, "Hello");
cout << "Shared pointer count: " << sptr1.use_count() << endl;
{
shared_ptr<MyClass> sptr2 = sptr1;
cout << "Shared pointer count: " << sptr1.use_count() << endl;
}
cout << "Shared pointer count: " << sptr1.use_count() << endl;
// Unique pointer example
unique_ptr<MyClass> uptr1 = make_unique<MyClass>(20, "World");
uptr1->Print();
unique_ptr<MyClass> uptr2 = move(uptr1);
if (uptr1 == nullptr) {
cout << "Unique pointer 1 is null" << endl;
}
if (uptr2 != nullptr) {
uptr2->Print();
}
// Weak pointer example
weak_ptr<MyClass> wptr1 = sptr1;
if (auto sptr3 = wptr1.lock()) {
sptr3->Print();
} else {
cout << "Shared pointer is null" << endl;
}
return 0;
}
/*
O/P =>
Constructor called for MyClass(10, Hello)
Shared pointer count: 1
Shared pointer count: 2
Shared pointer count: 1
Constructor called for MyClass(20, World)
MyClass object: 20, World
Unique pointer 1 is null
MyClass object: 20, World
MyClass object: 10, Hello
*/
The above function is self-explanatory.
Example 2:
In Below example tried to cover all the scenario of all 3 smart pointers.
#include <iostream>
#include <memory>
using namespace std;
class MyClass {
? ? public:
? ? ? ? int data;
? ? ? ? MyClass() { cout << "MyClass Constructor" << endl; }
? ? ? ? ~MyClass() { cout << "MyClass Destructor" << endl; }
};
int main() {
? ? // create a shared pointer using make_shared
? ? shared_ptr<MyClass> my_shared_ptr = make_shared<MyClass>();
? ? cout << "my_shared_ptr use count before weak ptr assignment : " << my_shared_ptr.use_count() << endl;
? ? // create a weak pointer from the shared pointer
? ? weak_ptr<MyClass> my_weak_ptr = my_shared_ptr;
? ? cout << "my_shared_ptr use count after weak ptr assignment: " << my_shared_ptr.use_count() << endl;
? ? cout << "my_weak_ptr use count: " << my_weak_ptr.use_count() << endl;
? ? // use lock to get a shared pointer from the weak pointer
? ? shared_ptr<MyClass> my_shared_ptr_2 = my_weak_ptr.lock();
? ? if (my_shared_ptr_2 != nullptr) {
? ? ? ? cout << "my_shared_ptr_2 use count: " << my_shared_ptr_2.use_count() << endl;
? ? } else {
? ? ? ? cout << "my_weak_ptr is expired" << endl;
? ? }
? ??
? ? // Empty unique_ptr object
? ? unique_ptr<int> ptr;
?
? ? // Check if unique pointer object is empty
? ? if(!ptr)
? ? ? ? cout<<"ptr is empty"<<endl;
?
// Check if unique pointer object is empty
? ? if(ptr == nullptr)
? ? ? ? cout<<"ptr is empty"<<endl;
?
? ? // can not create unique_ptr object by initializing through assignment
? ? // unique_ptr<MyClass> taskPtr2 = new MyClass(); // Compile Error
? ? // create a unique pointer using make_unique
? ? unique_ptr<MyClass> my_unique_ptr = make_unique<MyClass>();
? ? cout << "my_unique_ptr data: " << my_unique_ptr->data << endl;
?
? ? // Create a unique_ptr object through raw pointer
? ? unique_ptr<MyClass> my_unique_raw_ptr(new MyClass());
? ? // Check if taskPtr is empty or it has an associated raw pointer
? ? if(my_unique_raw_ptr!= nullptr)
? ? ? ? cout<<"my_unique_raw_ptr is? not empty"<<endl;
? ? cout<<"Reset the my_unique_raw_ptr"<<endl;
? ? // Reseting the unique_ptr will delete the associated
? ? // raw pointer and make unique_ptr object empty
my_unique_raw_ptr.reset();?
?
// reseting is only possible when used with raw pointer i.e new operator
// my_unique_ptr.reset();? // Runtime error , Segmentation fault (core dumped)
?
? ? // Check if my_unique_raw_ptr is empty or it has an associated raw pointer
if(my_unique_raw_ptr == nullptr)
? ? cout<<"my_unique_raw_ptr is? empty"<<endl;
? ? // unique_ptr object is Not copyable
// unique_ptr<MyClass> my_unique_ptr1 = my_unique_ptr; //compile error
? ? //my_unique_raw_ptr =? my_unique_ptr; //compile error
? ? // unique_ptr object is Not copyable
//unique_ptr<MyClass> my_unique_ptr_2 =? my_unique_ptr;
? ? // use move to transfer ownership of the unique pointer
? ? {
? ? ? ? unique_ptr<MyClass> my_unique_ptr_2 = move(my_unique_ptr);
? ? ? ? cout << "my_unique_ptr_2 data: " << my_unique_ptr_2->data << endl;
? ? ? ? //my_unique_ptr_2 goes out of scope and calls the destructor after this block
// if raw pointer is used it deletes the assocaited raw pointer after this block
? ? }
? ? // Create a unique_ptr object through raw pointer
? ? unique_ptr<MyClass>? my_unique_ptr_3(new MyClass);
?
if(my_unique_ptr_3 != nullptr)
cout<<"my_unique_ptr_3 is not empty"<<endl;
?
// Release the ownership of object from raw pointer
MyClass* ptr1 = my_unique_ptr_3.release();
? ? if(my_unique_ptr_3== nullptr)
? ? ? ? cout<<"my_unique_ptr_3 is empty after my_unique_ptr_3.release() i.e releasing ownership "<<endl;
? ? delete ptr1;
? ?
? ? // use reset to release ownership of the shared pointer
? ? my_shared_ptr.reset();
? ? cout << "my_shared_ptr is null after reset to release ownership : " << (my_shared_ptr == nullptr) << endl;
? ? // use reset to release ownership of the unique pointer
? ? my_unique_ptr.reset();
? ? cout << "my_unique_ptr is null after reset to release ownership: " << (my_unique_ptr == nullptr) << endl;
? ? return 0;
}
/*
MyClass Constructor
my_shared_ptr use count before weak ptr assignment : 1
my_shared_ptr use count after weak ptr assignment: 1
my_weak_ptr use count: 1
my_shared_ptr_2 use count: 2
ptr is empty
ptr is empty
MyClass Constructor
my_unique_ptr data: 0
MyClass Constructor
my_unique_raw_ptr is? not empty
Reset the my_unique_raw_ptr
MyClass Destructor
my_unique_raw_ptr is? empty
my_unique_ptr_2 data: 0
MyClass Destructor
MyClass Constructor
my_unique_ptr_3 is not empty
my_unique_ptr_3 is empty after my_unique_ptr_3.release() i.e releasing ownership
MyClass Destructor
my_shared_ptr is null after reset to release ownership : 1
my_unique_ptr is null after reset to release ownership: 1
MyClass Destructor
*/
Please read the above program carefully, then you will understand yourself.
What is the problem when a container is used to store objects that are managed by raw pointers or auto pointers, it can lead to issues such as double deletion or dangling pointers.
When auto_ptr was used with containers like vector, it caused issues due to its unique ownership semantics. The main problem was that auto_ptr does not support proper copy semantics. When an auto_ptr was copied, ownership of the managed resource was transferred, leaving the source auto_ptr in a null state. This meant that adding an auto_ptr to a container like vector would result in transferring ownership of the resource and leaving the original auto_ptr in an invalid state.
Consider the following example:
std::vector<std::auto_ptr<int>> vec;
std::auto_ptr<int> ptr1(new int(42));
vec.push_back(ptr1);
In this case, after pushing ptr1 into the vector, ptr1 becomes null. If you try to access ptr1 later, it would result in undefined behavior.
Also, if the vector is resized or elements are removed, the auto_ptr objects can become invalid. For example, if an element is removed from the middle of the vector, the auto_ptr objects in the vector after the removed element will be shifted to fill the gap. This shift can cause the auto_ptr objects to point to the wrong elements or even to be invalidated.
To solve these issues, modern C++ provides smart pointers such as unique_ptr and shared_ptr that can be used instead of raw pointers or auto_ptr objects. These smart pointers manage the memory of the pointed-to object and ensure that it is properly deallocated when the pointer goes out of scope or is no longer needed.
For example, consider the following code that uses unique_ptr and shared_ptr:
vector<unique_ptr<MyClass>> vec1;
vec1.push_back(make_unique<MyClass>());
vector<shared_ptr<MyClass>> vec2;
vec2.push_back(make_shared<MyClass>());
In this code, the vector objects are used to store unique_ptr and shared_ptr objects. These smart pointers ensure that the pointed-to objects are properly deallocated when the pointers go out of scope or are removed from the vector. Moreover, since unique_ptr objects cannot be copied, the issue of reference counting is avoided.
Which method to use for creating the unique pointer:
There are 2 ways to make the unique pointer as below:
1.Create a unique_ptr using make_unique
unique_ptr<MyClass> my_unique_ptr = make_unique<MyClass>();
2. Create a unique_ptr object through raw pointer
unique_ptr<MyClass> my_unique_raw_ptr(new MyClass());
Both allocate memory on the heap. However, the new operator is not used when calling make_unique<MyClass>(). Instead, make_unique uses new internally to allocate the memory and construct the object, and returns a unique_ptr that owns the memory.
So, the main difference between the two methods is that make_unique is preferred because it's safer and less error-prone. It's a single-step operation that both allocates the memory and constructs the object, and returns a unique_ptr that owns the memory.
On the other hand, using new to create a unique_ptr i.e using the raw pointer requires two separate steps: allocating the memory with new, and then passing the raw pointer to the unique_ptr constructor. This increases the risk of errors, such as forgetting to delete the object, or accidentally leaking memory if an exception is thrown before the unique_ptr is created.
Therefore, it's generally recommended to use make_unique over manually calling new when creating unique_ptrs.
Application of smart pointers in Embedded systems:
Smart pointers can be used in embedded systems just like any other C++ feature, with some considerations and adaptations to the specific constraints of embedded systems. Here are a few ways smart pointers can be used in embedded systems:
It's important to note that the usage of smart pointers in embedded systems should consider the specific constraints of the system, such as limited memory, real-time requirements, and hardware limitations. Careful consideration should be given to the use of dynamic memory allocation, as it may not be suitable for all scenarios in embedded systems.
Additionally, some embedded systems may have their own resource management mechanisms or specific guidelines that govern memory management. In such cases, the usage of smart pointers may need to align with the system's requirements and constraints.
Summary:
Smart pointers are a type of pointer used in C++ that automatically manage the lifetime of dynamically allocated objects. Here are some common scenarios where different types of smart pointers can be useful:
Thanks for reading, please comment if you have any.