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

时间:2018-07-11 05:58:21

标签: c++ smart-pointers

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

  

如果您使用智能指针来管理内存以外的资源   由new分配,记住要通过删除程序


更新:

正如在评论中被问及的那样,我不清楚所引用的文字和示例的原因是我对某事的想法是错误的,这是因为我一直认为智能指针仅是为动态内存管理而发明/与之相关的。因此,该示例使用智能指针来管理非动态内存,这让我感到困惑。

上司的一个很好的解释:

  

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


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
}

完整的屏幕截图(我清除了一些无关的文字):
Smart Pointers and Dumb classes

Smart Pointers and Dumb classes part 2

4 个答案:

答案 0 :(得分:8)

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

智能指针的典型用法是分配内存作为智能指针管理的资源,以便当智能指针超出范围时,通过使用{ {1}}运算符。

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

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

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

但是,对于某些资源,除了使用new分配的内存之外,不适合使用new运算符,因为该资源不是使用{{ 1}}运算符。

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

带有输出的简单示例

我将new与一个简单的示例放在一起,并将生成的输出显示为使用指针而不使用删除器,以及显示析构函数的显式使用。

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

delete

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

std::unique_ptr<>

其他帖子

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

Using custom deleter with std::shared_ptr

还请参见有关使用// 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; } 进行删除的讨论以及为什么不可用。 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++

答案 1 :(得分:5)

(显然)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

答案 2 :(得分: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的任何操作,str由{ {1}}。

答案 3 :(得分:2)

C ++允许您使用new编写自己的自定义分配器。就像您应该delete new进行所有操作一样,也应该删除自定义分配器分配的所有内容。

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

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

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

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