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: @Compiler Explorer.
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 useinstead 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.
If you're interested in smart pointers - have a look at my handy reference card. It covers everything you need to know about unique_ptr
, shared_ptr
and weak_ptr
, wrapped in a beautiful PDF:
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
For more practical examples of deleters you can see this post: Wrapping Resource Handles in Smart Pointers.
- 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_pt
: Why isn’t there any size fees with lambdas? - StackOverflow: How to pass deleter to
make_shared
?
Answer to the question about pointer size:
std::function
- heavy stuff, on 64 bit, gcc it showed me 40 bytes.- Function pointer - it’s just a pointer, so now
unique_ptr
contains two pointers: for the object and for that function… so2*sizeof(ptr)
= 8 or 16 bytes. - 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.
- 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)
- Lambda (statefull) - similar to statefull functor