How to avoid the need to specify deleter for std :: shared_ptr each time it is built or reset?

std::unique_ptr has 2 template parameters, the second of which is the used deleter. Because of this, you can easily assign a unique_ptr type to a type that requires user deletion (for example, SDL_Texture ) as follows:

 using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>; 

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

Given this alias, programmers can create and reset SDL_TexturePtr without concern or even know about user deletion:

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

std::shared_ptr , on the other hand, does not have a template parameter that allows you to specify a deleter as part of this 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 thing to do with an alias like:

 using SDL_TextureSharedPtr = shared_ptr<SDL_Texture>; 

But this has several advantages over using shared_ptr<SDL_Texture> explicitly, since the user must know the function of the deleter to use and indicate it every time they are created or reset a SDL_TextureSharedPtr in any case:

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

As you can see from the above example, the user needs to know the correct function to delete SDL_Texture (which is SDL_DestroyTexture() ) and pass a pointer to it each time. In addition to being inconvenient, this creates a small chance that the programmer may introduce an error by specifying the wrong function as a deleter.


I would like to somehow encapsulate the deleter in the type of the most common pointer. Since, as far as I see, there is no way to achieve this, simply using a type alias, I considered 3 options:

  • Create a class, wrapping std::shared_ptr<T> , which duplicates the shared_ptr interface, but allows you to specify the delete functor through its own template parameter. Then this shell will provide a pointer to its child instance operator() when calling the constructor method or reset() its base instance std::shared_ptr<T> from its own constructor or reset() method, respectively. The disadvantage, of course, is that the entire, fairly significant interface std::shared_ptr must be duplicated in this packaging class, which is WET.

  • Create a subclass of std::shared_ptr<T> that allows the deleteter functor to be specified through its own template parameter. This, assuming public inheritance, will help us avoid the need to duplicate the shared_ptr interface, but it will open the bank of our own worms. Although std::shared_ptr not final , it did not seem to be intended for a subclass, since it has a non-virtual destructor (although this is not a problem in this particular case). To make matters worse, the reset() method in shared_ptr not virtual and therefore cannot be overridden - it is only obscured, which opens the door for misuse: with public inheritance, users can pass a link to an instance of our subclass to some API by accepting std::shared_ptr<T>& , the implementation of which can call reset() , completely bypassing our method. With unspoken inheritance, we get the same thing as with option # 1.

In both of the above options, SDL_TextureSharedPtr can be expressed as follows: it is assumed that MySharedPtr<T, Deleter> is our (sub) class:

 using SDL_TextureSharedPtr = MySharedPtr<SDL_Texture, SDL2PtrDeleter>; 
  1. The third option was here, and it included the specialization std::default_delete<T> . This was based on my incorrect assumption that std::shared_ptr<T> uses std::default_delete<T> , for example, unique_ptr if no failure was explicitly provided. This is not the case. Thanks @DieterLücking for pointing this out!

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

Did I skip the simpler way to avoid specifying a delete for std::shared_ptr<T> with every instance of it or reset() ?

If this is not the case, are my arguments correct for the options I listed? Are there other objective reasons to prefer one of these options over the other?

+7
c ++ c ++ 11 unique-ptr shared-ptr c ++ 14
source share
4 answers
 using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>; 

Given this alias, programmers can create and reset SDL_TexturePtr without concern or even know about user deletion:

Well, this is (often fatal) oversimplification. Rather, it means that if the default built debit is suitable for building, then the current value of the debit is suitable for the reset pointer, rather than manually changing it.

You are right about the flaws that you discovered for wrapping or extending shared_ptr , although some might say that it allows you to add new instance methods.
You should minimize communication, but this means that you prefer free features, as you do not need more than the existing public interface for recording them.

If you do not specify a deleter, this will lead to the use of std::default_delete (which, unfortunately, is not), and you need only one deletion file for each type, or the standard expression for deletion will match your use case (which it does not use, it seems) A third option would be a better choice.

So another option: Use the constructor function to abstract the (possibly complex) construct and the user deleter.
This way you can write only once, and liberal use of auto can further reduce your headaches.

+4
source share

You did not include private inheritance as an option with abundant using directives to demonstrate immutable functionality.

This is easier than rewriting shared ptr when using a private copy, but allows you to write a custom reset without the risk of exposure.

Also note that generic ptr has a ctor conversion from unique ptr. If your factory functions create unique ptrs, they can be assigned to shared ptrs, if necessary, specify the correct debiter. Eliminate the original pointers in the code and the reset problem will disappear.

+2
source share

You hung up too much, placing the deleter in the type itself. Instead, focus on shared_ptr examples.

The most effective solution to this problem is proper centralization, where shared_ptr entered for this system. There must be one function that generates them; he is responsible for connecting the corresponding debiter.

Obviously, such a system does not give any guarantees. However, if you just never use shared_ptr::reset (and in fact, there are several reasons for this), and you never build it directly (copy / move is ok, but other constructors are not), then you are safe . If you need to reassign shared_ptr for a new instance, just use operator= ; what is this.

Ultimately, this is no different from the code base, which makes liberal use of make_shared .

+1
source share

You can use veneer :

 // A shared_ptr which will use SDL2PtrDeleter **by default**: class SharedTexure : public std::shared_ptr<SDL_Texture> { public: constexpr SharedTexure() : std::shared_ptr<SDL_Texture>() {} constexpr SharedTexure(std::nullptr_t) : std::shared_ptr<SDL_Texture>() {} explicit SharedTexure(SDL_Texture* texture) : std::shared_ptr<SDL_Texture>(texture, SDL2PtrDeleter()) {} SharedTexture(std::shared_ptr<SDL_Texture> texture) : std::shared_ptr<SDL_Texture>(std::move(texture)) {} }; 

What can be generalized with:

 template<class T, class D> class shared_ptr : public std::shared_ptr<T> { public: constexpr shared_ptr() : std::shared_ptr<T>() {} constexpr shared_ptr(std::nullptr_t) : std::shared_ptr<T>() {} template<class U> explicit shared_ptr(U* ptr) : std::shared_ptr<T>(ptr, D()) {} template<class U> shared_ptr(std::shared_ptr<U> ptr) : std::shared_ptr<T>(std::move(ptr)) {} }; using SharedTexure = shared_ptr<SDL_Texture, SDL2PtrDeleter>; 

You must be able to inherit the constructors:

 template<class T, class D> class shared_ptr : public std::shared_ptr<T> { public: using std::shared_ptr<T>::shared_ptr; template<class U> explicit shared_ptr(U* ptr) : std::shared_ptr<T>(ptr, D()) {} }; 

it did not seem to be intended for a subclass, since it has a non-virtual destructor

the link implies that it is safe for this use case. However, it would be interesting to have suitable references from the standard.

+1
source share

All Articles