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:
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.
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>;
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?
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.
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.
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).
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With