'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 1:[1]

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.

Solution 2:[2]

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.

Solution 3:[3]

You should be able to use a 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)) {}
};

Which 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 should be able to inherit 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 doesn't seem to have been designed to be subclassed, since it has a non-virtual destructor

The referenced paper implies that it is safe for this use case. It'd be interesting to have suitable references from the standard, however.

Solution 4:[4]

You are too hung up on putting the deleter in the type itself. Focus instead on where shared_ptr instances come from.

The most effective solution to this problem is to properly centralize where shared_ptrs for this system are getting introduced. There should be a single function which generates them; it is responsible for attaching the appropriate deleter.

Obviously such a system offers no guarantees. However, if you simply never use shared_ptr::reset (and really, there are few reasons to do so) and you never directly construct one (copying/moving is fine, but other constructors are not), then you're safe. If you need to reassign a shared_ptr to a new instance, just use operator=; that's what it's for.

Ultimately, this is no different from a codebase that makes liberal use of make_shared.

Solution 5:[5]

Just move a unique_ptr into a shared_ptr. The shared_ptr will inherit the custom deleter. See code (sorry was experimenting with cJSON lib but you'll get the point):

#include <memory>
#include <iostream>

struct cJSON { } obj;

extern "C" cJSON* cJSON_ParseWithLength() { return &obj; }

void cJSON_Delete(cJSON* item)
{
    std::cout << "custom deleter used" << std::endl;
}

auto factory()
{
    return std::unique_ptr<cJSON, decltype(&cJSON_Delete)>{ cJSON_ParseWithLength(), cJSON_Delete };
}

int main()
{
    auto uptr = factory();
    std::shared_ptr sptr{std::move(uptr)};

    sptr.reset();

    std::cout << "now we're done" << std::endl;
}

Output:

custom deleter used
now we're done

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4
Solution 5 glades