Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence, Computer Science.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

We sometimes see custom deleters used for C++ smart pointers. Previously, I have not thought too much about why and when we have to use custom deleters, and what the efficient way to include custom deleters is.


In this blog post, I would like to discuss the efficient usages and motivations of custom deleters for C++ std::unique_ptr and std::shared_ptr.

Custom Deleter

Example

// smart_ptr_custom_deleter.cpp
#include <cstdio>
#include <functional>
#include <memory>
#include <iostream>

// Helper function for the custom deleter for C read/write files.
// Remember, C does not have constructor and destructor.
// Custom objects will need custom initialization and free functions,
// especially when memory is allocated on heap.
// In modern C++, we read/write files using fstream
// https://en.cppreference.com/w/cpp/header/fstream
// std::fstream has a destructor and there is no need to use custom deleter.
void close_file(std::FILE* fp) { std::fclose(fp); }

// Helper functor for the custom deleter for C read/write files.
struct FileCloser
{
    void operator()(std::FILE* fp) const { std::fclose(fp); }
};

// A C++ class.
struct Foo
{
    Foo() {}
    ~Foo() {}
};

// The customer deleter is often not meaningful for C++ objects
// as C++ objects has their own destructors.
struct FooDeleter
{
    void operator()(Foo* p) const { delete p; }
};

int main()
{

    FileCloser fcloser;
    std::FILE* ptr_file = std::fopen("file.txt", "w");

    // std::unique_ptr requires deleter as part of the type.
    // std::unique_ptr can be a zero-overhead abstraction with some optimization
    // compared with raw pointer, provided that the deleter is stateless.
    // Templated class.
    // template<class T, class Deleter> std::unique_ptr
    // // Function pointer
    std::unique_ptr<std::FILE, decltype(&close_file)> unique_ptr_file(
        ptr_file, &close_file);
    // // Functor
    // std::unique_ptr<std::FILE, FileCloser> unique_ptr_file(ptr_file,
    // fcloser);
    // // Lambda expression.
    // std::unique_ptr<std::FILE, std::function<void(std::FILE*)>>
    // unique_ptr_file(ptr_file, [](std::FILE* fp) { std::fclose(fp); });

    if (unique_ptr_file)
    {
        std::fputs("fopen example", unique_ptr_file.get());
    }

    // No need to call std::fclose.

    // Templated constructor.
    // template<class Y, class Deleter> shared_ptr(Y *ptr, Deleter d)
    // The use of customer deleter for Foo is not meaningful here,
    // as Foo already has a destructor.
    std::shared_ptr<Foo> shared_ptr_foo_1(new Foo(), FooDeleter());
    // Constructor does not allow new deleter for an existing shared_ptr.
    // Can't have two deleters for the same object.
    // std::shared_ptr<Foo> shared_ptr_foo_2(shared_ptr_foo_1, FooDeleter());
    std::shared_ptr<Foo> shared_ptr_foo_2(shared_ptr_foo_1);

    std::cout << sizeof(std::FILE*) << std::endl; // 8 Bytes
    std::cout << sizeof(std::unique_ptr<std::FILE>) << std::endl; // 8 Bytes
    std::cout << sizeof(std::unique_ptr<std::FILE, FileCloser>) << std::endl; // 8 Bytes
    std::cout << sizeof(std::unique_ptr<std::FILE, decltype(&close_file)>) << std::endl; // 16 Bytes
    std::cout << sizeof(std::unique_ptr<std::FILE, std::function<void(std::FILE*)>>) << std::endl; // 40 Bytes
    std::cout << sizeof(std::shared_ptr<Foo>) << std::endl; // 16 Bytes

    return 0;
}

Execution and Memory Leak Check

$ g++ smart_ptr_custom_deleter.cpp -o smart_ptr_custom_deleter
$ valgrind --leak-check=full ./smart_ptr_custom_deleter 
==35880== Memcheck, a memory error detector
==35880== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==35880== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==35880== Command: ./smart_ptr_custom_deleter
==35880== 
8
8
8
16
40
16
==35880== 
==35880== HEAP SUMMARY:
==35880==     in use at exit: 0 bytes in 0 blocks
==35880==   total heap usage: 6 allocs, 6 frees, 78,321 bytes allocated
==35880== 
==35880== All heap blocks were freed -- no leaks are possible
==35880== 
==35880== For lists of detected and suppressed errors, rerun with: -s
==35880== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Discussions

When do we need custom deleters for smart pointers?

Usually it is when we need to use the objects from some C libraries for our application. Objects from C library do not have destructor and usually requires specific resource free functions to free the memory allocated on heap. If all the objects are from C++ libraries and have well-defined destructor, we can just create std::unique_ptr and std::shared_ptr without deleters.

Why don’t std::make_unique and std::make_shared accept custom deleters?

std::make_unique and std::make_shared do not accept custom deleters. Their purposes are mostly for replacing the new/delete behaviors for C++ objects. If we ever have to use custom deleters, we use std::unique_ptr and std::shared_ptr instead.

Why std::unique_ptr carries deleter type as its part of type whereas std::shared_ptr does not?

Most likely it is because of the performance. std::shared_ptr always carries control block to track the object sharing status and is thus less efficient. Creating a copy of an additional custom deleter will not likely affect its size and performance significantly. std::unique_ptr, however, is designed for more efficient purposes and tries to be as efficient as the raw pointer. With a stateless deleter and possibly empty base optimization, there is no additional memory required to store the deleter, therefore maintaining the performance of std::unique_ptr. For example, std::FILE*, std::unique_ptr<std::FILE>, and std::unique_ptr<std::FILE, FileCloser> are all 8 Bytes. So using stateless functor deleter for std::unique_ptr is as efficient as the raw pointer.

References