如何将自定义删除器与std :: unique_ptr成员一起使用?


133

我有一个具有unique_ptr成员的课程。

class Foo {
private:
    std::unique_ptr<Bar> bar;
    ...
};

Bar是具有create()函数和destroy()函数的第三方类。

如果我想std::unique_ptr在独立功能中使用它,我可以这样做:

void foo() {
    std::unique_ptr<Bar, void(*)(Bar*)> bar(create(), [](Bar* b){ destroy(b); });
    ...
}

有什么办法可以std::unique_ptr作为班级成员吗?

Answers:


133

假设createdestroy是具有以下签名的自由函数(在OP的代码片段中似乎是这种情况):

Bar* create();
void destroy(Bar*);

你可以写你的类Foo像这样

class Foo {

    std::unique_ptr<Bar, void(*)(Bar*)> ptr_;

    // ...

public:

    Foo() : ptr_(create(), destroy) { /* ... */ }

    // ...
};

请注意,您无需在此处编写任何lambda或自定义删除器,因为destroy它已经是删除器。


156
使用C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;

1
该解决方案的缺点是,它会使每个系统的开销加倍unique_ptr(它们必须全部将函数指针与指向实际数据的指针一起存储),要求每次都传递销毁函数,因此无法内联(因为模板无法专门针对特定的函数(仅签名),并且必须通过指针调用该函数(比直接调用的开销更大)。无论RICIDeduplicator的答案,由专业的仿避免所有这些成本。
ShadowRanger

@ShadowRanger是否在每次您是否显式传递它时都定义为default_delete <T>并存储了函数指针?
Herrgott

117

可以使用C ++ 11中的lambda干净地执行此操作(已在G ++ 4.8.2中进行了测试)。

鉴于此可重用typedef

template<typename T>
using deleted_unique_ptr = std::unique_ptr<T,std::function<void(T*)>>;

你可以写:

deleted_unique_ptr<Foo> foo(new Foo(), [](Foo* f) { customdeleter(f); });

例如,使用FILE*

deleted_unique_ptr<FILE> file(
    fopen("file.txt", "r"),
    [](FILE* f) { fclose(f); });

这样,您可以使用RAII获得异常安全清除的好处,而无需尝试/捕获噪声。


2
imo,这应该是答案。这是一个更漂亮的解决方案。还是有任何弊端,例如具有std::function定义等?
j00hi 2015年

17
@ j00hi,我认为该解决方案由于会产生不必要的开销std::function。与此解决方案不同,可以内联Lambda或自定义类(如接受的答案)。但是,如果您想隔离专用模块中的所有实现,则此方法具有优势。
magras 2015年

5
如果std :: function构造函数抛出异常,则会泄漏内存(如果lambda太大而无法放入std :: function对象中,则可能会发生)
StaceyGirl

4
Lambda真的需要在这里吗?deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);如果customdeleter遵循约定(返回空值并接受原始指针作为参数),则可能会很简单。
Victor Polevoy'9

这种方法有一个缺点。只要有可能,std :: function就不需要使用move构造函数。这意味着当您std :: move(my_deleted_unique_ptr)时,lambda包含的内容可能会被复制而不是移动,这可能不是您想要的。
GeniusIsme

70

您只需要创建一个删除器类:

struct BarDeleter {
  void operator()(Bar* b) { destroy(b); }
};

并将其提供为的模板参数unique_ptr。您仍然必须在构造函数中初始化unique_ptr:

class Foo {
  public:
    Foo() : bar(create()), ... { ... }

  private:
    std::unique_ptr<Bar, BarDeleter> bar;
    ...
};

据我所知,所有流行的c ++库都正确实现了这一点。由于BarDeleter 实际上没有任何状态,因此不需要占用中的任何空间unique_ptr


8
此选项是唯一可用于数组,std :: vector和其他集合的选项,因为它可以使用零参数std :: unique_ptr构造函数。其他答案使用的解决方案无法访问此零参数构造函数,因为在构造唯一指针时必须提供Deleter实例。但是此解决方案提供了Deleter类(struct BarDeleter)至std::unique_ptrstd::unique_ptr<Bar, BarDeleter>),该类允许std::unique_ptr构造函数自行创建Deleter实例。即允许以下代码std::unique_ptr<Bar, BarDeleter> bar[10];
DavidF '16

12
我将创建一个易于使用的typedeftypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
DavidF '16

@DavidF:或使用重复数据删除器的方法,该方法具有相同的优点(内联删除,每个方法没有额外的存储空间unique_ptr,构造时无需提供删除程序的实例),并增加了可以在std::unique_ptr<Bar>任何地方使用而无需记住的好处使用特殊typedef或显式提供程序的第二个模板参数。(要明确地说,这是一个很好的解决方案,我投票支持,但比无缝解决方案少了一步)
ShadowRanger

22

除非您需要能够在运行时更改删除程序,否则我强烈建议您使用自定义删除程序类型。例如,如果将函数指针用于删除器,则sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*)。换句话说,unique_ptr浪费了对象的一半字节。

但是,编写一个自定义的删除器来包装每个函数很麻烦。值得庆幸的是,我们可以编写一个以函数为模板的类型:

从C ++ 17开始:

template <auto fn>
using deleter_from_fn = std::integral_constant<decltype(fn), fn>;

template <typename T, auto fn>
using my_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;

// usage:
my_unique_ptr<Bar, destroy> p{create()};

在C ++ 17之前:

template <typename D, D fn>
using deleter_from_fn = std::integral_constant<D, fn>;

template <typename T, typename D, D fn>
using my_unique_ptr = std::unique_ptr<T, deleter_from_fn<D, fn>>;

// usage:
my_unique_ptr<Bar, decltype(destroy), destroy> p{create()};

好漂亮 我是否正确地实现了与rici的答案的仿函数相同的好处(将内存开销减少了一半,直接调用了函数而不是通过函数指针,完全消除了潜在的内联函数)?
ShadowRanger

是的,这应该提供自定义删除类的所有优点,因为这就是事实deleter_from_fn
rmcclellan

6

您可以简单地使用std::bind您的destroy函数。

std::unique_ptr<Bar, std::function<void(Bar*)>> bar(create(), std::bind(&destroy,
    std::placeholders::_1));

但是当然您也可以使用lambda。

std::unique_ptr<Bar, std::function<void(Bar*)>> ptr(create(), [](Bar* b){ destroy(b);});

6

您知道,使用自定义删除器并不是最好的方法,因为您必须在代码中全部提及它。
相反,由于::std只要涉及到自定义类型并且您尊重语义,就可以向名称空间级别的类添加特殊化,因此请执行以下操作:

专长std::default_delete

template <>
struct ::std::default_delete<Bar> {
    default_delete() = default;
    template <class U, class = std::enable_if_t<std::is_convertible<U*, Bar*>()>>
    constexpr default_delete(default_delete<U>) noexcept {}
    void operator()(Bar* p) const noexcept { destroy(p); }
};

也许也可以std::make_unique()

template <>
inline ::std::unique_ptr<Bar> ::std::make_unique<Bar>() {
    auto p = create();
    if (!p) throw std::runtime_error("Could not `create()` a new `Bar`.");
    return { p };
}

2
我对此会非常小心。开放std打开了一个全新的蠕虫罐头。还要注意,在std::make_uniqueC ++ 20之后不允许进行特殊化(因此不应在此之前完成),因为C ++ 20禁止对std不是类模板(std::make_unique是函数模板)的事物进行特殊化。请注意,如果传递给的指针std::unique_ptr<Bar>不是从分配的create(),而是从其他分配函数分配的,则最终也可能以UB结尾。
贾斯汀'18

我不认为这是允许的。在我看来,很难证明这种专业化std::default_delete可以满足原始模板的要求。我想这std::default_delete<Foo>()(p)将是一种有效的书写方式delete p;,因此,如果delete p;有效的书写方式(例如,如果Foo完成的话),将不会是相同的行为。此外,如果delete p;无效(Foo不完整),则将为指定新的行为std::default_delete<Foo>,而不是保持相同的行为。
贾斯汀

make_unique专业化是有问题的,但我肯定用的std::default_delete过载(不与模板enable_if,只为C的结构像OpenSSL的BIGNUM使用已知的破坏作用,在子类是不会发生的),它是迄今为止最简单的方法,如您的其余代码可以直接使用,unique_ptr<special_type>而无需将函子类型作为模板进行Deleter遍历,也不必使用typedef/ using为该类型命名,以免发生此问题。
ShadowRanger
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.