C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
52: Custom Deleters for Smart Pointers
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.
There are no comments for now.