Search code examples
c++c++11c++14shared-ptrunique-ptr

How to avoid the need to specify deleter for std::shared_ptr every time it's constructed or reset?


std::unique_ptr has 2 template parameters, the second of which is the deleter to be used. Thanks to this fact, one can easily alias a unique_ptr to a type, which requires a custom deleter (e.g. SDL_Texture), in the following manner:

using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;

...where SDL2PtrDeleter is a functor to be used as deleter.

Given this alias, programmers are able to construct and reset SDL_TexturePtr without caring or even knowing about custom deleter:

SDL_TexturePtr ptexture(SDL_CreateTexture(/*args*/));

//...

ptexture.reset(SDL_CreateTexture(/*args*/));

std::shared_ptr, on the other hand, doesn't have a template parameter, which would allow specifying the deleter as part of the type, so the following is illegal:

// error: wrong number of template arguments (2, should be 1)
using SDL_TextureSharedPtr = shared_ptr<SDL_Texture, SDL2PtrDeleter>;

So, the best one can do with a type alias is:

using SDL_TextureSharedPtr = shared_ptr<SDL_Texture>;

But this has few advantages over using shared_ptr<SDL_Texture> explicitly, since the user must know the deleter function to use and specify it each time they construct or reset an SDL_TextureSharedPtr anyway:

SDL_TextureSharedPtr ptexture(SDL_CreateTexture(/*args*/), SDL_DestroyTexture);

//...

ptexture.reset(SDL_CreateTexture(/*args*/), SDL_DestroyTexture);

As you can see from the example above, the user needs to know the correct function to delete SDL_Texture (which is SDL_DestroyTexture()) and pass a pointer to it every time. Besides being inconvenient, this creates a minor probability that a programmer might introduce a bug by specifying an incorrect function as a deleter.


I would like to somehow encapsulate the deleter in the type of shared pointer itself. Since there is no way, as far as I can see, to achieve this just by using a type alias, I have considered 3 options:

  1. Create a class, wrapping std::shared_ptr<T>, which would duplicate the interface of shared_ptr but allow specifying a deleter functor via its own template parameter. This wrapper would then supply a pointer to its deleter instance's operator() when invoking constructor or reset() method of its underlying std::shared_ptr<T> instance from its own constructor or reset() method, respectively. The downside, of course, is that the entire, quite sizeable, interface of std::shared_ptr would have to be duplicated in this wrapping class, which is WET.

  2. Create a subclass of std::shared_ptr<T>, which would allow specifying a deleter functor via its own template parameter. This would, assuming public inheritance, help us avoid the need to duplicate shared_ptr's interface, but would open a can of worms of its own. Even though std::shared_ptr is not final, it doesn't seem to have been designed to be subclassed, since it has a non-virtual destructor (though this is not a problem in this particular case). What's worse, reset() method in shared_ptr is not virtual, and so can't be overridden - only shadowed, which opens the door for incorrect usage: with public inheritance, users might pass a reference to an instance of our subclass to some API, accepting std::shared_ptr<T>&, whose implementation might invoke reset(), circumventing our method entirely. With non-public inheritance we get the same as with option #1.

For both of the above options, in the end, SDL_TextureSharedPtr could be expressed as following, assuming MySharedPtr<T, Deleter> is our (sub)class:

using SDL_TextureSharedPtr = MySharedPtr<SDL_Texture, SDL2PtrDeleter>;
  1. The third option used to be here and it involved specializing std::default_delete<T>. It was based on my incorrect assumption that std::shared_ptr<T> uses std::default_delete<T>, like unique_ptr does, if no deleter has been provided explicitly. This is not the case. Thanks to @DieterLücking for pointing this out!

Given these options and the reasoning above, here is my question.

Have I missed a simpler way to avoid having to specify a deleter for std::shared_ptr<T> each time its instance is constructed or reset()?

If not, is my reasoning correct for the options I listed? Are there other objective reasons to prefer one of these options over another?


Solution

  • using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;
    

    Given this alias, programmers are able to construct and reset SDL_TexturePtr without caring or even knowing about custom deleter:

    Well, that's an (often fatal) over-simplification. It's rather that iff the default-constructed deleter is suitable for construction, respectively the current value of the deleter is suitable for the reset pointer, than it need not be manually changed.

    You are right about the disadvantages you found for wrapping or extending shared_ptr, though some may say it allows you to add new instance-methods.
    You should minimize coupling though, which means prefering free functions, as you don't need more than the existing public interface to write them.

    If not specifying a deleter would result in using std::default_delete (which it unfortunately doesn't) and you only need one deleter per type, or the standard delete-expression would fit your use-case (which it doesn't seem to), the third option would be best you could choose.

    Thus, a different option: Use a constructor-function to abstract away the (possibly complex) construction and custom deleter.
    This way you can only write it once, and liberal use of auto can further reduce your headaches.