Using custom deleter with smarshared_ptr and unique_ptr in C++

Pranay Kumar
pranayaggarwal25
Published in
6 min readMay 29, 2020

How to use a custom deleter with an unique_ptr and shared_ptr

Related post: https://medium.com/pranayaggarwal25/a-tale-of-two-allocations-f61aa0bf71fc

Table of Contents

  1. Introduction
  2. The true unknown face of smart pointers
  3. What is std::default_delete indeed?
  4. Ways to specify custom deleters
  5. Using custom deleter with shared_ptr
  6. Using custom deleter with unique_ptr
  7. Storage of custom deleters
  8. Restrictions that come with custom deleters

Introduction

Why and when would we need something like that?

Case 1: In order to fully delete an object sometimes, we need to do some additional action. What if performing “delete” (that smart pointers do automatically)is not the only thing which needs to be done before fully destroying the owned object.

Case 2: We can’t bind a shared_ptr or unique_ptr to a stack-allocated object, because calling delete on it would cause undefined behaviour.

Case 3: Mix of programming languages code, such as C++ with Obj-C++. As objective-c may need a complex release mechanism for its data types such as calling CFRelease, we would be in need of a custom deleter.

Case 4: In C where, when you wrap FILE*, or some kind of a C style structure free(), custom deleters may be useful.

and a few other cases.

The true unknown face of smart pointers

std::unique_ptr

The complete type of std::unique_ptr has a second template parameter, its deleter that has a default type std::default_delete<T> .
What is that?? No need to worry, We’ll cover this together :)

template< class T, class Deleter = std::default_delete<T> > 
class unique_ptr;
// Manages a single object
template < class T, class Deleter>
class unique_ptr<T[], Deleter>;
// Manages a dynamically-allocated array of objects

std::default_delete<T> is a function object (a.k.a functor) that calls delete on the object when invoked. This is only the default type for invoking Deleter and it can be replaced with a custom deleter.

The invocation is done using operator() on the Deleter.

std::shared_ptr

You can pass any callable thing (lambda, functor) as deleter while constructing a shared pointer in the constructor as an additional argument.

template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d );
// One of the overloads of shared_ptr construction

thus specifying custom deleter with std::shared_ptr is comparatively easy.

On ref count reaches zero, the shared_ptr uses the delete-expression i.e. delete ptr .

Also since C++17 —

// shared_ptr can be used to manage a dynamically allocated array
// since C++17 by specifying template argument with T[N] or T[]. So // you may write
shared_ptr<int[]> myShared(new int[10]);

What is std::default_delete indeed?

This is defined in <memory> header.

template< class T > struct default_delete;template< class T > struct default_delete<T[]>;
  1. The non-specialized default_delete uses delete to deallocate memory for a single object.
  2. A partial specialization for array types that uses delete[] is also provided.

Members:

  1. Constructor — can be default or templated.
constexpr default_delete() noexcept = default; // defaulttemplate <class U>
default_delete( const default_delete<U>& d ) noexcept;
// templated
// Constructs a std::default_delete object from another.
// Overload resolution if
U* is implicitly convertible to T*.

2. operator() — overload for the operator() is needed for the callability of struct/class as its a function object( or functor).
At the point in the code where, this operator() is called, the type must be complete and defined.

Examples:

Example 1:
{
std::unique_ptr<int> ptr(new int(5));
}
// unique_ptr<int> uses default_delete<int>
====================================================================Example 2:
{
std::unique_ptr<int[]> ptr(new int[10]);
}
// unique_ptr<int[]> uses default_delete<int[]>
====================================================================Example 3:
// default_delete can be used anywhere a delete functor is needed
std::vector<int*> v;
for(int n = 0; n < 100; ++n)
v.push_back(new int(n));
std::for_each(v.begin(), v.end(), std::default_delete<int>());
// Constructing the function object to be called
====================================================================Example 4:{
std::shared_ptr<int> shared_bad(new int[10]);
}
// the destructor calls delete, undefined behavior as it's an array

{
std::shared_ptr<int> shared_good(new int[10], std::default_delete<int[]> ());
} // the destructor calls delete[], ok
====================================================================
Example 5: (Valid only C++17 onwards)
{
shared_ptr<int[]> shared_best(new int[10]);
}
// the destructor calls delete[], awesome!!
====================================================================

Ways to specify custom deleters

  1. std::function — Heavy size contribution ( ~32 bytes! on x64)
  2. Function pointer — Just a pointer
  3. Stateless functor / Stateless Lambda — None.
  4. Stateful functor / Stateful Lambda — sizeof(functor or lambda)

Using custom deleter with shared_ptr

Examples —

1. Use a proper functor —

(Requires custom deleter for array only Prior to C++17)

// declare the function objecttemplate< typename T >
struct array_deleter
{
void operator ()( T const * p)
{
delete[] p;
}
};
// and use shared_ptr as follows by constructing function object
std::shared_ptr<int> sp(new int[10], array_deleter<int>());

2. Use a plain lambda

std::shared_ptr<MyType> sp(new int[10], [](int *p) { delete[] p; });

3. Use default_delete (Only valid for array types before C++17)

std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());

Note: delete ptr is same as specifying default_delete<T>{}ptr .

Using custom deleter with unique_ptr

With unique_ptr there is a bit more complication. The main thing is that a deleter type will be part of unique_ptr type.

By default we get std::default_delete so here are some examples —

For a class MyType

class MyType {  // ...
// ...
};void deleter(MyType*) {
// ...
}
====================================================================
// 1. std::function
std::unique_ptr<MyType, std::function<void (MyType*)>> u1(new MyType(), deleter);
OR std::unique_ptr<MyType, decltype(&deleter)> u1(new MyType(), deleter); // 2nd argument in unique_ptr object construction is required.
It is optional ONLY in case a custom deleter is already wrapped as functor object (Point #3 below), as function objects are created by default in those cases.
// As a good practice it's always useful to pass second argument here.
====================================================================// 2. Function pointer
std::unique_ptr<MyType, void (*)(MyType *)> u2(new MyType(), deleter);
====================================================================// A stateless functor
struct MyTypeDeleterFunctor {
void operator()(MyType* p) {
// ...
}
};
// 3. Stateless functor
std::unique_ptr<MyType, MyTypeDeleterFunctor>u3(new MyType());
// deleter type is not a reference hence gets copied
MyTypeDeleterFunctor functor;
std::unique_ptr<MyType, MyTypeDeleterFunctor&>u3(new MyType(), functor );
// deleter type is a reference hence gets copied====================================================================// 4. The lambda wayauto deleter = [](MyType*){ ... }std::unique_ptr<MyType, void (*)(MyType *)>> u1(new MyType(), deleter);
// OR
std::unique_ptr<MyType, decltype(deleter)>> u1(new MyType(), deleter);
// Mind the decltype(deleter) here, without a & since deleter isn't a function here, rather a lambda

Storage of custom deleters

For shared_ptr
When you use a custom deleter it won’t affect the size of your shared_ptr type. If you remember, shared_ptr size should be roughly 2 x sizeof(ptr) so where does this deleter hide?

As we know, shared_ptr consists of two things: pointer to the object and pointer to the control block (that contains ref count). Inside the control block structure of shared_ptr, there is a space for custom deleter and allocator.

For unique_ptr
unique_ptr is small and efficient; the size is one pointer so where is the custom allocator hide in this case?

The deleter is part of the type of unique_ptr. And since the functor/lambda that is stateless, its type fully encodes everything there is to know about this without any size involvement. Using function pointer takes one pointer size and std::function takes even more size.

The shared_ptr always stores a deleter, this erases the type of the deleter, which might be useful in APIs. The disadvantages of using shared_ptr over unique_ptr include a higher memory cost for storing the deleter and a performance cost for maintaining the reference count.

Trivia: The size of weak_ptr is the same as that of shared_ptr. Weak pointer points to the same control block as it’s shared pointer. When a weak_ptr is created, destroyed, or copied a second reference count (weak pointer reference count) is manipulated. Weak count is connected with object storage deallocation (Refer prerequisite talk)

Restrictions that come with custom deleter

Can’t use make_shared with shared_ptr

Unfortunately, you can pass a custom deleter only in the constructor of shared_ptr there is no way to use make_shared. This might be a bit of disadvantage (Refer prerequisite talk)

One can use allocate_shared and custom allocator and deleter, but that’s too complex to be covered in this article.

Can’t use make_unique with unique_ptr

Similarly as with shared_ptr you can pass a custom deleter only in the constructor of unique_ptr and thus you cannot use make_unique.

Thanks for reading this article! Feel free to leave your comments and let me know what you think. Please feel free to drop any comments to improve this article.
Please check out my other articles and website, Have a great day!

--

--