Click here to Skip to main content
Click here to Skip to main content

Unique_ptr custom deleters and class factories

, 18 Sep 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
Making unique_ptr more user friendly.

Introduction

In this example, we'll explore customizing the deleter for the unique_ptr. This example uses char *, but any pointer type will do.

Background

In the early days of programming C, if you wanted a pointer, life was simple.

char *p = (char *)malloc(256);
strcpy( p, "Some text goes here" ); 
free(p);

If you allocated a pointer, you freed a pointer. The hardest issue to keep track of was which pointer was allocated.

 char *p = (char *)malloc(256);
 char *p2 = "This is some text";
 strcpy( p1, p2 );
 free(p);
 free(p2); // oops

Then C++ came along and not only do we have malloc, we now have new and new [], which require free, delete, and delete []. This completely ignores any Operating System specific calls to allocate and de-allocate memory. These three different ways are sufficient for our needs.

 char *p = malloc(256);
 char *p2 = new char;  // not terribly useful, but possible
 char *p3 = new char [256];
 char *p4 = "Just some random text";
 …
 free(p);
 delete p2;
 delete [] p3;

Along comes auto_ptr() but that only works with delete. That means it will only address one of the four presented cases. shared_ptr allows for customized deleter, but who wants to pay for the reference counting overhead when it isn’t used? Finally unique_ptr comes along. It does almost everything we want.

We can specify a template parameter and define how the memory will be deleted.

unique_ptr<char, void (*)(char *)> p( (char *)malloc(256), std::free );

At first (and even second) glance, this is confusing.

unique_ptr<char, void (*)(char *)> p( (char *)malloc(256), std::free );

The second template parameter says "I take a function pointer that has one parameter, it’s a char *, and I don’t’ return anything".

unique_ptr<char, void (*)(char *)> p( (char *)malloc(256), std::free );

The second parameter of the instantiation of the variable p, is the std::free function. When the unique_ptr p goes out of scope it will call the free function and we clean up the memory correctly.

unique_ptr has a template specialization to handle the array delete. We could define

unique_ptr< char [] > p3( new char[256] ); // ok, calls delete []

Now we can easily handle three out of the four cases.

unique_ptr<char, void (*)(char *)> p( (char *)malloc(256), std::free );
unique_ptr<char, > p2( new char );
unique_ptr<char [] > p3( new char [256] );

We have a problem with the pointer when no actual deletion is required. We can solve that by declaring our own solution.

void NoDelete( char * ) {}
unique_ptr<char, void (*)(char *)> p4( "Just some random text", NoDelete );

Now when p4 goes out of scope, it will call the NoDelete function, passing it the address of our text string. The NoDelete function will ignore the pointer and nothing will be done. We now cover every case where pointers are created and the de-allocation is correct. An issue arises if I want to do something with the pointer.

unique_ptr<char, void (*)(char *)> p( (char *)malloc(256), std::free );
unique_ptr<char, > p2( new char );
unique_ptr<char [] > p3( new char [256] );
void NoDelete( char * ) {}
unique_ptr<char, void (*)(char *)> p4( "Just some random text", NoDelete );

Suppose I want to create a function call PrintIt() that will print the contents of the unique_ptr. How should the function be defined? Each definition of the unique_ptr is different. There are a number of possible solutions:

  1. Pass raw pointer to PrintIt( char * );
  2. Template the function PrintIt( T const &ptr )
  3. Overload for each unique_ptr
  4. Make the unique_ptr declarations consistent

This article explores option 4. This option needs to make all the pointer definitions consistent. To do that, we need a definition that looks like:

unique_ptr<char, MyDeleter > p( (char *)malloc(256), std::free );
unique_ptr<char, MyDeleter > p2( new char , ???);
unique_ptr<char, MyDeleter > p3( new char [256], ??? );
unique_ptr<char, MyDeleter> p4( "Just some random text", NoDelete );

Our custom delete needs to handle a function pointer that takes a char *, does its magic, and returns nothing. To accomplish this, we will use std::tr1:: function. If you are using a more up to date compiler, use std::function. I’m using the VS 2010 compiler.

struct MyDeleter{
// What should be the default?? No deleter? delete? I choose to
// mimic the default of unique_ptr.
MyDeleter()
: f( [](char *p) { delete p;} )
{}

// allow the user to specify what type of deleter to use
explicit MyDeleter(std::tr1::function< void(char *)> const &f_ )
: f(f_)
{}

void operator()(char *p) const
{
f(p);
}

private:
std::tr1::function< void(char *)> f;
};

The default constructor of MyDeleter uses a lambda function which will simply call delete. This mimics the default behavior of unique_ptr.

The second constructor takes any function pointer that takes as its parameter a char * and returns void.

Let’s define a typedef to reduce the amount of clutter when typing.

typedef std::unique_ptr<char, MyDeleter > Unique_Ptr_2;

Now we just need to correct the syntax and fill in the ??? for our unknown function deleters.

Unique_Ptr_2 p ( (char *)malloc(256),     MyDeleter(std::free ) );
Unique_Ptr_2 p2( new char ,               MyDeleter( [](char *p) { delete p; } );
Unique_Ptr_2 p3( new char [256],          MyDeleter( [](char *p){ delete [] p; } );
Unique_Ptr_2 p4( "Just some random text", MyDeleter(NoDelete) );

For p2 and p3, we created anonymous lambda functions that take a char * and call delete or delete []. This can also be customized to work with Operating System specific allocation/de-allocation. If we wanted all the calls to look similar, we could instantiate all of them to use lambdas:

Unique_Ptr_2 p( (char *)malloc(256),     MyDeleter( [](char *p) { free(p); } ));
Unique_Ptr_2 p2( new char ,              MyDeleter( [](char *p) { delete p; } ));
Unique_Ptr_2 p3( new char [256],         MyDeleter( [](char *p){ delete [] p; } ));
Unique_Ptr_2 p4( "Just some random text", MyDeleter( [](char *){} ));

Because that’s a lot to type and remember, we can declare named lambdas to do the work.

auto CallFree =        [](char *p) { free(p); };
auto CallDelete =      [](char *p) { delete p; };
auto CallArrayDelete = [](char *p) { delete [] p; };
auto CallNoDelete =    [](char *)  {};
Unique_Ptr_2 p( (char *)malloc(256),     MyDeleter( CallFree ));
Unique_Ptr_2 p2( new char ,              MyDeleter( CallDelete ));
Unique_Ptr_2 p3( new char [256],         MyDeleter( CallArrayDelete ));
Unique_Ptr_2 p4( "Just some random text", MyDeleter( CallNoDelete ));

Now we can define our PrintIt function to work with all of our different types of pointers.

void PrintIt( Unique_Ptr_2 const & p ) {}

So what is the trade off? We now have another level of indirection when the pointer goes out of scope. The unique_ptr destructor calls our anonymous function which then calls the actual function that does the work.

This can also be extended to the class factory pattern. In this case, the caller shouldn’t care how the pointer was created or how it will be destroyed; they just want to use it.

Unique_Ptr_2 ClassFactory( int choice )
{
switch( choice ) {
case 0:
return Unique_Ptr_2 p( (char *)malloc(256), MyDeleter( CallFree ));
case 1:
//
} // end switch
} // end ClassFactory

Unique_Ptr_2 p = ClassFactory(0);

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

mzdude

United States United States
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web01 | 2.8.141022.2 | Last Updated 18 Sep 2012
Article Copyright 2012 by mzdude
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid