C++11 unique_ptr : what makes it unique? (includes C++14 features)

The C++11 unique pointer (or unique_ptr) that we are about to discuss here does exhibit some of the properties of shared_ptr but not all. shared_ptr allow sharing of resources. And so naturally, many shared_ptr can share one same resource without creating any conflict with one another.

Link :C++11 shared_ptr

The unique_ptr, on the other hand, tend to be selfish and so sharing of a resource is not allowed. If a unique_ptr owns a memory it becomes the sole owner until it goes out of scope. Since each unique_ptr owns a unique storage hence, the name unique pointer.

C++11 unique pointer and shared pointer

By the looks of it wouldn’t it be more suited if unique_ptr was named selfish_ptr. Well, that did not happen because it has other properties which makes it worthy of the name unique. It won’t be an exaggeration to say those other properties out weight the absence of sharing tendency that unique_ptr exhibit. Before we discuss those unique properties, let us see first how to declare and initialize a unique_ptr.


Declaring and initializing unique_ptr

unique_ptr like the shared_ptr is a template and it’s constructor accept only an explicit type.That means only direct initialization is allowed. Like the shared_ptr function make_shared, C++14 has introduced make_unique function to allow the creation of a new storage and initialize it to unique_ptr.

The function make_unique will accept a value -as an argument- of the type. And what this function does is, it create a storage that can hold the value and return a unique_ptr that points to the storage. The returned unique_ptr is assigned to our unique_ptr. An example is provided below.

Link :3 points why shared_ptr and unqiue_ptr constructor are made explicit?

unique_ptr<int> up(new int(90) ) ;

unique_ptr<int> upp=make_unique<int>(4352) ; //work fine

unique_ptr<int>up1 ;

cout<< *up << endl //prints 90
<< *upp << endl ; //prints 4352

cout<< *up1 << endl; //undefine

The unique_ptr up1 is not initialized to any memory, it is a doodler pointer. For such pointer the storage can be assigned later using the member function reset() or release() (discussed later).

You may think we can also use the copy constructor or assignment operator to assign a storage to unique_ptr. No! we can’t the compiler won’t allow it.

unique_ptr<int>up(new int(90)) ;

unique_ptr<int>up1(up) ; //error! copy constructor call

//or say
up1=up ; //error! assignment operator call

The reason why the above assignment method is not allowed is explained in another post.



why copy constructor and operator=( ) are disabled?

The reason for this tends to bend towards the behaviour the unique_ptr exhibit. The unique_ptr purpose is to have the solemn right over the storage which it points to. Copy constructor and operator=() purpose, on the other hand, is to share the storage or make sharing of the object with another object possible. If you compare their purpose they are like fire and water.

If you are using code::blocks and if you go to the directory “..\MinGW\lib\gcc\mingw32\4.7.1\include\c++\bits\unique_ptr.h” and go to lines 256 and 2557, you will see something like this:

unique_ptr(const unique_ptr&) = delete ;

unique_ptr& operator=(const unique_ptr&) = delete ;

The =delete at the end of the function name means the compiler will not generate those functions for you. In other sense, it means they are disabled so they cannot be called.

If you want to transfer the ownership of the storage from one unique_ptr to another you can use the reset( ) and the release( ) member function.These two member functions and some other member functions are discussed here.


Passing a unique_ptr as argument.

When you want to pass a unique_ptr to another function what are some of the ways the compiler allows you to do so? The compiler allows you to pass a normal object as a value or as a reference or as a pointer. But when you are passing a unique_ptr you cannot pass it as a value. The reason is simple, when you pass an argument as a value the copy constructor is called to copy the argument but we know unique_ptr doesn’t allow copy constructor call. Hence, the compiler won’t allow it.

void func( unique_ptr<int> upArg )
{
cout<< *upArg << endl ;
}

int main( )
{
unique_ptr<int>up(new int(90) ) ;

func(up) ;  //error!

return 0 ;
}

To pass a unique_ptr as an argument you can either pass it as a reference or as a pointer. In both the cases, there is no extra need for call to copy constructor as we are simply passing the address of the unique_ptr.

If the argument is a unique_ptr pointer then to access the value in the funtion a double ‘*’ sign must be used.Consider the program below.

void funcRefArg( unique_ptr<int> &Arg ) //pass by reference
{
cout<< *Arg ;
}

void funcPtrArg(unique_ptr<int> *Arg )//Arg is a pointer to unique_ptr
{
cout<< *(*Arg ) ; //Beware 
}

int main( )
{
unique_ptr<int>up(new int(90) );

funcRefArg(up) ;

funcPtrArg(&up) ;

return 0 ;
}

In the function ‘funcPtrArg’ why ‘*’ is written two times to access the value? a brief description is given below.

unique_ptr are not pointers but an object made to behave like a pointer. When unique_ptr is passed as pointer we are passing the address of the object note, the object does not point to the storage. There is another pointer inside the unique_ptr class which points to the storage. By using double ‘*’ sign we are using that pointer inside unique_ptr class to access the value.

Among the double ‘*’ sign, the inside ‘*’ sign represent the normal syntax form that accompanies while accessing the unique_ptr value and the outside ‘*’ sign is due to the argument being passed as actual pointer.


Returning unique_ptr from a function

Returning a unique_ptr from a function is allowed. When a function returns a unique_ptr the value is copied to the variable in the receiving side. This is in fact contradictory to the fact that any copying or sharing of resources is not allowed with unique_ptr. However, in this case, an exception is applied.Consider the code below.

unique_ptr<int> funcRetArg( )
{
unique_ptr<int>up(new int(8)) ;

return up ;
}

int main( )
{
int i=*( funcRetArg( ) ) ;
cout<< *( funcRetArg( ) ) ;

return 0 ;
}

The unique_ptr value returned but the funcRetArg( ) is copied to the ‘i’ variable,but why does unique_ptr allow copying of data here?

When the function funcRetArg( ) execution ends the memory occupied by ‘up’ becomes a temporary storage, it will get destroy as soon as the value of ‘up’ is assigned to ‘i’. Since the storage is temporary and has no further uses the unique_ptr will allow copying of the data.

We give another term for such behaviour of unique_ptr -allowing copying of temporary object- as moving of object. How does unique_ptr perform moving of object and the underlying principle of this method is discussed in another post. For now, knowing that unique_ptr allows copying of temporary object through moving of object is good enough.



Array and unique pointer

Suppose you want to make a unique_ptr manage an array resources, in such case you cannot use the normal unique_ptr syntax declaration and allocate an array storage and make the unique_ptr point to the array storage. This method cannot actually making the unique_ptr handle the array and things can go wrong.Consider the code below.

unique_ptr<int>up(new int[2]{4,5} );

cout<< *up ; //access the first element

The above method holds two disadvantages :
 
i)You cannot access the second element,so up[1] is undefined .
 
ii)The pointer ‘up’ will only take responsibility for the first element. This means when the pointer ‘up’ goes out of scope it will only free the storage of the first element and the second element storage is not destroyed. And we do not want this.

To solve this problem C++11 introduce another way of managing an array. In this method, we have to make the template know that we are making the unique_ptr manage an array type resources. To do this we will add the ‘[ ]‘ after the data type when we passed the template argument type during the unique_ptr declaration. The code below shows an instance of this method.

unique_ptr< int[] > upArr(new int[3]{3,5,6} ); //note the ‘[]’ after the type

cout<< apArr[0] << endl
 << apArr[1] << endl
 << apArr[2] << endl ;

Now we have no problem accessing all the elements of the array and also all the storage is deleted safely when the pointer upArr goes out of scope.

The drawback of adding the subscript ‘[]’ in the template argument declaration is the ‘*’ operator becomes non-functional. Accessing the array with ‘*’ operator is just plain error.

cout<< *apArr ; //error

Adding a deleter function to unique_ptr.

The purpose of a deleter function is to delete the storage when the unique_ptr or shared_ptr goes out of scope. There are two ways to pass deleter function to shared_ptr: passing the deleter function name during initialization and secondly, during reset() function call.

In unique_ptr we can also call the deleter function. However, we cannot call the deleter function when the reset() function is called, we can only call it when the unique_ptr is initialized.

During unique_ptr initialization, the deleter function name is passed as the second argument and it is also required that the pointer to the deleter function type is mentioned as the second type in the template parameter. Can’t understand what this means? look at the code below.

void deleterFunction(int *memPtr )
{
delete memPtr ;
}

unique_ptr<int , deleterFunction_Type>up(new int(89) , deleterFunction ) ;

The second argument type of the unique_ptr which is deleterFunction_Type is the pointer to the deleterFunction type. To obtain a function pointer type we can use either a typedef or decltype( ) function (if you don’t know what is decltype() visit this link) .The code below shows how to achieved it.

void deleterFunction( int *memPtr )
{
delete i ;
}

typedef void ( *deleterFunction_Type )( int *memPtr ) ; /*deleterFunction_Type is a pointer to deleterFunction type */

int main( )
{
 {
 unique_ptr<int , deleterFunction_Type>up(new int(89) , deleterFunction ) ; //works fine

//or you can also use decltype( )
unique_ptr<int , decltype( deleterFunction )* >up(new int(89) , deleterFunction ) ; //works fine

 }//deleterFunction( ) is called here because the scope ends here

return 0 ;
}

If decltype( ) is used, note the ‘*‘ sign which is appended after the closing bracket.Actually, decltype() only returns the type of the deleterFunction, but we want a pointer to that function as a second argument type so the ‘*’ sign is attached.

The member functions of unique_ptr is discussed here unique_ptr member functions.


Casting of unique_ptr

We can cast a raw pointer to unique_ptr using static_cast function. Note make_unique cannot convert a raw pointer to unique_ptr it can only create a storage for unique_ptr to point to.

int *mem=new int( 1234 ) ;

unique_ptr<int>up=static_cast< unique_ptr<int> >( iNew ) ;

cout<< *up ;

Casting is allowed, that doesn’t mean you should cast the raw pointer whenever you want to, it is insecure and error-prone due to the reason discuss later in the topic “Limitations of unique pointer“.

There is no way to cast a unique_ptr to a raw pointer or another smart pointer. Even if casting from unique_ptr to raw pointer is allowed it wouldn’t make any sense because raw pointer is just a built-in pointer and unique_ptr is a class template. Also, conversion between the smart pointers are not allowed, if it is allowed we can already foresee many insidious errors it may introduce in our program.


Limitation of unique_ptr

The limitation of unique_ptr is visible when a raw pointer is assigned to two different unique_ptr. In such a scenario when one of the unique_ptr goes out of scope it would have deleted the storage and the other would be pointing to some invalid storage. This also means when the other pointer goes out of scope it will try to delete the same storage again which is, in fact, a dangerous action.Consider the code below.

int *iMem=new int(893) ;

unique_ptr<int> up=static_cast<int>(iMem) ;

{
unique_ptr<int> up1=static_cast<int>(iMem) ;
}///storage deleted here

cout<< *up ; ///undefined

//when the program ends, up1 will try to delete the storage again

The best advice to protect against such error is never use raw pointer to assign to any unique_ptr.

There is another post when unique_ptr must be used 2 points why unique_ptr should be preferred.




Leave a Reply

Your email address will not be published. Required fields are marked *