C++

C++ : Smart Pointers

Smart Pointers.

  • A smart pointer is a wrapper class that manages a raw ( normal ) pointer. These were introduced in C++11.
    • A variety of smart pointers ( Unique pointer, Shared pointer, Weak pointer etc..) abstract a raw pointer while providing some useful functionalities.
  • Smart pointers are better alternatives to raw pointers as they address the problems that arise with raw pointers like non-deletion of dynamically allocated objects that cause memory leaks.
    • To avoid memory leaks a raw pointer has to be explicitly destroyed whereas with a smart pointer the memory leaks are automatically taken care of when the smart pointer object goes out of scope.
  • Smart pointers are defined in the <memory> header file of the std namespace.

Example : Smart pointer ( unique pointer ) vis-à-vis a raw pointer.

  • Smart pointer is a class template that is declared on the stack, and initialized using a raw pointer that points to an object allocated on the heap.
  • Once initialized, the smart pointer owns the raw pointer and is responsible for deleting the memory that the raw pointer allocated.
  • The destructor of the smart pointer contains the call to delete, and because the smart pointer is declared on the stack, when it goes out of scope, its destructor is invoked.


#include<iostream>
#include<memory>
#include<string>

using namespace std;

class AudioBook {
    
    private:
    string title;
    string author;
    public:
    AudioBook() {}
    AudioBook (string arg_title, string arg_author): title(arg_title), author(arg_author)
    {}
    inline void PlayBook() {
       cout << "Playing : " << title << endl;
    }   
};

void With_UniquePointer(void) {

    unique_ptr<AudioBook> uptr_a(new AudioBook("Lord Of The Rings", "J. R. R. Tolkien")); 
    uptr_a->PlayBook();

    // Note : make_unique function efficiently creates and returns a unique pointer and is recommended over
    // directly calling unique_ptr constructor.
    auto uptr_b = make_unique<AudioBook>("Game Of Thrones", "George RR Martin"); 
    uptr_b->PlayBook();
}

void With_RawPointer(void) {
    AudioBook * rawptr = new AudioBook("The Hitchhiker's Guide to the Galaxy", "Douglas Adams"); 
    rawptr->PlayBook();

    // Make sure that raw pointer (rawptr) is deleted, else it would cause memory leak.
    delete rawptr;
}

int main() {
    With_UniquePointer();
    With_RawPointer(); 
    return 0;
}

Output

Playing : Lord Of The Rings
Playing : Game Of Thrones
Playing : The Hitchhiker's Guide to the Galaxy

Unique pointer ( std :: unique_ptr ) VS Shared pointer ( std :: shared_ptr )


Unique pointer ( std :: unique_ptr ) Shared pointer ( std :: shared_ptr )
- Unique pointer ( std :: unique_ptr ) does not share the ownership of the underlying raw pointer.

- Unique pointer cannot be copied into some other unique pointer, also it cannot be passed by value to a function. A unique_pointer can only be moved.

- make_unique function creates and returns a unique_pointer to an object constructed using the provided arguments.
- make_unique provides exception safety and is preferred over calling unique_ptr constructors.

- Object pointed by unique pointer is destroyed and its memory deallocated when either of the following happens:
1. The unique_ptr object is destroyed.
2. The unique_ptr object is assigned another pointer via operator= or reset().Unique_Ptr









- Shared pointer ( std :: shared_ptr ) shares the ownership of the underlying raw pointer while keeping a count of the references to the dynamically allocated object.

- Shared pointer after initialization can be copied into another shared_pointer, and can be passed by value in function arguments.

- make_shared function creates and returns a shared_pointer to an object constructed using the provided arguments.
- make_shared provides exception safety and is preferred over calling shared_ptr constructors.

- Object pointed by shared pointer is destroyed and its memory deallocated when either of the following happens:
1. The last shared_ptr managing the object is destroyed.
2. The last shared_ptr managing the object is assigned another pointer via operator= or reset().Shared_Ptr

Shared pointer ( std :: shared_ptr ) can give rise to circular references, causing a memory leak.

Consider the below scenario where an object A ( Google stock object ) has a reference to object B ( Tesla stock object) and vice-versa.

#include <iostream>
#include <memory>
 
class Stock
{
    private:
    std :: string stock_name;
    std :: shared_ptr<Stock> purchase;
    
    public:
    Stock (const std::string& name) : stock_name (name) { 
        std :: cout << "Stock object [" << stock_name << "] created" << std :: endl;
    }   

    ~Stock () {
        std :: cout << "Stock object [" << stock_name << "] destroyed" << std :: endl;
    }   
    
    // The Transact function purchases stock_b with stock_a and vice-versa thereby creating a circular references.
    static void Transact (std :: shared_ptr<Stock>& stock_a, std :: shared_ptr<Stock>& stock_b) {
   
        if (stock_a && stock_b) {
            stock_a->purchase = stock_b;
            stock_b->purchase = stock_a;
            std :: cout << "Transaction 1" << std :: endl;
            std :: cout << "Stock [" << stock_a->stock_name << "] purchased with stock [" << stock_b->stock_name << "]\n";
            std :: cout << "Transaction 2" << std :: endl;
            std :: cout << "Stock [" << stock_b->stock_name << "] purchased with stock [" << stock_a->stock_name << "]\n";
        }
    }   
};
 
int main() {

   auto tesla { std :: make_shared<Stock>("Tesla") }; 
   auto google { std :: make_shared<Stock>("Google") }; 

   // Purchase Tesla stock with Google and vice-versa
   Stock :: Transact (tesla, google);
   return 0;
}

Note : The destructor of the Stock class doesn’t get called even though the shared pointers ( tesla & google ) go out of scope and cause a memory leak.
We notice
2 shared pointers pointing to Tesla object. One being std :: shared_ptr ( tesla ) and the other is Google’s ( purchase )
2 shared pointers pointing to Google object. One being std :: shared_ptr ( google ) and the other is Tesla’s ( purchase )

At the end of the main function, before std :: shared_ptr ( tesla ) goes out of scope it checks if there are any references to Tesla’s object.
Since’s Google’s std :: shared_ptr ( purchase ) still holds a reference on Tesla’s object, tesla’s destructor doesn’t get called hence Tesla’s object remains in memory. The same thing happens to std :: shared_ptr ( google ). google’s destructor doesn’t get called because Tesla’s std :: shared_ptr ( purchase ) has a reference on Google’s object.

Output

Stock object [Tesla] created
Stock object [Google] created
Transaction 1
Stock [Tesla] purchased with stock [Google]
Transaction 2
Stock [Google] purchased with stock [Tesla]

The memory leak caused by a std :: shared_ptr in circular references can be fixed using a weak pointer ( std :: weak_ptr )
Note : std :: weak_ptr does not fix the circular references problem. A design change would be needed to address the circular reference problem (which is beyond the scope of this article).

Weak pointer ( std :: weak_ptr )
- Weak pointer ( std :: weak_ptr ) holds a weak ( non - owning ) reference to the dynamically allocated object that is managed by a shared pointer ( std :: shared_ptr ). i.e The weak pointer acts as an observer and can access the object owned by a std :: shared_ptr; but does not own the object.

- Weak pointer ( std :: weak_ptr ) cannot be used directly as it does not have -> operator. It has to be converted into a shared pointer ( std :: shared_ptr ) to access the dynamically allocated object.

Addressing memory leak
When the std :: shared_ptr goes out of scope, it only considers the references held on the dynamically created object that it points to by other std :: shared_ptr and not by std :: weak_ptr. So if ( purchase ) is an std :: weak_ptr, the reference that it holds on the Stock object is not considered when the std :: shared_ptr goes out of scope.

Thus in the above example,

  • When std :: shared_ptr ( tesla ) and std :: shared_ptr ( google ) go out of scope, their corresponding destructors get called and the Stock object gets deallocated.


#include<iostream>
#include<memory>
#include<string>

class AudioBook {

    private:

    std::string title;
    std::string author;

    public:

    AudioBook() {}

    AudioBook (std::string arg_title, std::string arg_author): title(arg_title), author(arg_author) { 
        std :: cout << "Parameterized constructor for AudioBook got called." << std :: endl;
    }   

    inline void PlayBook() {
        std::cout << "Playing : " << title << std::endl;
    }   

    ~AudioBook() {
        std :: cout << "Destructor for AudioBook got called." << std :: endl;
    }   
};

int main() {
 
    auto uptr_a = std::make_unique<AudioBook>("Lord Of The Rings", "J. R. R. Tolkien"); 
    uptr_a->PlayBook();
 
    auto uptr_b = std::move(uptr_a);
    uptr_b->PlayBook();
 
    uptr_b.reset(); // Memory freed as nobody owns it now.
    return 0;
}

Output

Parameterized constructor for AudioBook got called.
Playing : Lord Of The Rings
Playing : Lord Of The Rings
Destructor for AudioBook got called.
#include<iostream>
#include<memory>
#include<string>

class AudioBook {

    private:

    std :: string title;
    std :: string author;

    public:
    AudioBook() {}
    AudioBook (std :: string arg_title, std :: string arg_author) : title(arg_title), author(arg_author) {
        std :: cout << "Parameterized constructor for AudioBook got called" << std :: endl;
    }
    inline void PlayBook() {
       std::cout << "Playing : " << title << std :: endl;
    }
    ~AudioBook() {
        std :: cout << "Destructor for AudioBook got called" << std :: endl;
    }
};

int main() {

    auto shared_ptr_a = std :: make_shared<AudioBook>("Sherlock Holmes", "Arthur Conan Doyle");
    shared_ptr_a->PlayBook();

    {
        // Initialization via assignment operator increments the reference count.
        auto shared_ptr_b = shared_ptr_a;
        shared_ptr_b->PlayBook();

        // Initialization via copy constructor increments the reference count.
        auto shared_ptr_c(shared_ptr_b);
        shared_ptr_c->PlayBook();

        // When the below shared pointer that is not being shared with others goes out of scope,
        // the destructor gets called.
        auto shared_ptr_scoped = std :: make_shared<AudioBook>("Rabbit & Bear", "Julian Gough");
        shared_ptr_scoped->PlayBook();
    }

    // Shared pointer can also be initialized with null pointer
    auto shared_ptr_d(nullptr);

    std :: cout << "Program ends. Returning now" << std :: endl;
    return 0;
}

Output

Parameterized constructor for AudioBook got called
Playing : Sherlock Holmes
Playing : Sherlock Holmes
Playing : Sherlock Holmes
Parameterized constructor for AudioBook got called
Playing : Rabbit & Bear
Destructor for AudioBook got called
Program ends. Returning now
Destructor for AudioBook got called
#include <iostream>
#include <memory>

class Stock
{
    private:
    std :: string stock_name;
    // Shared pointer [ std :: shared_ptr<Stock> ] would give rise to circular references, causing a memory leak.
    // The memory leak could be fixed by making purchase a weak pointer [ std :: weak_ptr<Stock> ]
    std :: weak_ptr<Stock> purchase;

    public:
    Stock (const std :: string& name) : stock_name (name) {
        std :: cout << "Stock object [" << stock_name << "] created" << std :: endl;
    }

    ~Stock () {
        std :: cout << "Stock object [" << stock_name << "] destroyed" << std :: endl;
    }

    // // The Transact function purchases stock_b with stock_a and vice-versa thereby creating a circular references.
    static void Transact (std :: shared_ptr<Stock>& stock_a, std :: shared_ptr<Stock>& stock_b) {

        if (stock_a && stock_b) {
            stock_a->purchase = stock_b;
            stock_b->purchase = stock_a;
            std :: cout << "Transaction 1" << std :: endl;
            std :: cout << "Stock [" << stock_a->stock_name << "] purchased with stock [" << stock_b->stock_name << "]\n";
            std :: cout << "Transaction 2" << std :: endl;
            std :: cout << "Stock [" << stock_b->stock_name << "] purchased with stock [" << stock_a->stock_name << "]\n";
        }
    }
};

int main() {

   auto tesla { std :: make_shared<Stock>("Tesla") };
   auto google { std :: make_shared<Stock>("Google") };

   // Purchase Tesla stock with Google and vice-versa
   Stock :: Transact (tesla, google);
   return 0;
}

Output

Stock object [Tesla] created
Stock object [Google] created
Transaction 1
Stock [Tesla] purchased with stock [Google]
Transaction 2
Stock [Google] purchased with stock [Tesla]
Stock object [Google] destroyed
Stock object [Tesla] destroyed


Copyright (c) 2019-2024, Algotree.org.
All rights reserved.