C++ unique/shared ptr custom deleters

I’ve recently found the need for a custom deleter function attached to smart pointers in C++ and I found there are a number of different ways to implement this.

I’ll touch on the advantages/disadvantages of the different ways to use custom deleters and the difference between unique and shared ptr.

std::unique_ptr:

The type of the deleter is defined as part of the unique_ptr and you define it as the second template parameter, then when constructing the type, you must pass through the type specified as a parameter.

Instead of std::unique_ptr calling ‘delete’ and invoking the destructor on the internally stored pointer, it will instead pass the pointer to the function provided.

The deleter must by a type which implements a operator() which takes a T* as a parameter.

So in the example below, we specify that the deleter is std::function<void(MyClass*)>. Then on construction, we pass through the function we want to call.

class MyClass
{
public:
    ~MyClass() { }
};

using DefaultPtr = std::unique_ptr<MyClass>;
DefaultPtr pDefault; //Will call MyClass::~MyClass()

using CustomDeletePtr = std::unique_ptr<MyClass, std::function<void(MyClass*)>>;
CustomDeletePtr pCustom; //Will call function passed through on construction

static void CustomDeleter(MyClass* pMyClass)
{
    //This will be called instead of delete and invoking the constructor.
}

void InitPointers()
{
    pDefault = DefaultPtr(new MyClass());
    pCustom = CustomDeletePtr(new MyClass(), &CustomDeleter);
}

std::unique_ptr – cheapest implementations:

The custom deleter type you specify will affect the size of your std::unique_ptr. The sizes below are from C++ VS2019 64bit.

  • Default: std::unique_ptr<MyClass> (no custom deleter) – 8 bytes.
  • 1: std::unique_ptr<MyClass, CustomDeleteClass> – 8 bytes.
  • 2 std::unique_ptr<MyClass, decltype(lambda)> – 8 bytes.
  • 2: std::unique_ptr<MyClass, void(*)(MyClass*)> – 16 bytes.
  • 3: std::unique_ptr<MyClass, std::function<void(MyClass*)> – 72 bytes.

Lets talk about the sizes above. std::unique_ptr by default is a really simple wrapper for a pointer and it only has to store the 8 byte pointer.

Adding custom deleters can increase the size of std::unique_ptr because it has to store the type specified. std::function really bloats out the size of the std::unique_ptr because of the flexibility and features it provides.

Providing a C Style function as the deleter void(*)(MyClass*) increases the size to 16 bytes. This is because it needs to store a pointer to the function specified. Its increase the size of the function but offers good flexibility because you can provide any function you want when constructing.

Using a custom deleter class with an operator() or a lambda incurs no additional storage overhead because the custom deleter function is always the same for all objects created and the DeleterClass contains no member variables.

class DeleterClass
{
public:
    void operator()(MyClass* pMyClass)
    {
    }
};

using CustomDeleteOperatorPtr = std::unique_ptr<MyClass, DeleterClass>;
CustomDeleteOperatorPtr MyCustomPtr(new MyClass());

auto lambda = [](MyClass* p) { delete p; };
std::unique_ptr<MyClass, decltype(lambda)> PointerWithLambda(new MyClass(), lambda);

One bonus when using the operator() over a lambda is that you don’t even have to specify it when constructing new instances of the object as you can see above, assuming the class can be default constructed.

std::shared_ptr

std::shared_ptr requires some different syntax compared to std::unique_ptr. Behind the scenes, std::shared_ptr allocates a ‘control block’ containing the atomic reference counter and other data such as the custom deleter. The image below from Oreilly.com shows it nicely:

When using a custom deleter with shared_ptr we don’t pass any additional template arguments, our custom deleter is passed when creating the object and it exists within the control block.

class MyClass
{
public:
    ~MyClass() { }
};

static void CustomDeleter(MyClass* pMyClass)
{
    delete pMyClass;
}

int main()
{
    //1 - Function
    std::shared_ptr<MyClass> s1(new MyClass(), CustomDeleter);
    
    //2 - Lambda
    std::shared_ptr<MyClass> s2(new MyClass(), [](MyClass* pMyClass) { delete pMyClass; });

    //3 - Custom Class
    class DeleterClass
    {
    public:
        DeleterClass() 
        { 
        
        }
        void operator()(MyClass* pMyClass)
        {
            delete pMyClass;
        }
    };
    DeleterClass d; //Must be CopyConstructible. 
    std::shared_ptr<MyClass> s3(new MyClass(), d);
}

The size of the shared_ptr remains 16 bytes on a 64 bit compiler. One pointer to the object and another pointer to the control block. Depending on the implementation, the size of the control block could change if a custom deleter is or isn’t allocated.

On MSVC 2019, using std::shared_ptr by default, the control block size is 24 bytes. The size of the control block can increase depending on the type of deleter used. Using a lambda or class with an operator() incurs no additonal storage overhead. Using a function pointer will increment the size by the cost of a pointer and using more complex types such as std::function will increase storage further, as it does with std::unique_ptr.

Note: Unfortunately you cannot use a custom deleter with std::make_shared (nor with std::make_unique). Something else interesting to know about std::shared_ptr is that it’s more optimal to use std::make_shared() rather than std::shared_ptr<T>(new T()) because internally the implementation needs to allocate a control block so if you provide your own pointer, it has to call ‘new’ to allocate its own control block. If you called make_shared(), it will allocate the memory for your data and the control block in the same allocation!

Closing thoughts

When wanting to use a custom deleter with a std::unique_ptr and you want it to be the same function for all objects, use a custom class with operator() as this incurs the least overhead, can be inlined at runtime and doesn’t increase the size of unique_ptr.

If you’re wanting to call a different function for each of your unique_ptrs, opt for a straight up function pointer/ lambda. The only additional overhead is the cost of storing the function pointer.

For std::shared_ptr, the storage overhead of the deleter is stored in the control block only once instead of for every object like with std::unique_ptr, so it’s cheaper to use a complex delete type with larger storage requirements.

5 responses to “C++ unique/shared ptr custom deleters”

  1. Looking for a fresh online gaming experience? Check out g9777mx they seem to have a nice range of games. I am going to try it out this weekend. You can find them here: g9777mx

  2. Bets74 caught my eye recently. They seem to have a decent platform and a wide variety to bet on. Don’t take my word for it, see for yourself bets74!

  3. Yo, casinooxxo surprised me, actually! The gameplay is surprisingly good, and they have a decent variety of games. I’m not complaining! Well worth your time! Take a look at casinooxxo

  4. I would like to show my appreciation for your kind-heartedness giving support to folks that need guidance on this particular question. Your real dedication to getting the message around appears to be exceptionally significant and have surely made ladies just like me to realize their ambitions. Your personal informative guide entails a whole lot to me and far more to my fellow workers. Regards; from everyone of us. Walter Linko

Leave a Reply

Your email address will not be published. Required fields are marked *