Last Update:
Custom Deleters for C++ Smart Pointers

Table of Contents
Let’s say we have the following code:
LegacyList* pMyList = new LegacyList();
...
pMyList->ReleaseElements();
delete pMyList;
In order to fully delete an object we need to do some additional action.
How to make it more C++11? How to use unique_ptr
or shared_ptr
here?
Intro
We all know that smart pointers are really nice things and we should be
using them instead of raw new
and delete
. But what if deleting a
pointer is not only the thing we need to call before the object is fully
destroyed? In our short example we have to call ReleaseElements()
to
completely clear the list.
Side Note: we could simply redesign LegacyList
so that it properly
clears its data inside its destructor. But for this exercise we need to
assume that LegacyList
cannot be changed (it’s some legacy, hard to
fix code, or it might come from a third party library).
ReleaseElements
is only my invention for this article. Other things
might be involved here instead: logging, closing a file, terminating a
connection, returning object to C style library… or in general: any
resource releasing procedure,
RAII.
To give more context to my example, let’s discuss the following use of
LegacyList
:
class WordCache {
public:
WordCache() { m_pList = nullptr; }
~WordCache() { ClearCache(); }
void UpdateCache(LegacyList *pInputList) {
ClearCache();
m_pList = pInputList;
if (m_pList)
{
// do something with the list...
}
}
private:
void ClearCache() {
if (m_pList) {
m_pList->ReleaseElements();
delete m_pList;
m_pList = nullptr;
}
}
LegacyList *m_pList; // owned by the object
};
You can play with the source code here: using Coliru online compiler.
This is a bit old style C++ class. The class owns the m_pList
pointer,
so it has to be cleared in the constructor. To make life easier there is
ClearCache()
method that is called from the destructor or from
UpdateCache()
.
The main method UpdateCache()
takes pointer to a list and gets
ownership of that pointer. The pointer is deleted in the destructor or
when we update the cache again.
Simplified usage:
WordCache myTestClass;
LegacyList* pList = new LegacyList();
// fill the list...
myTestClass.UpdateCache(pList);
LegacyList* pList2 = new LegacyList();
// fill the list again
// pList should be deleted, pList2 is now owned
myTestClass.UpdateCache(pList2);
With the above code there shouldn’t be any memory leaks, but we need to
carefully pay attention what’s going on with the pList
pointer. This
is definitely not modern C++!
Let’s update the code so it’s modernized and properly uses RAII (smart
pointers in these cases). Using unique_ptr
or shared_ptr
seems to be
easy, but here we have a slight complication: how to execute this
additional code that is required to fully delete LegacyList
?
What we need is a Custom Deleter
Custom Deleter for shared_ptr
I’ll start with shared_ptr
because this type of pointer is more
flexible and easier to use.
What should you do to pass a custom deleter? Just pass it when you create a pointer:
std::shared_ptr<int> pIntPtr(new int(10),
[](int *pi) { delete pi; }); // deleter
The above code is quite trivial and mostly redundant. If fact, it’s more
or less a default deleter - because it’s just calling delete
on a
pointer. But basically, you can pass any callable thing (lambda,
functor, function pointer) as deleter while constructing a shared
pointer.
In the case of LegacyList
let’s create a function:
void DeleteLegacyList(LegacyList* p) {
p->ReleaseElements();
delete p;
}
The modernized class is super simple now:
class ModernSharedWordCache {
public:
void UpdateCache(std::shared_ptr<LegacyList> pInputList) {
m_pList = pInputList;
// do something with the list...
}
private:
std::shared_ptr<LegacyList> m_pList;
};
- No need for constructor - the pointer is initialized to
nullptr
by default - No need for destructor - pointer is cleared automatically
- No need for helper
ClearCache
- just reset pointer and all the memory and resources are properly cleared.
When creating the pointer we need to pass that function:
ModernSharedWordCache mySharedClass;
std::shared_ptr<LegacyList> ptr(new LegacyList(),
DeleteLegacyList)
mySharedClass.UpdateCache(ptr);
As you can see there is no need to take care about the pointer, just create it (remember about passing a proper deleter) and that’s all.
Were is custom deleter stored?
When you use a custom deleter it won’t affect the size of your
shared_ptr type. If you remember, that should be roughly
2 x sizeof(ptr)
(8 or 16 bytes)… so where does this deleter hide?
shared_ptr
consists of two things: pointer to the object and pointer
to the control block (that contains reference counter for example).
Control block is created only once per given pointer, so two
shared_pointers (for the same pointer) will point to the same control
block.
Inside control block there is a space for custom deleter and allocator.
Can I use make_shared?
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, because as I described in Why create shared_ptr with
make_shared?
- from my old blog post, make_shared
allocates the object and its
control block for it next to each other in memory. Without make_shared
you get two, probably separate, blocks of allocated mem.
Update: I got a very good comment on reddit: from
quicknir
saying that I am wrong in this point and there is something you can use
instead of make_shared
.
Indeed, you can use
allocate_shared
and leverage both the ability to have custom deleter and being able to
share the same memory block. However, that requires you to write custom
allocator, so I considered it to be too advanced for the original
article.
Custom Deleter for 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
:
template <
class T,
class Deleter = std::default_delete<T>
> class unique_ptr;
Deleter is part of the pointer, heavy deleter (in terms of memory consumption) means larger pointer type.
What to chose as deleter?
What is best to use as a deleter? Let’s consider the following options:
std::function
- Function pointer
- Stateless functor
- State-full functor
- Lambda
What is the smallest size of unique_ptr
with the above deleter types?
Can you guess? (Answer at the end of the article)
How to use?
For our example problem let’s use a functor:
struct LegacyListDeleterFunctor {
void operator()(LegacyList* p) {
p->ReleaseElements();
delete p;
}
};
And here is a usage in the updated class:
class ModernWordCache {
public:
using unique_legacylist_ptr =
std::unique_ptr<LegacyList,
LegacyListDeleterFunctor>;
public:
void UpdateCache(unique_legacylist_ptr pInputList) {
m_pList = std::move(pInputList);
// do something with the list...
}
private:
unique_legacylist_ptr m_pList;
};
Code is a bit more complex than the version with `shared_ptr` - we need to define a proper pointer type. Below I show how to use that new class:
ModernWordCache myModernClass;
ModernWordCache::unique_legacylist_ptr pUniqueList(new LegacyList());
myModernClass.UpdateCache(std::move(pUniqueList));
All we have to remember, since it’s a unique pointer, is to move the pointer rather than copy it.
Can I use make_unique?
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
.
Fortunately, make_unique
is only for convenience (wrong!) and
doesn’t give any performance/memory benefits over normal construction.
Update: I was too confident about make_unique
:) There is always a
purpose for such functions. Look here GotW #89 Solution: Smart
Pointers
- guru question 3:
make_unique
is important because:
First of all:
Guideline: Use make_unique to create an object that isn’t shared (at
least not yet), unless you need a custom deleter or are adopting a raw
pointer from elsewhere.
Secondly:
make_unique
gives exception safety: Exception safety and
make_unique
So, by using a custom deleter we lose a bit of security. It’s worth knowig the risk behind that choice. Still, custom deleter with unique_ptr is far more better than playing with raw pointers.
Sorry for a little interruption in the flow :)
I’ve prepared a little bonus if you’re interested in smart pointers - a
reference card, check it out here:
Download a free copy of my C++ Smart Pointers Ref Card!
Things to remember:
Custom Deleters give a lot of flexibility that improves resource
management in your apps.
Summary
In this post I’ve shown you how to use custom deleters with C++ smart
pointer: shared_ptr
and unique_ptr
. Those deleters can be used in
all the places wher ‘normal’ delete ptr
is not enough: when you wrap
FILE*
, some kind of a C style structure (SDL_FreeSurface
, free()
,
destroy_bitmap
from Allegro library, etc).
Remember that proper garbage collection is not only related to memory
destruction, often some other actions needs to be invoked. With custom
deleters you have that option.
Gist with the code is located here: fenbf/smart_ptr_deleters.cpp
Let me know what are your common problems with smart pointers?
What blocks you from using them?
References
- Item 18, 19, 21 from Effective Modern C++ by Scott Meyers
- The C++ Standard Library, 2nd, by Nicolai M. Josuttis (my review)
- Smart pointer gotchas
- More C++ Idioms/Resource Acquisition Is Initialization
- StackOverflow: C++ std::unique_ptr : Why isn’t there any size fees with lambdas?
- StackOverflow: How to pass deleter to make_shared?
Answer to the question about pointer size
1. std::function
- heavy stuff, on 64 bit, gcc it showed me 40
bytes.
2. Function pointer - it’s just a pointer, so now unique_ptr
contains
two pointers: for the object and for that function… so 2*sizeof(ptr)
=
8 or 16 bytes.
3. Stateless functor (and also stateless lambda) - it’s actually very
tircky thing. You would probably say: two pointers… but it’s not. Thanks
to empty base optimization -
EBO the final size is
just a size of one pointer, so the smallest possible thing.
4. State-full functor - if there is some state inside the functor then
we cannot do any optimizations, so it will be the size of
ptr + sizeof(functor)
5. Lambda (statefull) - similar to statefull functor
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: