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);
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; 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] );
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:
- Pass raw pointer to
PrintIt( char * );
- Template the function
PrintIt( T const &ptr )
- Overload for each
unique_ptr
- 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 <code><code>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{
MyDeleter()
: f( [](char *p) { delete p;} )
{}
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:
}
}
Unique_Ptr_2 p = ClassFactory(0);