When you work with dynamic resources in C++, the standard library’s smart pointers are your best friends. std::shared_ptr offers reference‑counted ownership, but its default deleter only knows how to delete raw pointers. In real applications you often need to free resources in a custom way—for example, closing a file handle, releasing a DirectX texture, or invoking a C API cleanup function. A custom deleter can be supplied either at construction time or via std::shared_ptr::reset. Below is a step‑by‑step guide to creating and using a std::shared_ptr with a custom deleter, along with some common pitfalls and performance considerations.
1. Defining a Custom Deleter
A deleter is any callable object that takes a pointer of the same type that the shared_ptr manages. The simplest form is a lambda:
auto fileDeleter = [](FILE* f) {
if (f) {
std::fclose(f);
std::printf("File closed.\n");
}
};
If the resource needs more context, you can wrap the deleter in a struct:
struct TextureDeleter {
void operator()(ID3D11Texture2D* tex) const {
if (tex) {
tex->Release(); // DirectX specific
std::printf("Texture released.\n");
}
}
};
2. Constructing the shared_ptr
Pass the custom deleter to the constructor:
std::shared_ptr <FILE> filePtr(
std::fopen("example.txt", "r"),
fileDeleter // custom deleter
);
Or if you already have a raw pointer:
FILE* rawFile = std::fopen("example.txt", "r");
std::shared_ptr <FILE> filePtr(rawFile, fileDeleter);
For types that require allocation via a factory function:
auto createTexture = []() -> ID3D11Texture2D* {
// ... create texture ...
return tex;
};
std::shared_ptr <ID3D11Texture2D> texPtr(
createTexture(),
TextureDeleter()
);
3. Using reset to Switch Resources
You can replace the managed object while keeping the same shared_ptr:
filePtr.reset(std::fopen("another.txt", "w"), fileDeleter);
The old resource will be freed automatically before the new one is set.
4. Avoiding Common Mistakes
| Mistake | Why it’s a problem | Fix |
|---|---|---|
| Using a lambda that captures by reference | Captured references may dangle if the lambda outlives the captured objects. | Capture by value or use a stateless deleter. |
| Forgetting to check for null | Some APIs may return null; the deleter must handle it gracefully. | if (ptr) { /* ... */ }. |
Mixing new/delete with C functions |
Using delete on a pointer obtained from malloc or fopen is UB. |
Ensure the deleter matches the allocation method. |
Ignoring constexpr deleter overhead |
Some compilers emit small inline code, but others may incur a function pointer call. | For trivial deleters, use a lambda that can be inlined; otherwise accept the minor overhead. |
5. Performance Considerations
std::shared_ptr stores the control block (reference counts and deleter) in a separate allocation. When the deleter is a type with a non‑empty size, it’s stored inside the control block. This means that if you pass a lambda that captures data, the size of the control block increases, potentially causing more heap traffic.
For high‑performance or low‑memory‑footprint scenarios, consider:
- Using
std::unique_ptrwhen ownership is exclusive; it stores the deleter inline with the pointer. - Preallocating control blocks using
std::make_sharedto reduce fragmentation. - Avoiding unnecessary dynamic allocation by using custom allocators for the control block if you have a specialized memory pool.
6. Example: Managing a Custom Resource
Below is a full example that shows how to wrap a hypothetical C API that allocates and frees a Widget object.
// C API
typedef struct Widget Widget;
Widget* widget_create(int size);
void widget_destroy(Widget* w);
struct WidgetDeleter {
void operator()(Widget* w) const {
if (w) {
widget_destroy(w);
std::printf("Widget destroyed.\n");
}
}
};
int main() {
// Create a shared_ptr that owns a Widget
std::shared_ptr <Widget> widgetPtr(
widget_create(42),
WidgetDeleter()
);
// Use the widget
// widgetPtr->do_something();
// When widgetPtr goes out of scope, widget_destroy is called automatically.
}
7. Summary
- A custom deleter lets
std::shared_ptrmanage any kind of resource, not just raw pointers. - Provide the deleter at construction or via
reset; ensure it matches the allocation method. - Handle null pointers and avoid dangling captures in lambdas.
- For performance‑critical code, consider the control block size and possibly use
unique_ptror custom allocators.
With these guidelines, you can confidently employ std::shared_ptr to manage diverse resources in modern C++ programs.