Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

Custom Deleters for C++ Smart Pointers

$
0
0

Originally posted at Bartek's Code and Graphics blog.


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.


Where 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:


  1. std::function
  2. Function pointer
  3. Stateless functor
  4. State-full functor
  5. 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.


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



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


Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>