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

Why and when do I need to supply my own deleter?

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

If you use a smart pointer to manage a resource other than memory allocated by new, remember to pass a deleter.


更新:

正如评论中所问,我对引用的文本和示例不清楚的原因是我对某些事情的想法是错误的,我一直认为智能指针只是发明的 for/related到动态内存管理。所以这个例子使用智能指针来管理非动态内存的东西让我很困惑。

前辈讲解得很好:

The smart pointer doesn't care at all about something being dynamic memory as such. It's just a way to keep track of something while you need it, and destroy that something when it goes out of scope. The point of mentioning file handles, network connections, etc., was to point out that they're not dynamic memory, but a smart pointer can manage them just fine anyway.


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 不适用于解除分配、释放、丢弃或以其他方式处置生命周期受智能指针控制的资源时,您需要为智能指针创建提供自己的删除指针。

智能指针的典型用途是分配内存作为智能指针管理的资源,这样当智能指针超出范围时,被管理的资源(在本例中为内存)将被丢弃,方法是使用delete 运算符。

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

所以需要自己的删除器的关键问题是"what actions that were done before the object constructor was invoked need to be unwound and backed out after the object's destructor completes?"

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

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

所以当对这种delete运算符不合适的资源使用智能指针时,您需要提供自己的删除方法或函数或运算符,以便智能指针在退出时使用作用域并触发其自身的析构函数,该析构函数将依次处理智能指针管理的任何资源的丢弃。

带有输出的简单示例

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

一个简单的 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<> 删除器的讨论以及为什么它不可用。

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

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

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

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

如果您实际上是出于某种原因手动分配自己的内存,那么当 delete 尝试释放它时,您将遇到非常糟糕的情况。

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

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

入门中的示例实际上非常好(我在 之后欠他们一个),但是 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.

该示例演示了如何利用类型实例的确定性生命周期。它们被破坏时发生的事情由析构函数定义(排除内置类型,它们没有)。析构函数是 "cleans up" 其状态的类型的一部分。虽然通常没什么可做的,但内存分配确实需要清理,在示例中,必须调用断开连接函数。这对于任何手动管理资源的类型都是如此(除了简单的聚合或成员变量的熟悉),这个例子同样可以是

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 ("aggregation") 拥有的值,清理会自动发生(与 istr 无关由 std::string::~string()) 管理。