How to Safely Use std::shared_ptr with Custom Deleters

In modern C++ the std::shared_ptr is a powerful tool for managing shared ownership of dynamically allocated objects. However, when the resources to be managed are not plain heap objects—such as files, sockets, or memory allocated by a C API—providing a custom deleter becomes essential. This article walks through the nuances of creating and using std::shared_ptr with custom deleters, highlights common pitfalls, and demonstrates idiomatic patterns to keep your code safe and maintainable.

Why Custom Deleters Matter

`std::shared_ptr

` by default calls `delete` on its managed pointer. For many use cases this is fine, but in several scenarios you must: – **Close files**: Use `std::fclose` instead of `delete`. – **Release network sockets**: Call `closesocket` (Windows) or `close` (POSIX). – **Free memory from a custom allocator**: Use `allocator.deallocate`. – **Destroy objects constructed by placement new**: Invoke the destructor manually. A mismatched deleter can lead to resource leaks, double frees, or undefined behavior. ## Basic Syntax “`cpp std::shared_ptr sp(new Foo, [](Foo* p){ delete p; }); “` The lambda is the custom deleter. It receives the raw pointer `p` and performs the required cleanup. The deleter must be CopyConstructible because `std::shared_ptr` copies it when it copies the pointer. ## Common Patterns ### 1. Function Pointers “`cpp std::shared_ptr file( fopen(“data.txt”, “r”), [](FILE* f){ if (f) fclose(f); } ); “` A simple function pointer is often enough for standard C library resources. ### 2. Functors (Stateful Deleters) “`cpp struct FileDeleter { void operator()(FILE* f) const { if (f) fclose(f); } }; std::shared_ptr file( fopen(“data.txt”, “r”), FileDeleter{} ); “` Using a functor allows you to store additional state if needed (e.g., a logging context). ### 3. Binding with `std::bind` “`cpp auto deleter = std::bind(&ResourceAllocator::deallocate, &allocator, std::placeholders::_1); std::shared_ptr ptr(rawPtr, deleter); “` This is handy when the deleter needs to call a member function on an existing object. ## Handling Arrays `std::shared_ptr` does not know that an array was allocated with `new[]` by default. Provide a deleter that calls `delete[]`: “`cpp int* arr = new int[10]; std::shared_ptr arrPtr(arr, [](int* p){ delete[] p; }); “` Alternatively, use `std::unique_ptr` which handles arrays natively and can be converted to `shared_ptr` if shared ownership is required: “`cpp std::unique_ptr arr(new int[10]); std::shared_ptr arrPtr(std::move(arr), [](int* p){ delete[] p; }); “` ## Thread Safety The control block (reference count) of `std::shared_ptr` is thread-safe. However, the deleter itself may not be. If your deleter performs I/O or accesses shared data, protect it with mutexes or design it to be re-entrant. ## Pitfalls to Avoid | Pitfall | What Happens | Prevention | |———|————–|————| | **Mismatched allocation/deallocation** | UB or leaks | Verify that `new` matches `delete`, `new[]` matches `delete[]`. | | **Deleting a null pointer** | Safe in C++ but can hide logic errors | Explicitly check or use standard library functions that handle `nullptr`. | | **Owning a pointer not allocated on the heap** | UB | Only wrap heap-allocated objects. | | **Returning a raw pointer that outlives the `shared_ptr`** | Use after free | Ensure the `shared_ptr` lives as long as the raw pointer is used. | ## Real‑World Example: Managing a C++ Socket “`cpp #include #include #ifdef _WIN32 #include #else #include #include #endif // A wrapper that ensures the socket is closed when no longer used. std::shared_ptr makeSocket(int fd) { #ifdef _WIN32 auto closeFn = [](int* p){ if (*p != INVALID_SOCKET) closesocket(*p); }; #else auto closeFn = [](int* p){ if (*p != -1) close(*p); }; #endif return std::shared_ptr (new int(fd), closeFn); } int main() { int sock = socket(AF_INET, SOCK_STREAM, 0); auto sockPtr = makeSocket(sock); // Use *sockPtr for network operations… // No explicit close needed; it will be called automatically. } “` This pattern keeps the socket lifetime under the control of the smart pointer, simplifying error handling and preventing leaks even in the presence of exceptions. ## Summary – **Custom deleters** enable `std::shared_ptr` to manage non‑`delete` resources safely. – **Lambdas** and **functors** are flexible; choose based on state requirements. – **Thread safety**: the control block is safe; the deleter may need protection. – **Arrays**: provide `delete[]` deleter or use `unique_ptr` with array specialization. – **Avoid mismatched allocation/deallocation** and other common pitfalls. By mastering custom deleters, you can confidently use `std::shared_ptr` to handle a wide range of resources, keeping your C++ code robust, exception‑safe, and expressive.

发表评论