【问题标题】:Typedef a shared_ptr type with a static custom deleter, similar to unique_ptrtypedef 一个带有静态自定义删除器的 shared_ptr 类型,类似于 unique_ptr
【发布时间】:2015-02-04 12:48:34
【问题描述】:

我已经阅读了许多关于shared_ptrunique_ptr 的自定义删除器的 SO 问题,以及两者之间的区别。但是,我仍然没有找到任何明确的答案:

如何最好地使用自定义删除器创建一个充当shared_ptr 的类型,类似于unique_ptr 将删除器作为类型定义的一部分?

对于unique_ptr 的用法,我使用了一个删除器类,它处理单个类型的删除(为简洁起见,将其限制为两种类型):

struct SDL_Deleter {                                
  void operator()( SDL_Surface* ptr ) { if (ptr) SDL_FreeSurface( ptr );} 
  void operator()( SDL_RWops* ptr )   { if (ptr) SDL_RWclose( ptr );} 
};

using SurfacePtr = std::unique_ptr<SDL_Surface, SDL_Deleter>;
using RWopsPtr = std::unique_ptr<SDL_RWops, SDL_Deleter>;

这可以与类似的东西一起使用

SurfacePtr surface(IMG_Load("image.png"));

并且会在销毁时调用SDL_FreeSurface


这一切都很好。但是,如何为shared_ptr 实现相同的目标?它的类型定义为

template< class T > class shared_ptr;

提供自定义删除器的方法是通过构造函数。 shared_ptr 包装器的用户需要知道包装了哪种指针类型以及应该如何删除该指针,这感觉不对。实现与上述unique_ptr 示例相同的用法的最佳方法是什么。

换句话说,我最终会得到:

SurfaceShPtr surface(IMG_Load("image.png"));

而不是类似的东西

SurfaceShPtr surface(IMG_Load("image.png"),
                     [=](SDL_Surface* ptr){SDL_FreeSurface(ptr);});

或者,稍微好一点

SurfaceShPtr surface(IMG_Load("image.png"),
                     SDL_Deleter());

有没有办法做到这一点,而不必创建 RAII 包装类(而不是 typedef),从而增加更多开销?


如果答案是“这不可能”。为什么不呢?

【问题讨论】:

  • search on this site 产生 nothing 有用吗?
  • @WhozCraig 是的。我做到了。如果您发现我遗漏的内容,可以将其标记为重复。
  • @JonathanWakely 如果您想告诉我如何操作,我将不胜感激。
  • unique_ptr&lt;SDL_Surface, decltype(&amp;SDL_FreeSurface)&gt; p(IMG_load("image.png", &amp;SDL_FreeSurface);
  • 或查看我的答案的更新。我决定对所有内容都使用一个删除器类型,而不是类模板。

标签: c++ shared-ptr smart-pointers unique-ptr raii


【解决方案1】:

这里提供的另一个答案是,可以通过带有自定义删除器的unique_ptr 的函数返回来完成与我所要求的接近的事情,该删除器可以隐式转换为shared_ptr

给出的答案是 std::shared_ptr 无法定义为类型特征的删除器。建议的答案作为替代方案,使用返回 unique_ptr 的函数,隐式转换为 shared_ptr

由于这不是类型的一部分,因此可能会犯一个简单的错误,从而导致内存泄漏。这是我想要避免的。

例如:

// Correct usage:
shared_ptr<SDL_Surface> s(createSurface(IMG_Load("image.png")));

// Memory Leak:
shared_ptr<SDL_Surface> s(IMG_Load("image.png"));

我想表达的概念是将删除器作为类型的一部分(unique_ptr 允许),但具有shared_ptr 的功能。我建议的解决方案源自shared_ptr,并提供删除器类型作为模板参数。这不会占用额外的内存,并且与unique_ptr 的工作方式相同。

template<class T, class D = std::default_delete<T>>
struct shared_ptr_with_deleter : public std::shared_ptr<T>
{
  explicit shared_ptr_with_deleter(T* t = nullptr)
      : std::shared_ptr<T>(t, D()) {}

  // Reset function, as it also needs to properly set the deleter.
  void reset(T* t = nullptr) { std::shared_ptr<T>::reset(t, D());  }
};

与删除器类一起使用(感谢 Jonathan Wakely。比我的宏更干净(现已删除)):

struct SDL_Deleter
{
  void operator()(SDL_Surface* p) const { if (p) SDL_FreeSurface(p); }
  void operator()(SDL_RWops* p) const { if (p) SDL_RWclose(p); }
};

using SurfacePtr = std::unique_ptr<SDL_Surface, SDL_Deleter>;
using SurfaceShPtr = shared_ptr_with_deleter<SDL_Surface, SDL_Deleter>;

using RWopsPtr = std::unique_ptr<SDL_RWops, SDL_Deleter>;
using RWopsShPtr = shared_ptr_with_deleter<SDL_RWops, SDL_Deleter>;

具有SurfaceShPtr 成员的实例类型保证可以正确清理,与SurfacePtr 相同,这正是我想要的。

// Correct Usage (much harder to use incorrectly now):
SurfaceShPtr s(IMG_Load("image.png"));

// Still correct usage
s.reset(IMG_Load("other.png"));

对于 cmets 等,我将暂时搁置此问题,但不接受答案。也许我错过了更危险的警告(非虚拟析构函数不是一个,因为父级 shared_ptr 负责删除)。

【讨论】:

  • 这是有道理的。对象切片没有问题,因为你的派生类型没有额外的数据或函数,它只用于一个目的:在构造时传递一个删除器,一旦完成,对象是否被切片并不重要,你只保留shared_ptr&lt;T&gt; 基础部分。就我个人而言,我仍然更喜欢我的创建函数返回unique_ptr 的建议,它可以转换为仍然使用正确删除器的shared_ptr。我认为这更灵活,您可以从unique_ptr 转换为shared_ptr,但反之则不行。
  • “我建议的解决方案是从 shared_ptr 派生的,使用静态删除器作为类型特征。” 这不是类型特征,它是默认模板参数。
  • 谢谢乔纳森。我修正了不正确的措辞。我应该补充一点,我完全同意这种方式不太灵活,但需要限制,因为我永远不希望能够使用不正确的删除器创建 shared_ptr。传递 shared_ptr 并不能保证这一点。再次。感谢您对此的帮助!
  • 您的方法也不提供保证。 SurfaceShPtr s; s.reset(new SDL_Surface); 现在s 有错误的删除器。所以你的类型不是万无一失的,你需要遵循一个约定来安全地使用它,这也适用于我的建议“总是使用返回unique_ptr的创建函数创建类型”,如果你一直这样做并且永远不会写new SDL_Surface,那么你有同样的保证:你永远不会有一个没有正确删除器的shared_Ptr&lt;SDL_Surface&gt;,因为获得一个的唯一方法是从unique_ptr转换为正确的删除器
  • 不应该添加void reset(T* t = nullptr) { std::shared_ptr&lt;T&gt;(t, D()); }保留保证,即使使用s.reset(new SDL_Surface);
【解决方案2】:

typedef 是一种静态的编译时特性。

传递给shared_ptr 的删除器是动态的运行时属性。删除器是“类型擦除”的,不是shared_ptr 接口的一部分。

因此,您不能声明 typedef 来表示替代删除器,您只需将一个传递给构造函数。

实现与上述unique_ptr 示例相同的用法的最佳方法是什么。

您可以使用函数来创建资源并将它们返回到shared_ptr

shared_ptr<SDL_Surface> create_sdl_surface(const char* s)
{
  return shared_ptr<SDL_Surface>(IMG_load(s), SDL_FreeSurface);
}

但是我会让这些函数返回一个unique_ptr,它可以转换为shared_ptr,如下所示。

我会摆脱宏并做这样的事情:

// type with overloaded functions for freeing each resource type
struct SDL_deleter
{
  void operator()(SDL_Surface* p) const { if (p) SDL_FreeSurface(p); }
  void operator()(SDL_RWops* p) const { if (p) SDL_RWclose(p); }
  // etc.
};

// a unique_ptr using SDL_deleter:
template<typename P>
  using SDL_Ptr = std::unique_ptr<P, SDL_deleter>;

// typedefs for the common ptr types:
using SurfacePtr = SDL_ptr<SDL_Surface>;
using RWopsPtr = SDL_ptr<SDL_RWops>;
// etc.

要回答您问题的 shared_ptr 部分,请定义创建资源并在 SDL_ptr 中返回它们的函数:

SurfacePtr createSurface(const char* s) { return SurfacePtr(IMG_load(s)); }
RWopsPtr createRWops([...]) { return RWopsPtr([...]); }
// etc.

然后,您可以从这些函数的结果轻松创建shared_ptr

shared_ptr<SDL_Surface> s = createSurface("image.png");

shared_ptr 自动从unique_ptr 获取正确的删除器。

【讨论】:

  • 抱歉 const char* 的使用。我的例子有点快,犯了一个错误。我现在已经修好了。
  • 如果您想快速查看我的替代答案,我将不胜感激。干杯。
  • 注意删除器只有在p != nullptr为真时才会被调用,所以不需要if(p)
  • @AlexisWilke,这通常不是真的。对于shared_ptr,如果删除器“拥有”一个指针,则使用删除器,无论是否为空。 Live example。从unique_ptr 转换时,如果unique_ptr 不拥有任何东西,shared_ptr 确实不会拥有任何东西(至少,因为我的LWG 2415 问题已针对 C++17 解决),但是上面定义的删除器可以安全地直接与shared_ptr 一起使用。对于shared_ptr 未从unique_ptr 转换的情况,可能需要进行空检查。
  • 啊!抱歉,我对删除运算符重载感到困惑......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-09-11
  • 2017-05-19
  • 2017-05-15
  • 1970-01-01
  • 1970-01-01
  • 2018-10-16
相关资源
最近更新 更多