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:

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>) << "

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

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

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

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

"; //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 << "

"; //0 { //Block auto fh = Tracker::open("test.txt", "w"); if(fh) { std::fputs("Hello World!", fh.get()); } std::cout << Tracker::NumOpenFiles << "

"; //1 }//End Block. File Closed. std::cout << Tracker::NumOpenFiles << "

"; //0 }