【问题标题】:delete this & private destructor删除这个 & 私有析构函数
【发布时间】:2012-09-01 06:17:11
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: c++ oop memory this destructor


    【解决方案1】:

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

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

    【讨论】:

    • 为什么?我有很多对象,他们最清楚是否仍然需要它们。创建它们的东西明确地这样做是因为 IT 不想为管理某些外部 gizmo 的细节而烦恼。
    • 但是对象本身不知道是否在栈上。如果这是一个问题,请使用share_ptr
    • 那些对象肯定知道,因为它们的 ctor 是私有的。
    • 不会将ctor的返回值存储在静态变量中吗?
    • 好吧,ctors 没有返回值,但假设您指的是新对象的地址:也许。即使这样,dtor 也会知道这样一个私有静态变量,它可以自行删除。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      任何使用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();
      };
      

      【讨论】:

      • 这假定“创建对象的人”保留了一个指针。如果那是同一个类的静态方法,它可能不知道。或者它可能有一个类内部的方法来确定一个指针是否悬空。
      • @MSalters:我没有看到任何假设。调用函数的人必须有一个指针才能这样做,并且必须知道事后不要使用指针才能安全地这样做。
      • 这就是为什么我提到了同一个类的静态方法。另一种情况是 dtor 可以发出到期信号的任何设置,如果多个调用者可能导致对象结束,则需要这种设置。
      • 是否可以从具有私有 dtor 的类派生类,例如您的 class Nasty
      • @Vidak:不,如果你想将复杂的生命周期管理与复杂的对象关系结合起来,它必须是protected,几乎可以肯定是virtual。到那时,称它为Nasty 是轻描淡写的。
      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

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

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

        【讨论】:

          【解决方案6】:

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

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

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

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

          【讨论】:

            【解决方案7】:
            delete this;
            

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

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

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

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

            【讨论】:

              【解决方案8】:

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

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

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

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

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

              【讨论】:

                【解决方案9】:

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

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

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

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

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

                1) 正如其他人所指出的,一个合法的用例是您拥有一个对象,该对象可以确定其工作何时结束并因此自行销毁。例如,事件处理程序在事件被处理时删除自己,网络通信对象在指定执行的事务结束后删除自己,或者调度程序中的任务对象在任务完成时删除自己。然而,这留下了一个大问题:向外界发出它不再存在的信号。这就是为什么许多人提到“侵入式引用计数”方案,这是确保对象仅在没有更多引用时才被删除的一种方法。另一种解决方案是使用“有效”对象的全局(类似单例)存储库,在这种情况下,对对象的任何访问都必须通过存储库中的检查,并且对象还必须同时从存储库中添加/删除自身进行 new 和 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() );
                };
                

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

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2011-11-05
                  • 2012-11-28
                  • 2011-07-15
                  • 2017-10-08
                  • 1970-01-01
                  • 2017-05-18
                  • 1970-01-01
                  相关资源
                  最近更新 更多