Skip to Content
Course content

52: Custom Deleters for Smart Pointers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Throughout this course, we've treated smart pointers as the ultimate solution for memory management. Usually, that means std::unique_ptr or std::shared_ptr handles a piece of memory allocated with new and cleans it up with delete. But in the real world, you're rarely just managing memory. You're managing resources: file handles, database connections, sockets, or legacy C-style API handles that have their own specific teardown functions.

The crash you'll get by trusting the default

Let's say you're working with a legacy C library, or even just the standard cstdio header. You use fopen to get a FILE*. Your first instinct—the one that's actually a trap—is to wrap that pointer in a std::unique_ptr to ensure you don't leak the file handle if an exception throws. It looks clean, right?

FILE* fp = std::fopen("data.txt", "r");
std::unique_ptr<FILE> filePtr(fp); 
// ... do some work ...

This code will compile, but it will crash your program the moment filePtr goes out of scope. Why? Because std::unique_ptr's default behavior is to call delete. However, a FILE* wasn't allocated with new; it was allocated by the OS/C-runtime. Calling delete on a pointer that wasn't returned by new is undefined behavior, and in practice, it usually leads to a segmentation fault or a heap corruption error. I've seen this trip up even senior devs who are so used to "just use a smart pointer" that they forget what the smart pointer is actually doing under the hood.

Teaching the pointer how to clean up

To fix this, we need to tell the smart pointer: "Don't use delete; use fclose instead." This is where custom deleters come in. For a std::unique_ptr, the deleter isn't just a constructor argument; it's part of the type itself. I usually prefer using a function pointer or a lambda for this.

auto fileCloser = [](FILE* fp) { 
    if (fp) std::fclose(fp); 
};

// Note the second template argument: the type of the deleter
std::unique_ptr<FILE, decltype(fileCloser)> filePtr(std::fopen("data.txt", "r"), fileCloser);

Now, when filePtr goes out of scope, it invokes the lambda, which calls fclose. The resource is cleaned up correctly, and we still have the RAII guarantees we wanted. It's a small change, but it turns a ticking time bomb into professional-grade code.

The hidden cost of unique vs. shared deleters

Here is where it gets interesting, and where you have to make a design choice based on performance and API cleanliness. Notice that for std::unique_ptr, I had to add decltype(fileCloser) to the template signature. This means if you pass this pointer to another function, that function must also know the exact type of the deleter. It makes the type signatures bulky.

std::shared_ptr handles this differently. It uses something called "type erasure." When you pass a custom deleter to a shared_ptr, it doesn't become part of the type. You can just use std::shared_ptr<FILE>, and the pointer will remember how to delete itself internally.

// Much cleaner type signature
std::shared_ptr<FILE> filePtr(std::fopen("data.txt", "r"), std::fclose);

So why not use shared_ptr every time? Because type erasure isn't free. shared_ptr has to allocate a "control block" on the heap to store the reference count and the deleter. unique_ptr, if you use a stateless lambda or a function pointer, often has zero overhead—the compiler can inline the deleter call, and the pointer remains the size of a single raw pointer. If you're writing a high-performance driver or a tight loop, that extra heap allocation in shared_ptr is a non-starter. If you're writing high-level application logic, the cleaner syntax of shared_ptr is usually worth the cost.




📋 Practical Task

Exercise: Implementing a Safe Win32/POSIX Handle Wrapper

In many system-level APIs, you deal with "handles" (which are often just void* or int) that must be closed with a specific function like CloseHandle (Windows) or close() (POSIX).

Your task is to create a small program that simulates this. Since we want this to be cross-platform for the exercise, I've provided a mock API below. You need to implement a ResourceGuard using std::unique_ptr that ensures Mock_ReleaseResource is called automatically.

#include <iostream>
#include <memory>

// --- MOCK API (Do not modify this part) ---
void* Mock_AcquireResource() {
    std::cout < "[API] Resource acquired\n";
    return new int(42); // Simulating a handle
}

void Mock_ReleaseResource(void* handle) {
    std::cout < "[API] Resource released\n";
    delete static_cast<int*>(handle);
}
// -----------------------------------------

int main() {
    // TODO: Create a std::unique_ptr that manages the resource 
    // returned by Mock_AcquireResource.
    // It must use Mock_ReleaseResource as the custom deleter.
    
    // Ensure that the "Resource released" message prints 
    // automatically when the pointer goes out of scope.
    
    return 0;
}

Requirements:

  • Use std::unique_ptr.
  • Use a custom deleter (either via a lambda or function pointer).
  • The program must print "Resource acquired" and then "Resource released" without any manual calls to the release function in main.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.