2

我一直在考虑delete this在 c++ 中的可能用途,并且我已经看到了一种用途。

因为你只能说delete this当一个对象在堆上时,我可以将析构函数设为私有并完全停止在堆栈上创建对象。最后,我可以通过delete this在充当析构函数的随机公共成员函数中说来删除堆上的对象。我的问题:

1)为什么我要强制对象在堆上而不是在堆栈上?

2)除此之外还有其他用途delete this吗?(假设这是对它的合法使用:))

4

9 回答 9

7

任何使用的方案delete this都有些危险,因为调用执行此操作的函数的人都会留下一个悬空指针。(当然,正常删除对象的时候也是这样,但是那样的话,很明显对象已经被删除了)。尽管如此,还是有一些合理的情况需要一个对象来管理它自己的生命周期。

它可以用来实现一个讨厌的、侵入性的引用计数方案。您将具有“获取”对对象的引用的功能,防止它被删除,然后在完成后“释放”它,如果没有其他人获得它,则将其删除,如下所示:

class Nasty {
public:
    Nasty() : references(1) {}

    void acquire() {
        ++references;
    }
    void release() {
        if (--references == 0) {
            delete this;
        }
    }
private:
    ~Nasty() {}
    size_t references;
};

// Usage
Nasty * nasty = new Nasty; // 1 reference
nasty->acquire();          // get a second reference
nasty->release();          // back to one
nasty->release();          // deleted
nasty->acquire();          // BOOM!

我更愿意std::shared_ptr为此目的使用它,因为它是线程安全的、异常安全的、适用于任何类型而不需要任何显式支持,并且在删除后阻止访问。

更有用的是,它可以在事件驱动系统中使用,在该系统中创建对象,然后管理它们自己,直到它们收到一个告诉它们不再需要它们的事件:

class Worker : EventReceiver {
public:
    Worker() {
        start_receiving_events(this);
    }    
    virtual void on(WorkEvent) {
        do_work();
    }
    virtual void on(DeleteEvent) {
        stop_receiving_events(this);
        delete this;
    }
private:
    ~Worker() {}
    void do_work();
};
于 2012-09-07T11:10:44.640 回答
4

1)为什么我要强制对象在堆上而不是在堆栈上?

1) 因为对象的生命周期在逻辑上与范围无关(例如,函数体等)。要么是因为它必须管理自己的生命周期,要么是因为它本质上是一个共享对象(因此,它的生命周期必须附加到它的共同依赖对象的生命周期)。这里的一些人指出了一些例子,比如事件处理程序、任务对象(在调度程序中),以及复杂对象层次结构中的一般对象。

2)因为您想控制为分配/释放和构造/销毁执行代码的确切位置。这里的典型用例是跨模块代码(跨可执行文件和 DLL(或 .so 文件)传播)。由于模块之间的二进制兼容性和单独堆的问题,通常要求您严格控制这些分配构造操作发生在哪个模块中。这意味着仅使用基于堆的对象。

2)除此之外还有其他用途吗?(假设这是对它的合法使用:))

好吧,您的用例实际上只是“操作方法”而不是“为什么”。当然,如果您要delete this;在成员函数中使用语句,那么您必须有适当的控制来强制所有创建发生new(并且在发生语句的同一翻译单元中delete this;)。不这样做只会是非常糟糕的风格和危险的。但这并没有解决您要使用它的“原因”。

1)正如其他人指出的那样,一个合法的用例是您拥有一个对象,该对象可以确定其工作何时结束并因此自行销毁。例如,事件处理程序在事件被处理时删除自己,网络通信对象在指定执行的事务结束后删除自己,或者调度程序中的任务对象在任务完成时删除自己。然而,这留下了一个大问题:向外界发出它不再存在的信号。这就是为什么许多人提到“侵入式引用计数”方案,这是确保对象仅在没有更多引用时才被删除的一种方法。另一种解决方案是使用“有效”对象的全局(类似单例)存储库,delete this;调用(作为重载的 new/delete 的一部分,或与每个 new/delete 调用一起)。

然而,有一种更简单且侵入性更小的方法来实现相同的行为,尽管不太经济。可以使用自引用shared_ptr方案。如此:

class AutonomousObject {
  private:
    std::shared_ptr<AutonomousObject> m_shared_this;

  protected:
    AutonomousObject(/* some params */);

  public:

    virtual ~AutonomousObject() { };

    template <typename... Args>
    static std::weak_ptr<AutonomousObject> Create(Args&&... args) {
      std::shared_ptr<AutonomousObject> result(new AutonomousObject(std::forward<Args>(args)...));
      result->m_shared_this = result;  // link the self-reference.
      return result;  // return a weak-pointer.
    };

    // this is the function called when the life-time should be terminated:
    void OnTerminate() {
      m_shared_this.reset( NULL );  // do not use reset(), but use reset( NULL ).
    };
};

使用上面的方法(或者这个粗略的例子的一些变化,取决于你的需要),只要它认为有必要并且没有其他人在使用它,对象就会一直存在。弱指针机制充当代理,由对象的可能外部用户查询对象的存在。这种方案使对象更重(其中有一个共享指针),但实现起来更容易、更安全。当然,您必须确保对象最终会自行删除,但在这种情况下,这是给定的。

2)我可以想到的第二个用例与将对象限制为仅堆的第二个动机有关(见上文),但是,它也适用于您不对其进行限制的情况。如果要确保释放和销毁都被分派到正确的模块(分配和构造对象的模块),则必须使用动态分派方法。为此,最简单的方法就是使用虚函数。但是,虚拟析构函数不会将其切断,因为它只调度销毁,而不是释放。解决方案是使用调用delete this;相关对象的虚拟“销毁”功能。这是一个简单的方案来实现这一点:

struct CrossModuleDeleter;  //forward-declare.

class CrossModuleObject {
  private:
    virtual void Destroy() /* final */;

  public:
    CrossModuleObject(/* some params */);  //constructor can be public.

    virtual ~CrossModuleObject() { };  //destructor can be public.

    //.... whatever...

    friend struct CrossModuleDeleter;

    template <typename... Args>
    static std::shared_ptr< CrossModuleObject > Create(Args&&... args);
};

struct CrossModuleDeleter {
  void operator()(CrossModuleObject* p) const {
    p->Destroy();  // do a virtual dispatch to reach the correct deallocator.
  };
};

// In the cpp file:

// Note: This function should not be inlined, so stash it into a cpp file.
void CrossModuleObject::Destroy() {
  delete this;
};

template <typename... Args>
std::shared_ptr< CrossModuleObject > CrossModuleObject::Create(Args&&... args) {
  return std::shared_ptr< CrossModuleObject >( new CrossModuleObject(std::forward<Args>(args)...), CrossModuleDeleter() );
};

上述方案在实践中运行良好,它的优点是该类可以充当基类,而不会受到派生类中这种虚拟销毁机制的额外入侵。而且,您还可以修改它以仅允许基于堆的对象(通常使构造函数-析构函数私有或受保护)。如果没有基于堆的限制,优点是您仍然可以根据需要将对象用作局部变量或数据成员(按值),但是,当然,任何使用班级。

据我所知,这些是我在任何地方见过或听说过的唯一合法用例(第一个很容易避免,正如我所展示的,而且经常应该避免)。

于 2012-09-07T15:22:48.020 回答
3

一般原因是对象的生命周期是由类内部的某些因素决定的,至少从应用程序的角度来看是这样。因此,它很可能是一个调用delete this;.

显然,当对象是唯一知道需要多长时间的对象时,您不能将其放在随机线程堆栈上。有必要在堆上创建这样的对象。

于 2012-09-07T11:21:11.030 回答
1
delete this;

有时可能很有用,通常用于控制另一个对象的生命周期的控件类。使用侵入式引用计数,它控制的类是从它派生的类。

使用这样一个类的结果应该是让你的类的用户或创建者更容易处理生命周期。如果它没有实现这一点,那就是不好的做法。

一个合理的例子可能是你需要一个类在它被破坏之前清理对它自己的所有引用。在这种情况下,每当您存储对它的引用时(大概在您的模型中),您“告诉”该类,然后在退出时,您的类会在它调用delete this自身之前将这些引用或其他任何内容清空。

对于您班级的用户,这一切都应该发生在“幕后”。

于 2012-09-07T11:51:36.737 回答
1

“为什么我要强制对象在堆上而不是在堆栈上?”

通常,当您强制这样做不是因为您要这样做时,这是因为该类是某些多态层次结构的一部分,并且获得它的唯一合法方法是从工厂函数返回不同派生类的实例,根据你传递给它的参数,或者根据它知道的一些配置。然后很容易安排工厂函数使用new. 即使他们想要,这些类的用户也不可能将它们放在堆栈上,因为他们事先不知道他们正在使用的对象的派生类型,只知道基本类型。

一旦你有了这样的对象,你就知道它们是用 销毁的delete,你可以考虑以最终结束的方式管理它们的生命周期delete this。只有当对象能够以某种方式知道何时不再需要它时,您才会这样做,这通常是(如 Mike 所说),因为它是某些框架的一部分,它不会显式管理对象生命周期,但会告诉它的组件他们已经被分离/注销/无论如何[*]。

如果我没记错的话,James Kanze 就是你的人选。我可能记错了,但我想他偶尔会提到,在他的设计delete this中不仅使用过,而且很常见。这样的设计避免了共享所有权和外部生命周期管理,有利于实体对象网络管理它们自己的生命周期。并且在必要时,在摧毁自己之前从任何了解他们的信息中注销自己。因此,如果您在“工具带”中有多个“工具”,那么您不会认为工具带“拥有”对每个工具的引用,您会想到工具将自己放入和取出皮带。

[*] 否则,您将让您的工厂返回 aunique_ptrauto_ptr鼓励调用者将对象直接填充到他们选择的内存管理类型中,或者您将返回原始指针但通过文档提供相同的鼓励。所有你习惯看到的东西。

于 2012-09-07T11:51:42.157 回答
1

这通常是一个非常糟糕的主意。在极少数情况下,例如,COM 对象强制执行侵入式引用计数。您只会在非常具体的情境原因下执行此操作 - 永远不会针对通用课程。

于 2012-09-07T11:14:40.567 回答
1

1)为什么我要强制对象在堆上而不是在堆栈上?

因为它的生命周期不是由范围规则决定的。

2)除此之外还有其他用途吗?(假设这是对它的合法使用:))

delete this当对象是最适合负责其自身寿命的对象时,您使用它。我知道的最简单的例子之一是 GUI 中的窗口。窗口对事件做出反应,其中的一个子集意味着窗口必须关闭并因此被删除。在事件处理程序中,窗口执行delete this. (您可以将处理委托给控制器类。但是“窗口将事件转发给决定删除窗口的控制器类”的情况并没有太大不同delete this,窗口事件处理程序将被删除窗口。您也可以需要将关闭与删除分离,但您的理由与 ) 的可取性无关delete this

于 2012-09-07T11:21:40.630 回答
0

曾几何时,我正在编写一些插件代码。我相信我混合构建(调试插件,发布主代码或可能相反),因为一个部分应该很快。或者可能发生了另一种情况。这样的 main 已经基于 gcc 发布,并且插件正在 VC 上调试/测试。当主代码从插件中删除某些内容或插件删除某些内容时,会发生内存问题。这是因为它们都使用了不同的内存池或 malloc 实现。所以我有一个私有 dtor 和一个名为 deleteThis() 的虚函数。

-edit- 现在我可能会考虑重载删除运算符或使用智能指针,或者只是声明永远不要删除函数。这将取决于并且通常不应该重载 new/delete 除非你真的知道你在做什么(不要这样做)。我决定使用 deleteThis() 是因为我发现它比 C 中的 thing_alloc 和 thing_free 更容易,因为 deleteThis() 感觉更像是 OOP 方式

于 2012-09-07T11:09:10.303 回答
0

一个好的经验法则是不使用delete this.

简单地说,使用的东西new应该有足够的责任在使用delete对象时使用。这也避免了堆栈/堆上的问题。

于 2012-09-07T11:07:15.030 回答