C++ Custom Deleters: unique_ptr vs shared_ptr

C++11 gives us two useful indispensable smart pointers, std::unique_ptr and std::shared_ptr. So much has been written about these that there’s no point me re-hashing anything other than to re-iterate that if you are using “naked” owning pointers in your code these days, you are simply doing it wrong.

I wanted to mention briefly the use of custom deleters with smart pointers. If you haven’t looked at this aspect of smart pointers before, it basically gives us a way to specify what should happen when the smart pointer goes out of scope.

By default, when your smart pointer goes out of scope, delete (or delete[], as appropriate) is called on the resource, ensuring that it is cleaned up. Having the ability to specify how the resource is deallocated means we can use these smart pointers for other tasks, e.g. emulating C#’s “using” keyword. Another example might be for using a smart pointer for storing a FILE* pointer from fopen() and calling fclose() from the custom deleter.

A further example: I’ve recently been doing some work with the SDL library. There’s a library function IMG_Load() which takes an image filename as a parameter. It loads the image, and returns a pointer to an SDL_Surface (which represents a bitmap). To free the memory pointed to by the pointer, one needs to call SDL_FreeSurface() with the SDL_Surface pointer as its parameter. One can neither directly delete nor free an SDL_Surface pointer because it’s a pointer to a struct, which presumably contains a pointer to some allocated memory within it. So how to use RAII with this construct? Enter custom deleters – we can specifically tell our smart pointer to call SDL_FreeSurface when our smart pointer goes out of scope.

I don’t specifically want to get into SDL specifics any more than I have, so here’s a completely self-contained program to replicate what I’ve been talking about. Note the class “OldThing” which pretends to be an old, non-smart resource-allocating API call.

This code also shows the difference between using custom deleters with unique_ptr and shared_ptr which was the original intention of this post. I’ll explain more about the difference after the listing.

#include <iostream>
#include <memory>

class OldThing {
  public:
    ~OldThing() 
    {
      if (allocated_) {
        std::cout << "MEMORY LEAK!" << std::endl;
      }
    }
    void Deallocate()
    {
      std::cout << "Deallocate called." << std::endl;
      allocated_ = false;
    }
  private:
    bool allocated_{ true };
};

int main() {
  {
    OldThing oldThing;
    // oldThing going out of scope causes a "memory leak"
  }

  {
    // so let's use a smart pointer with a custom "deleter":
    std::shared_ptr<OldThing> p(new OldThing, 
      [] (OldThing* ot) {
        ot->Deallocate();
        delete ot;
      }
    );
  }

  {
    // So that works OK. But what if we want
    // to use unique_ptr instead?
        
    // 1. Don't try this unless you like 
    // seeing loads of template errors:
    /*
    std::unique_ptr<OldThing> p(new OldThing, 
      [] (OldThing* ot) {
        ot->Deallocate();
        delete ot;
      }
    );
    */

    // 2. So we need to include the   
    // deleter's type in the template parameters:
    std::unique_ptr<OldThing, void(*)(OldThing*)> p(new OldThing, 
      [] (OldThing* ot) {
         ot->Deallocate();
         delete ot;
      }
    );
  }

  return 0;
}

So to compile this with g++, use g++ -std=c++11 -o foo foo.cpp (assuming you’ve called the source foo.cpp). If you run ./foo, you should see the following output, each line corresponding to one of the three scope blocks in main():

MEMORY LEAK!
Deallocate called.
Deallocate called.

Essentially we’re seeing a fake “memory leak” when the non-smart OldThing object goes out of scope, then two calls to Deallocate() from the two smart pointers. No real memory allocation going on, just calls to the “deallocation” method.

Note the difference between specifying a deleter for a shared_ptr and a unique_ptr. This is the reason for this post… initially I (without really thinking it through) had some shared_ptrs with custom deleters and figured they should really be unique_ptrs, so I basically had code like the commented out section marked “1”. This generates a spew of template errors and it can be difficult to figure out what’s going wrong (try it and see).

But to make sense of why we need to specify the deleter differently for a unique_ptr, let’s think about how the two smart pointers are implemented. A shared_ptr actually sets up a structure in memory which allows for reference counting, weak pointers, etc. It’s only when reference counts drop to zero can the resource be deallocated. Its constructor takes a function object as its second parameter to allow us to specify a deleter.

If we don’t need to share this smart pointer, using a shared_ptr is an overhead we don’t need. A unique_ptr is a simpler templated class. If you don’t specify the deleter’s function signature as the second template parameter, the default_deleter is used. So as you can see in the section marked “2” we provide the correct template parameter, a function pointer to a void function that takes an OldThing pointer, which makes it work as expected.

Edit: I reviewed this post recently to remind myself about one aspect and realise the way I declare the unique_ptr with a deleter can be simplified to avoid function pointer declarations (which were never very self-explanatory). It just consists of creating the lambda first, then simply using decltype to add the lambda as the template parameter:

auto dt = []( Foo *f ) {
  delete f;
  std::cout << "Deleter called\n";
  };
std::unique_ptr<Foo, decltype(dt)> p( new Foo, dt );

4 Replies to “C++ Custom Deleters: unique_ptr vs shared_ptr”

  1. Sorry, this isn’t related to your post, but I couldn’t make the contact page work.

    Until recently, I used your crossword helper pages to track down phrases. It seems to have disappeared and I was wondering if you had any plans to bring it back. It’s really useful.

    Thanks,
    Alastair

  2. Hi Alastair, sorry for the delay in response…! I didn’t think anyone really used that page, but to be honest I used to find it really useful (which is why I wrote it!) so I may well resurrect it. Trouble is I’m so busy at the moment!! Watch this space.

  3. Thanks for the explanation.
    There are not a lot of good documentation on this very specific topic, but your post let me understand how to resolve my problem.

Leave a Reply

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