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>) << "\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.
}
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):