std::unique_ptr


Smart Pointers

Smart pointers are wrappers around raw pointers that act much like the raw pointers they wrap, but that avoid
many of their pitfalls. You should therefore prefer smart pointers to raw pointers. Smart pointers can do virtually everything raw pointers can, but with far fewer opportunities for error.

std::auto_ptr is a deprecated leftover from C++98. It was an attempt to standardize what later became C++11’s std::unique_ptr. Doing the job right required move semantics, but C++98 didn’t have them. As a workaround, std::auto_ptr co-opted its copy operations for moves. This led to surprising code (copying a std::auto_ptr
sets it to null!)

std::unique_ptr does everything std::auto_ptr does, plus more. It does it as efficiently, and it does it without warping what it means to copy an object. It’s better than std::auto_ptr in every way. The only legitimate use case for std::auto_ptr is a need to compile code with C++98 compilers. Unless you have that constraint, you should replace std::auto_ptr with std::unique_ptr and never look back.



std::unique_ptr for exclusive-ownership

  • Size of std::unique_ptr is equal to a raw pointer(Custom deleter functions can increase the size) 
  • std::unique_ptr embodies exclusive ownership semantics.
  • A non-null std::unique_ptr always owns what it points to.
  • Moving a std::unique_ptr transfers ownership from the source pointer to the destination pointer. (The source pointer is set to null.)
  • Copying a std::unique_ptr isn’t allowed, because if you could copy a std::unique_ptr, you’d end up with two std::unique_ptrs to the same resource, each thinking it owned that resource.
  • std::unique_ptr is thus a move-only type.
  • Upon destruction, a non-null std::unique_ptr destroys its resource.
  • By default, resource destruction is accomplished by applying delete to the raw pointer inside the std::unique_ptr
  • std::unique_ptr comes in two forms, one for individual objects(std::unique_ptr<T>) and one for arrays (std::unique_ptr<T[]>).
  • During construction, std::unique_ptr objects can be configured to use custom deleters (Function objects, including those arising from lambda expressions) to be invoked when its time for their resources to be destroyed. 
  • std::unique_ptr<ClassName,decltype(lambda)> Obj (nullPtr, lambdaFunction)
  • When a custom delete is to be used, its type must be specified as the second type argument to std::unique_ptr.
  • std::unique_ptr with default deleter looks like std::unique_ptr<int,std::default_delete<int>>
  • All custom deletion functions accept a raw pointer to the object to be destroyed. The custom delete takes a parameter of type Base*. Regardless of the derived object created, it will ultimately be deleted. This means we will be deleting a derived class object via a base class pointer.
  • Converting a std::unique_ptr to a std::shared_ptr is easy.


A sample program without std::unique_ptr looks like this

void funWithRawPointer(int x)
{
      int *ptr = new int(20);

      if( x%2 == 0)
      {
                 cout<<x<<" is an Even number\n";
                 delete ptr;
                 return;
      }
      else if( x%2 == 1 )
      {
                 cout<<x<<" is an Odd number\n";
                 delete ptr;
                 return;
      }
}

As you can see from the above example, the dynamically allocated pointer on heap has to be deleted before returning from the function to save memory leak. It can be solved with std::unique_ptr 

void funWithRawPointer(int x)
{
     std::unique_ptr ptr(new int(20));


      if( x%2 == 0)
      {
                 cout<<x<<" is an Even number\n";
                 return;
      }
      else if( x%2 == 1 )
      {
                 cout<<x<<" is an Odd number\n";
                 return;
      }
}

In this modified version we don;t have to explicitly delete the pointer, because that is the beauty of std::unique_ptr, calls the destructor when goes out of scope.

std::unique_ptr cannot be moved.

If we have a function which accepts unique_ptr as an argument.

void fun(std::unique_ptr<int> uPtr);

Following are the ways to call the above fun.


  • fun(std::unique_ptr<int>(new int(20)));
  • fun(std::make_unique<int>(20));
  • std::unique_ptr<int> ptr(new int(20)); fun(move(ptr));
  • fun(ptr) ; // It gives an error because the copy operations are deleted in the unique pointer implementations.


As stated earlier, std::unique_ptr takes custom deleter functions as an optional argument. std::unique_ptr with default deleter looks like std::unique_ptr<int,std::default_delete<int>>

The passed deleter functions increases the size of std::unique_ptr, based on number of variables captured and used in the lambda function.


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

using namespace std;

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
       return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

int main()
{
   int _int = 20;
   int *rawPointer = &_int;
   double _double = 10.2;

   std::string _str = "Unique pointer Testing";

   cout<<"Sizes Of data members\n";
   cout<<"===================================\n";
   cout<<"sizeof(int) : "<<sizeof(_int)<<endl;
   cout<<"sizeof(rawPointer) : "<<sizeof(rawPointer)<<endl;
   cout<<"sizeof(double) : "<<sizeof(_double)<<endl;
   cout<<"sizeof(string) : "<<sizeof(_str)<<endl;
   cout<<"===================================\n";

   std::unique_ptr<int> uPtr1 = make_unique<int>(10);


   auto deleterFun1 = [](int *x)
   { 
      cout<<"Empty Capture\n";
      delete x;
   };

   auto deleterFun2 = [&](int *x)
   { 
      cout<<"Capture by Reference\n";

      // By Priting the captured variable, the size of the unique_ptr associated with
      // this deleterFunction will increase.
      cout<<_int<<endl;
      cout<<_double<<endl;
      delete x;
   };
   auto deleterFun3 = [=](int *x) 
   {   
      cout<<"Capture by Value\n";

      // By Priting the captured variable, the size of the unique_ptr associated with
      // this deleterFunction will increase.

      cout<<_int<<endl;
      cout<<_double<<endl;
      delete x;
   };

   std::unique_ptr<int,decltype(deleterFun1)> uPtr2(nullptr, deleterFun1);
   uPtr1.reset(new int(10));

   std::unique_ptr<int,decltype(deleterFun2)> uPtr3(nullptr, deleterFun2);
   uPtr2.reset(new int(10));

   std::unique_ptr<int,decltype(deleterFun3)> uPtr4(nullptr, deleterFun3);
   uPtr3.reset(new int(10));

   cout<<"Raw Pointer : Size - "<<sizeof(rawPointer)<<endl;
   cout<<"unique_ptr (No Custom Deleter) : Size - "<<sizeof(uPtr1)<<endl;
   cout<<"unique_ptr (Custom Deleter. No Capture) : Size - "<<sizeof(uPtr2)<<endl;
   cout<<"unique_ptr (Custom Deleter. Capture by Reference) : Size - "<<sizeof(uPtr3)<<endl;
   cout<<"unique_ptr (Custom Deleter. Capture by vaue) : Size - "<<sizeof(uPtr4)<<endl;

}

We can make a few observations by running the code.
The size of a std::unique_ptr without a custom deleter and a std::unique_ptr with custom deleter which doesn't capture anything is the same.

We can capture the local variables in lambda either, by value(=) or by reference(&).

Here are two interesting sub-observations-

  • If we capture all the local variables by using = or & in the capture list, but do not use it, we do not have to pay for it ( in terms of extra memory). However if we use a particular captured variable ( or subset of variables), memory of std::unique_ptr is increased corresponding to it's ( their ) size .
  • If we capture a specific variable by mentioning the variable name in capture list, then it adds to the memory of the unique_ptr, irrespective of whether it is used or not.

Comments

Popular posts from this blog

C++ Guidelines for Multithreaded System

Signalling System #7(SS7) Protocol.

std::shared_ptr