unique_ptr with custom deleter

Question | Mar 31, 2020 | hkumar 

Intro

An std::unique_ptr is a smart pointer that exclusively manages the lifetime of an object. The managed object is deleted when the unique_ptr is destroyed.

A unique_ptr can be declared and initialized with a user-provided custom deleter that is called to destroy the managed object. When no custom deleter is specified, std::unique_ptr uses the std::default_delete that calls the delete operator to deallocate memory for the object.

A unique_ptr can be used to manage a resource that is acquired from a pool or a pre-initialized store of resources. Another case in hand is that we might have to construct an object at a shared memory or a custom cache location. A custom deleter is required in those cases to release the resource back to the allocator.

Let's take an example of such a scenario. In the following code, the class Pool holds a few expensive Resource objects in a cache (array) that are initialized when the application starts. We assume that the instance of Pool lives for the lifetime of the process. The method get() of Pool returns a unique_ptr that contains a pointer to one of the Resource objects in the cache and calls a custom deleter to release it. A Resource is marked not-free when it is acquired. The custom deleter marks the Resource free, so it can be obtained again, as shown below:

struct Resource {
 //..stuff..
 bool isFree{true};
};

class Pool {
public:
 //...
 //Custom Resource deleter
 struct Deleter {
  //Called by unique_ptr to destroy/free the Resource
  void operator()(Resource* r) {
    if(r)
     r->isFree = true; // Mark the Resource as free
  }
 };

 //auto return type requires C++14
 auto get() {
  //Create the unique_ptr with nullptr   
  auto rp = std::unique_ptr<Resource, Deleter>(nullptr, Deleter());   
  //Find the first free Resource
  for(auto& r : resources) {
   if(r.isFree) {
    //Found a free Resource
    r.isFree = false; //Mark the Resource as not-free
    rp.reset(&r); //Reset the unique_ptr to this Resource*
    break;
   }
  }  
  return rp;
 }
//...
private:
 Resource resources[5]; //Cache of Resources
};

Deleter is passed as an argument to constructor and stored as a member of a unique_ptr object. Moreover, the deleter's type is a parameter to the std::unique_ptr template. Consequently, a deleter can be a function object, a function pointer, a lambda, or even a reference to the aforementioned. Here is an illustration that shows the association between Pool, Resource, and the unique_ptr with the custom deleter:


unique_ptr Resource custom deleter


It should be emphasized that a deleter might not occupy any space at all in a unique_ptr object. For instance, a typical implementation of unique_ptr utilizes the empty base optimization, such that an empty function object or a capture-less lambda does not take any space. In those cases, the size of a unique_ptr is the same as the size of a raw pointer. However, a function pointer or a function object with data members or an std::function custom deleter increases the size of unique_ptr object. This fact should be taken into consideration during the design where a significant number of unique_ptr objects are to be kept in memory.

For a reference, these are the sizes of unique_ptr with the different types of deleters on a typical system:

//With default function object (std::default_delete). 
std::cout << sizeof(std::unique_ptr<int>) << "\n"; //8

//With a custom empty function object. 
struct CD { void operator()(int* p) { delete p; } };
std::cout << sizeof(std::unique_ptr<int, CD>) << "\n"; //8

//With a capture-less lambda. 
auto l = [](int* p) { delete p; };
std::cout 
     << sizeof(std::unique_ptr<int, decltype(l)>) 
     << "\n"; //8 

//With a function pointer. 
std::cout 
    << sizeof(std::unique_ptr<int, void(*)(int*)>) 
    << "\n"; //16

//With a std::function. Much more expensive.
std::cout
    << sizeof(std::unique_ptr<int, std::function<void(int*)>>)
    << "\n"; //64

Back to our example. The following code shows how we can use Pool to get a Resource, which is freed automatically when the holding unique_ptr is destroyed. It is essential to consider that the type of custom deleter is passed as a template parameter, so it becomes a part of a unique_ptr's declaration. Therefore, we declare an alias to the unique_ptr below to make the code more readable:

using ResourcePtr = std::unique_ptr<Resource, Pool::Deleter>;
//...
void foo(Pool& pool) { 
 ResourcePtr rp;
 //...
 rp = pool.get(); //OK. Invokes move-assignment 
 if(rp) {
  //Use resource...     
 }
 //Resource is freed when 'foo' returns.
} 

Another Example and A Question

Let's take a contrived example of managing file handles with unique_ptr. The Tracker class tracks (increments a counter) the files as they are opened and closed. Client code calls the Tracker::open static method to get a unique_ptr of an opened file-handle (FILE*). The returned unique_ptr is created with a custom deleter (Closer) that closes the file, as shown below:

//Closer (Custom Deleter)
using Closer =  _______; //Intentionally Omitted

struct Tracker {

 static void close(std::FILE* fp) {
  if(fp) {
   //Track open files. Decrement counter
   NumOpenFiles--;
   std::fclose(fp); // Close the file
  }
 }

 static auto
 open(const char* fileName, const char* mode) {
  //Create unique_ptr of FILE*. Tries to open the file.  
  auto fh = std::unique_ptr<std::FILE, Closer>(std::fopen(fileName, mode),
                                                 &Tracker::close);

  if(fh) { //If not NULL, the file is open
   //Track open file. Increment counter
   NumOpenFiles++;
  }  
  return fh;
 }

 static int NumOpenFiles;
};

int Tracker::NumOpenFiles = 0; //Initialize static counter 

The Tracker can be used as follows:

void qux() {
 std::cout << Tracker::NumOpenFiles << "\n"; //0
 { //Block
  auto fh = Tracker::open("test.txt", "w");
  if(fh) {
   std::fputs("Hello World!", fh.get());
  }   
  std::cout << Tracker::NumOpenFiles << "\n"; //1
 }//End Block. File Closed.
 std::cout << Tracker::NumOpenFiles << "\n"; //0
}

We have deliberately omitted the type of the custom deleter alias (Closer) in the above code. Here, we are not concerned about any increase in the size of the unique_ptr object due to the Closer. Choose the correct choice below that can be used as a type for the Closer (check Explanations for details on the correct answer):