Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 287
TerraPass Avatar asked Jun 19 '16 13:06

TerraPass


People also ask

Do I have to delete shared_ptr?

The purpose of shared_ptr is to manage an object that no one "person" has the right or responsibility to delete, because there could be others sharing ownership. So you shouldn't ever want to, either.

What happens when shared_ptr goes out of scope?

All the instances point to the same object, and share access to one "control block" that increments and decrements the reference count whenever a new shared_ptr is added, goes out of scope, or is reset. When the reference count reaches zero, the control block deletes the memory resource and itself.

What happens when you move a shared_ptr?

By moving the shared_ptr instead of copying it, we "steal" the atomic reference count and we nullify the other shared_ptr . "stealing" the reference count is not atomic, and it is hundred times faster than copying the shared_ptr (and causing atomic reference increment or decrement).

How do I reset a shared pointer?

std::shared_ptr<T>::reset. Replaces the managed object with an object pointed to by ptr . Optional deleter d can be supplied, which is later used to destroy the new object when no shared_ptr objects own it. By default, delete expression is used as deleter.


2 Answers

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.

like image 154
Deduplicator Avatar answered Sep 22 '22 00:09

Deduplicator


You did not include, as an option, private inheritance with copious using directives to expose unchanged functionality.

It is simpler than rewriting shared ptr while usong a private copy, but lets you write a custom reset with no danger of exposure.

Also note that shared ptr has a converting ctor from unique ptr. If your factory functions create unique ptrs, they can be assigned to shared ptrs if needed,mand the correct deleter is used. Eliminate raw pointers in your code and the problem of reset goes away.

like image 23
Yakk - Adam Nevraumont Avatar answered Sep 21 '22 00:09

Yakk - Adam Nevraumont