如何避免在每次构造或重置std :: shared_ptr时指定删除器?

时间:2016-06-19 13:29:09

标签: c++ c++11 c++14 shared-ptr unique-ptr

std::unique_ptr有2个模板参数,第二个是要使用的删除器。由于这个事实,可以通过以下方式轻松地将unique_ptr替换为类型,这需要自定义删除(例如SDL_Texture):

using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;

...其中SDL2PtrDeleter是一个用作删除器的仿函数。

鉴于这个别名,程序员能够构建和重置SDL_TexturePtr而无需关心或甚至不知道自定义删除器:

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

//...

ptexture.reset(SDL_CreateTexture(/*args*/));
另一方面,

std::shared_ptr没有模板参数,这允许将删除器指定为类型的一部分,因此以下是非法的:

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

因此,对类型别名最好的是:

using SDL_TextureSharedPtr = shared_ptr<SDL_Texture>;

但与明确使用shared_ptr<SDL_Texture>相比,这几乎没有什么优势,因为用户必须知道要使用的删除函数,并在每次构造或重置SDL_TextureSharedPtr时指定它:

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

//...

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

从上面的示例中可以看出,用户需要知道删除SDL_TextureSDL_DestroyTexture())的正确函数,并且每次都要传递指针。除了不方便之外,程序员可能会通过将不正确的函数指定为删除程序来引入错误。

我想以某种方式将删除器封装在共享指针本身的类型中。由于我无法通过使用类型别名来实现这一点,因此我考虑了3个选项:

  1. 创建一个包装std::shared_ptr<T>的类,它将复制shared_ptr的接口,但允许通过自己的模板参数指定删除器仿函数。然后,当从其自己的构造函数或operator()方法调用其底层reset()实例的构造函数或std::shared_ptr<T>方法时,此包装器将提供指向其删除器实例reset()的指针。 , 分别。当然,缺点是std::shared_ptr的整个,相当大的接口必须在这个包装类中重复,这是WET。

  2. 创建std::shared_ptr<T>的子类,允许通过自己的模板参数指定删除函子。假设public继承,这将帮助我们避免复制shared_ptr接口的需要,但会打开一堆自己的蠕虫。即使std::shared_ptr不是final,它似乎也没有被设计为子类,因为它具有非虚拟析构函数(尽管在这种特殊情况下这不是问题) 。更糟糕的是,reset()中的shared_ptr方法不是虚拟的,因此无法覆盖 - 只会被遮蔽,从而为错误使用打开了大门:使用public继承,用户可能会将对我们子类的实例的引用传递给某个API,接受std::shared_ptr<T>&,其实现可能会调用reset(),完全绕过我们的方法。对于非公共继承,我们与选项#1相同。

  3. 对于上述两个选项,最后,SDL_TextureSharedPtr可以表示如下,假设MySharedPtr<T, Deleter>是我们的(子)类:

    using SDL_TextureSharedPtr = MySharedPtr<SDL_Texture, SDL2PtrDeleter>;
    
    1. 第三个​​选项曾经在这里,它涉及专门化std::default_delete<T>。这是基于我的错误假设std::shared_ptr<T>使用std::default_delete<T>,如unique_ptr那样,如果没有明确提供删除器。这是的情况。感谢@DieterLücking指出这一点!
    2. 鉴于以上这些选项和推理,这是我的问题。

      我是否错过了一种更简单的方法,可以避免每次构建实例时std::shared_ptr<T>都指定删除器reset()

      如果没有,我的推理是否正确我列出的选项?还有其他客观理由偏好这些选项中的一种而不是另一种吗?

4 个答案:

答案 0 :(得分:4)

using SDL_TexturePtr = unique_ptr<SDL_Texture, SDL2PtrDeleter>;
     

鉴于这个别名,程序员能够构建和重置SDL_TexturePtr而无需关心或甚至不知道自定义删除器:

嗯,这是一种(通常是致命的)过度简化。相反,iff默认构造的删除器适合于构造,删除器的当前值分别适合于重置指针,而不需要手动更改。

您对包装或扩展shared_ptr所发现的缺点是正确的,尽管有些人可能会说它允许您添加新的实例方法。
你应该尽量减少耦合,这意味着更喜欢自由功能,因为你不需要比现有的公共接口更多来编写它们。

如果没有指定删除器会导致使用std::default_delete(遗憾的是它不会)并且每种类型只需要一个删除器,或者标准的delete-expression适合您的用例(它似乎没有),第三种选择是你可以选择的最佳选择。

因此,一个不同的选择: 使用构造函数来抽象出(可能很复杂的)构造和自定义删除器 这样你只能写一次,自由使用auto可以进一步减轻你的头痛。

答案 1 :(得分:2)

作为一个选项,您没有包含大量using指令的私有继承,以显示未更改的功能。

它比使用私有副本重写共享ptr更简单,但允许你编写一个没有暴露危险的自定义reset

另请注意,共享ptr具有来自唯一ptr的转换ctor。如果您的工厂函数创建了唯一的ptrs,则可以根据需要将它们分配给共享的ptrs,并且使用正确的删除器。消除代码中的原始指针,重置问题就消失了。

答案 2 :(得分:2)

您应该可以使用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)) {}
};

可以概括为:

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>;

你应该能够继承构造函数:

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()) {}
};
  

它似乎没有设计为子类,   因为它有一个非虚拟析构函数

referenced paper表示此用例是安全的。然而,从标准中获得合适的参考资料会很有趣。

答案 3 :(得分:1)

你太过于把删除器放在类型本身。而是关注shared_ptr 实例的来源。

解决此问题的最有效方法是正确地集中引入此系统shared_ptr的位置。应该有一个生成它们的功能;它负责附加适当的删除者。

显然,这样的系统不能保证。但是,如果你从不使用shared_ptr::reset(实际上,没有理由这样做)并且你从不直接构造一个(复制/移动很好,但其他构造函数不是),那么你就是安全。如果您需要将shared_ptr重新分配给新实例,请使用operator=;这就是它的用途。

最终,这与自由使用make_shared

的代码库没什么区别