In the previous blog post, we implemented a simple unique_ptr class to demonstrate how its internal mechanics work. One crucial aspect of unique_ptr is the deleter, which I briefly mentioned earlier. In this post, we will focus on the deleter in detail. To begin, let’s review its standard definition 1

template<
    class T,
    class Deleter = std::default_delete<T>
> class unique_ptr;

The deleter is a callable entity that operates on a pointer of the specified template type. By default, it uses the delete operator to free the allocated memory. However, the key feature of unique_ptr is its flexibility, allowing users to replace the default deleter with a custom one.

For instance, consider a scenario where a codebase manages files using fopen and fclose instead of std::fstream. Suppose the code owner prefers this approach for some unspecified reason:

FILE* handle = fopen("sample", "r");
if (nullptr == handle) {
    return -1;
}
// Perform operations
fclose(handle);

By leveraging the powerful RAII (Resource Acquisition Is Initialisation) technique, developers can use unique_ptr to manage resources beyond memory, such as sockets, files, or database connections, ensuring appropriate cleanup at the end of the scope.

Here’s how to adapt the previous example to use unique_ptr:

#include <cstdio>
#include <memory>

int main() {
    using Handler = std::unique_ptr<FILE, decltype(&fclose)>;
    auto fileHandler = Handler(fopen("sample", "r"), &fclose);
    if (!fileHandler) { // utilises the `operator bool`
        return -1;
    }
    // Perform operations
    return 0;
}

Using a Lambda to Execute a Function at Scope End

unique_ptr can also invoke a function when it goes out of scope, which is particularly useful for non-memory resource management. Here’s an example:

#include <iostream>
#include <memory>

void end_of_scope(int* final_count) {
    std::cout << "Final count: " << *final_count << "\n";
}

int main() {
    using ScopeFunc = std::unique_ptr<int, decltype(&end_of_scope)>;
    int count = 100;
    ScopeFunc scope{&count, &end_of_scope};
    return 0;
}

Managing Dynamically Allocated Arrays

unique_ptr supports dynamically allocated arrays, though it’s worth noting that using std::string or std::vector is often more practical. However, in some embedded environments, using these containers may be restricted. Here’s an example of managing a dynamically allocated array with unique_ptr:

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<char[]> message{new char[]{"HelloWorld!"}};
    std::cout << message.get() << '\n';
    return 0;
}

While alternatives like std::string or std::vector are generally preferred for such tasks, unique_ptr with arrays remains a valid option, especially in environments with limited library support.

In summary, unique_ptr is a versatile tool for managing dynamically allocated resources, whether they are memory, files, or other types of resources, thanks to its customisable deleter.