为啥以及何时需要提供我自己的删除器?

Posted

技术标签:

【中文标题】为啥以及何时需要提供我自己的删除器?【英文标题】:Why and when do I need to supply my own deleter?为什么以及何时需要提供我自己的删除器? 【发布时间】:2018-07-11 05:58:20 【问题描述】:

为什么以及何时需要提供我自己的删除器?关键字delete 还不够吗?

如果您使用智能指针来管理内存以外的资源 由new分配,记得传递一个删除器


更新:

正如在 cmets 中被问到的那样,我不清楚引用的文本和示例的原因是我想错了一些事情,我一直认为智能指针只是为动态内存管理而发明的/与动态内存管理相关.所以这个例子使用智能指针来管理一个非动态内存的东西让我很困惑。

一位前辈的好解释:

智能指针根本不关心动态的东西 记忆之类的。这只是一种在您进行时跟踪某事的方法 需要它,并在它超出范围时销毁它。这 提到文件句柄、网络连接等的要点是 指出它们不是动态内存,但智能指针可以 无论如何都可以很好地管理它们。


C++ Primer 5th以伪网络连接(不定义析构函数)来说明。

不好:

struct destination; // represents what we are connecting to
struct connection; // information needed to use the connection
connection connect(destination*); // open the connection
void disconnect(connection); // close the given connection
void f(destination &d /* other parameters */)

// get a connection; must remember to close it when done
connection c = connect(&d);
// use the connection
// if we forget to call disconnect before exiting f, there will be no way to closes

好:

void end_connection(connection *p)  disconnect(*p); 
void f(destination &d /* other parameters */)

connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed

完整上下文截图(我清除了一些不相关的文字):

【问题讨论】:

delete 用于内存释放,与 new 相反。但是,这不是在谈论使用new 分配的内存,而是“由new 分配的内存以外的资源”,这意味着delete 不适合释放资源,因为new 不用于创建/生成/分配资源。 谁是知道如何释放或清理资源的最佳用户,而不是使用它的用户? 例如,如果您想存储从名为get_database_handle() 的函数获得的数据库句柄,则可能需要使用一个名为release_database_handle() 的对应函数。 delete 会做完全错误的事情。 @RichardChambers ...和对象销毁。 您仍然可以在不使用new 的情况下分配动态内存 - 最明显的是malloc,您需要提供free 作为删除器。 【参考方案1】:

当标准 delete 不适用于释放、释放、丢弃或以其他方式处置其生命周期由智能指针控制的资源时,您需要为智能指针创建提供自己的删除。

智能指针的典型用途是将内存分配为由智能指针管理的资源,这样当智能指针超出范围时,被管理的资源(在这种情况下为内存)通过使用 @ 丢弃987654329@运营商。

标准的delete 操作符做了两件事:(1) 调用对象的析构函数以允许对象在其分配的内存被释放或解除分配之前执行它需要做的任何清理工作;(2) 释放已分配的内存由构造对象时的标准new 运算符。这是 new 操作符发生的相反顺序,它 (1) 为对象分配内存并执行为对象建立构造环境所需的基本初始化,以及 (2) 调用对象的构造函数来创建对象的起始状态。见What does the C++ new operator do other than allocation and a ctor call?

因此,需要您自己的删除器的关键问题是“在对象的析构函数完成后,在调用对象构造函数之前执行的哪些操作需要展开并退出?”

通常这是某种类型的内存分配,例如由标准 new 运算符完成。

但是,对于使用new 运算符分配的内存以外的某些资源,使用delete 运算符是不合适的,因为该资源不是使用new 运算符分配的内存。

因此,当在 delete 运算符不适合的此类资源中使用智能指针时,您需要提供自己的删除器方法或函数或运算符,当智能指针超出范围并触发时将使用它们它自己的析构函数将依次处理丢弃由智能指针管理的任何资源。

一个简单的输出示例

我将一个简单的示例与std::unique_ptr&lt;&gt; 放在一起,以及生成的输出显示使用和不使用带有指针的删除器以及显式使用析构函数。

一个简单的 Windows 控制台应用程序的源代码如下所示:

// ConsoleSmartPointer.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <memory>
#include <string>
#include <iostream>

class Fred 
public:
    Fred()  std::cout << "  Fred Constructor called." << std::endl; 
    ~Fred()  std::cout << "  Fred Destructor called." << std::endl; 
;
class George 
public:
    George()  std::cout << "   George Constructor called" << std::endl; 
    ~George()  std::cout << "   George Destructor called" << std::endl; 
private:
    int iSomeData;
    std::string  a_label;
    Fred  myFred;
;

void cleanupGeorge(George *)

    // just write out a log and do not explicitly call the object destructor.
    std::cout << "  cleanupGeorge() called" << std::endl;


void cleanupGeorge2(George *x)

    // write out our message and then explicitly call the destructor for our
    // object that we are the deleter for.
    std::cout << "  cleanupGeorge2() called" << std::endl;
    x->~George();    // explicitly call destructor to do cleanup.


int func1()

    // create a unique_ptr<> that does not have a deleter.
    std::cout << "func1 start. No deleter." << std::endl;

    std::unique_ptr<George> p(new George);

    std::cout << "func1 end." << std::endl;
    return 0;


int func2()

    // create a unique_ptr<> with a deleter that will not explicitly call the destructor of the
    // object created.
    std::cout << "func2 start. Special deleter, no explicit destructor call." << std::endl;

    std::unique_ptr<George, void(*)(George *)> p(new George, cleanupGeorge);

    std::cout << "func2 end." << std::endl;
    return 0;


int func3()

    // create a unique_ptr<> with a deleter that will trigger the destructor of the
    // object created.
    std::cout << "func3 start. Special deleter, explicit destructor call in deleter." << std::endl;

    std::unique_ptr<George, void(*)(George *)> p(new George, cleanupGeorge2);

    std::cout << "func3 end." << std::endl;
    return 0;


int main()

    func1();
    func2();
    func3();
    return 0;

上面的简单应用程序生成以下输出:

func1 start. No deleter.
  Fred Constructor called.
   George Constructor called
func1 end.
   George Destructor called
  Fred Destructor called.
func2 start. Special deleter, no explicit destructor call.
  Fred Constructor called.
   George Constructor called
func2 end.
  cleanupGeorge() called
func3 start. Special deleter, explicit destructor call in deleter.
  Fred Constructor called.
   George Constructor called
func3 end.
  cleanupGeorge2() called
   George Destructor called
  Fred Destructor called.

其他帖子

What is a smart pointer and when should I use one?

Using custom deleter with std::shared_ptr

还可以查看有关 std::make_shared&lt;&gt; 的删除器的讨论以及为什么它不可用。 How to pass deleter to make_shared?

Is custom deleter for std::unique_ptr a valid place for manual call to destructor?

When does std::unique_ptr<A> need a special deleter if A has a destructor?

RAII and smart pointers in C++

【讨论】:

释放非内存资源是析构函数的工作。关键是并不是所有的类都具有良好的析构函数(引自入门)。如果我们不能修复这些类并且仍然想用智能指针来管理它们的生命周期,那么我们应该求助于自定义删除器。另一个用例是当@Paul Sander 的回答中提到的标准new 没有分配对象时(也作为入门中的示例给出)。【参考方案2】:

当(显然)delete 不是您想要销毁对象的方式时。使用placement new 分配的对象可能就是一个简单的例子。

入门中的示例实际上非常好(我在trashing them earlier 之后欠他们一个),但std::shared_ptr(或std::unique_ptr)的另一个创造性用途可能是管理COM 对象的生命周期。这些是通过调用Release () 方法而不是调用delete 来释放的(如果你这样做了,那么,晚安维也纳)。

所以,为了说明这一点,您可以这样做:

static void release_com_object (IUnknown *obj)  obj->Release (); 

IUnknown *my_com_object = ...
std::shared_ptr <IUnknown> managed_com_object (my_com_object, release_com_object);

您无需了解任何有关 COM 的知识即可理解这里的基本思想。一般来说,释放资源的方法有很多种,一组合适的自定义删除器可以处理所有这些,这是一个非常酷的技巧。


啊,我现在真的进入了最佳状态。这是另一个给你的,这次是std::unique_ptr 和一个 lambda(不知道他们为什么在那里使用shared_ptr - 它要贵得多)。注意使用 std::unique_ptr 时的不同语法 - 你必须告诉模板删除器的函数签名:

FILE *f = fopen ("myfile", "r");

if (f)

    std::unique_ptr <FILE, void (*) (FILE *)> (f, [] (FILE *f)  fclose (f); );
    // Do stuff with f
   // file will be closed here

天哪,你能做的太多了。

Live demo.

【讨论】:

使用shared_ptr 来管理 COM 对象对我来说有点矫枉过正...... @Neil 我同意。当然应该是unique_ptr。鉴于 OP 在他的问题中所说的话,我只是顺其自然。请参阅我的后续补充示例,其中我确实使用了unique_ptr【参考方案3】:

该示例演示了如何利用类型实例的确定性生命周期。它们销毁时发生的情况由析构函数定义(排除内置类型,它们没有)。析构函数是“清理”其状态的类型的一部分。虽然通常没有太多工作要做,但确实必须清理内存分配,并且在示例中,必须调用断开连接函数。这适用于任何手动管理资源的类型(除了简单的聚合或成员变量的认识),示例同样可以是

class ConnectionHandle 
    public:
        ConnectionHandle(destination& d) : c(connect(d)) 
        ~ConnectionHandle()  end_connection(c); 
    private:
        connection& c;
;

当这种类型的生命周期由智能指针管理时,使用智能指针的析构函数来清理资源是一种可能性,这就是示例的内容。这适用于std::shared_ptrstd::unique_ptr,尽管在后一种情况下,自定义删除器是类型签名的一部分(传递unique_ptr 时需要更多输入)。

将这种情况与需要没有自定义删除器的情况进行比较也很有启发性:

struct A  int i; std::string str; ;

auto sp = std::make_shared<A>(42, "foo");

这里A 的资源是A(“聚合”)拥有的值,清理会自动发生(i 无事可做,strstd::string::~string() 管理)。

【讨论】:

【参考方案4】:

C++ 允许您使用new 编写自己的自定义分配器。就像你应该如何 delete new 一样,你应该让你的自定义分配器分配的所有东西也被它删除。

由此引起的问题的一个具体示例是,如果您使用自定义分配器来跟踪内存预算(即,您将每个分配分配给某个预算,并在超出任何这些预算时发出警告)。假设这包含了newdelete,所以当你的智能指针超出范围时,只有delete 被调用,并且自定义分配器不知道内存已被释放,最终你的内存使用不准确预算。

如果使用相同类型的包装分配器来检测泄漏,直接调用delete 会导致误报。

如果您实际上是出于某种原因手动分配自己的内存,那么当delete 试图释放它时,您将度过一段非常糟糕的时光。

在您的示例中,网络连接的内存在没有先能够完全断开连接的情况下被释放。在实际情况下,结果可能是连接的另一端挂起直到超时,或者出现某种关于断开连接的错误。

【讨论】:

以上是关于为啥以及何时需要提供我自己的删除器?的主要内容,如果未能解决你的问题,请参考以下文章

为啥存在 Google maven 存储库以及我应该何时使用它?

为啥以及何时应该在块的末尾使用逗号?

何时以及为啥需要在 C++ 中使用 cin.ignore()?

为啥以及何时使用重组分支?

为啥我们需要复制构造函数以及何时应该在 java 中使用复制构造函数

使用调度者