How to Properly Use std::shared_ptr with Custom Deleters

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_ptr when ownership is exclusive; it stores the deleter inline with the pointer.
  • Preallocating control blocks using std::make_shared to 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_ptr manage 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_ptr or custom allocators.

With these guidelines, you can confidently employ std::shared_ptr to manage diverse resources in modern C++ programs.

发表评论