C++中允许使用"delete this"吗?

279

如果删除语句是该类实例上将要执行的最后一个语句,是否允许执行 delete this; 呢?当然,我确定由 this-指针表示的对象是新创建的。

我在考虑这样做:

void SomeModule::doStuff()
{
    // in the controller, "this" object of SomeModule is the "current module"
    // now, if I want to switch over to a new Module, eg:

    controller->setWorkingModule(new OtherModule());

    // since the new "OtherModule" object will take the lead, 
    // I want to get rid of this "SomeModule" object:

    delete this;
}

我可以这样做吗?


16
主要问题在于,如果你删除这个东西,你就在类和用于创建该类对象的分配方法之间创建了紧密耦合。 这是非常糟糕的面向对象设计,因为面向对象编程最基本的事情是使类成为自主的,不知道也不关心它们的调用者正在做什么。 因此,一个正确设计的类不应该知道或关心它是如何被分配的。 如果出于某种原因需要这样一种奇特的机制,我认为更好的设计是在实际类周围使用包装类,并让包装类处理分配。 - Lundin
你不能在 setWorkingModule 中删除吗? - Jimmy T.
@Lundin MFC中的CFrameWnd类在PostNcDestroy中执行delete this;,因为这时候它所包装的WinAPI类被销毁了。因此,我认为它确实有自己有效的用例。 - Ayxan Haqverdili
@Lundin 问题不在于释放内存,而在于对象的销毁。在C++中,唯一正确的方法是使用shared_ptr来分离这两个过程,并且仍然实现封装和多态性。unique_ptr无法分离它们。该类并不关心内存的分配/释放,但它想要控制其生命周期。我敢打赌,使用shared_ptr/enable_shared_from_this可以正确地设计该类,但我不喜欢必须以这种方式完成,特别是因为shared_ptr/enable_shared_from_this会占用大量代码空间,因此对于我的嵌入式开发来说无法使用。 - Dragan
10个回答

272

22
相应的 FQA 也有一些有用的注释:http://yosefk.com/c++fqa/heap.html#fqa-16.15 - Alexandre C.
3
为了安全起见,您可以在原始对象上使用私有析构函数,以确保它不会被构造在栈上或作为数组或向量的一部分。 - Cem Kalyoncu
定义“careful” - CinCout
5
“Careful”在链接的常见问题解答文章中有定义。(虽然该常见问题解答链接大多数都是抱怨 - 就像其中的几乎所有内容一样 - C++ 有多糟糕) - CharonX
@CharonX • Yossi不再使用C++进行编程了。他现在更加快乐。这就是为什么FQA没有针对现代C++进行维护的原因。我喜欢FQA,因为它从一个批判和苛刻的角度来看待C++的真正痛点(尽管是C++98时期的)——这对于那些已经适应或灌输了这些痛点的人(包括我自己)来说很容易被忽视。 - Eljay

106
是的,delete this;有明确定义的结果,只要(正如您注意到的那样)确保对象动态分配,并且(当然)在销毁后不会尝试使用该对象。多年来,关于标准具体说明delete this;与删除其他指针有什么区别已经有很多问题被问到。对此的答案相当简短和简单:它没有说任何东西。它只是说delete的操作数必须是指向对象或一组对象的指针的表达式。它详细讨论了如何确定(如果有的话)释放内存调用哪个解除分配函数等诸多细节,但整个关于delete的部分(§[expr.delete])根本没有特别提到delete this;。析构函数部分则在一处(§[class.dtor]/13)提到了delete this

在虚拟析构函数的定义点(包括隐式定义(15.8)),非数组释放函数被确定为类的析构函数中出现的delete this表达式(参见8.3.5)。

这倾向支持标准认为delete this;有效——如果无效,其类型将没有意义。据我所知,这是标准提到delete this;的唯一地方。

总之,有些人认为delete this是一个可怕的黑客技术,并告诉任何倾听的人应该避免使用它。经常引用的问题之一是确保该类的对象仅动态分配的困难。其他人认为这是完全合理的习惯用法,并且经常使用它。个人而言,我介于两者之间:很少使用它,但在似乎是正确工具的情况下会毫不犹豫地使用它。使用该技术的主要场景是针对一个几乎完全自主存在的对象。James Kanze举了一个例子,他曾为一家电话公司开发一个计费/跟踪系统。当你开始打电话时,有些东西会记录下来并创建一个phone_call对象。从那时起,phone_call对象会处理电话的各个细节(例如拨号时建立连接、向数据库添加条目以表示通话开始的时间、如果进行多方通话则可能连接更多人等)。当通话中的最后一位人挂断电话时,phone_call对象做出最后的账务处理(例如向数据库添加条目以表示您挂断电话的时间,这样他们可以计算通话的持续时间),然后销毁自己。phone_call对象的生命周期基于第一个人开始拨打电话和最后一群人离开通话的时间——从整个系统的角度来看,它基本上是完全随意的,因此你无法将其与代码中的任何词法范围或类似的内容相关联。

对于任何关心编码可靠性的人:如果你在欧洲的任何地方打电话、接电话或通过电话,很可能至少部分由执行这种操作的代码处理。

2
谢谢,我会把它放在我的记忆里。我想你将构造函数和析构函数定义为私有,并使用一些静态工厂方法来创建这样的对象。 - Alexandre C.
@Alexandre:在大多数情况下,你可能会这样做——我不知道他所工作的系统的所有细节,所以我不能确定。 - Jerry Coffin
我通常解决内存分配问题的方法是在构造函数中包含一个bool selfDelete参数,并将其分配给成员变量。当然,这意味着程序员有足够的自由度来搞砸它,但我认为这比内存泄漏更可取。 - MBraedley
1
@MBraedley:我也做过同样的事情,但我更喜欢避免看起来像是一个临时解决方案的东西。 - Jerry Coffin
对于任何可能关心的人...有很大的机会它(至少部分)被处理的代码正好是做this。是的,这段代码正好被处理成了this。 ;) - Galaxy

53

如果这让你感到害怕,有一个完全合法的技巧:

void myclass::delete_me()
{
    std::unique_ptr<myclass> bye_bye(this);
}

我认为delete this在C++中是惯用语,我只是将其作为一种好奇的展示。

这种构造实际上有一个有用的情况 - 在抛出需要对象成员数据的异常后,您可以删除该对象。对象在抛出异常之前仍然有效。

void myclass::throw_error()
{
    std::unique_ptr<myclass> bye_bye(this);
    throw std::runtime_exception(this->error_msg);
}

注意:如果您使用的编译器早于C++11,则可以使用std::auto_ptr代替std::unique_ptr,它将执行相同的操作。


我无法使用c++11编译它,是否有一些特殊的编译器选项?此外,它不需要移动this指针吗? - Owl
@Owl 不确定你的意思,这对我有效:http://ideone.com/aavQUK。从*另一个*`unique_ptr`创建`unique_ptr`需要移动,但不是从原始指针。除非在C++17中修改了这些事情? - Mark Ransom
啊,原来是因为C++14版本呀。我需要在我的开发机上更新我的C++。今晚我会在我最近安装的gentoo系统上尝试再次运行! - Owl
这是一种“hack”的方法,除非你将析构函数设置为私有,否则将阻止unique_ptr正常工作。 - Dragan

27

C++ 设计的原因之一是为了方便代码重用。通常情况下,C++ 应该编写成无论类实例化在堆、数组或栈上都能正常运行。"Delete this" 是一种非常糟糕的编码实践,因为它只适用于单个实例在堆上定义的情况;而且最好不要使用其他开发人员通常用来清理堆的 delete 语句。这样做还假定未来没有维护程序员会通过添加 delete 语句来解决错误地感知到的内存泄漏。

即使您事先知道当前计划仅分配堆上的单个实例,但如果某个轻率的开发人员在未来决定在栈上创建实例呢?或者,如果他剪切并粘贴类的某些部分到一个他打算在栈上使用的新类中呢?当代码到达 "delete this" 时,它将删除它,但当对象超出作用域时,它将调用析构函数。析构函数将尝试再次删除它,然后您就完蛋了。过去,做这样的事情不仅会捣乱程序本身,还会影响操作系统,导致计算机需要重新启动。无论如何,这是极其不建议的,几乎应该避免。除非我绝望、酩酊大醉或真的讨厌我为之工作的公司,否则我都不会编写这样的代码。


7
我不明白为什么你会被踩。"应该编写 C++ 代码,使其能够在堆上、数组中或栈上实例化类时都能正常工作",这是非常好的建议。 - Joh
1
你可以将想要删除的对象本身包装在一个特殊的类中,该类会删除对象并自行销毁,然后使用此技术来防止堆栈分配:https://dev59.com/c3VD5IYBdhLWcg3wAGeD 有时确实没有可行的替代方案。我刚刚使用这种技术来自删除由DLL函数启动的线程,但是DLL函数必须在线程结束之前返回。 - Felix Dombek
1
你不能编写代码,以至于只要有人复制粘贴你的代码,就会误用它。 - Jimmy T.

24

允许使用delete this(仅限在此之后不再使用该对象),但我不会在实践中编写这样的代码。我认为delete this应该仅出现在调用了releaseRelease函数的函数中,并且看起来像这样:void release() { ref--; if (ref<1) delete this; }


这在我的每个项目中都只出现一次... :-) - cmaster - reinstate monica

21

在组件对象模型(COM)中,delete this构造可以作为Release方法的一部分,当您想要释放已获取的对象时调用该方法:

void IMyInterface::Release()
{
    --instanceCount;
    if(instanceCount == 0)
        delete this;
}

9

这是引用计数对象的核心习语。

引用计数是确定性垃圾回收的一种强形式-它确保对象管理其自身生命周期,而不是依赖“智能”指针等来进行管理。底层对象只能通过“Reference”智能指针访问,这些指针被设计为将成员整数(引用计数)递增和递减到实际对象中。

当最后一个引用从堆栈中删除或被删除时,引用计数将变为零。您的对象的默认行为将是调用“delete this”进行垃圾回收-我编写的库为基类提供了受保护的虚拟“CountIsZero”调用,以便您可以为缓存等事物覆盖此行为。

使其安全的关键是不允许用户访问所讨论的对象的构造函数(将其设为protected),而是让他们调用某个静态成员-工厂-例如“static Reference CreateT(...)”。这样,您就可以确定地知道它们总是使用普通的“new”构建的,并且没有原始指针可用,因此“delete this”不会出现问题。


1
为什么不能只有一个(单例)类“分配器/垃圾收集器”,通过该接口进行所有分配,并让该类处理所分配对象的所有引用计数?而不是强迫对象本身处理垃圾回收任务,这与它们指定的目的完全无关。 - Lundin
1
您也可以将析构函数设置为 protected,来禁止对您的对象进行静态和栈分配。 - Game_Overture

7

你可以这样做。但是,你不能对其进行赋值。因此,你为此所陈述的原因,“我想改变视图”,似乎非常值得质疑。在我看来,更好的方法是让持有视图的对象替换该视图。

当然,你正在使用RAII对象,因此实际上根本不需要调用delete...对吧?


4
这是一个旧问题,已经有答案了,但@Alexandre问道“为什么有人想这么做?”,我想提供一个我今天下午考虑的用例。
遗留代码。使用裸指针Obj* obj,并在末尾使用delete obj。
不幸的是,有时我需要让对象保持更长时间的生命。
我正在考虑将其制作成引用计数智能指针。但是,如果我要在所有地方都使用ref_cnt_ptr ,那么需要改变的代码将非常多。而且如果你混合使用裸的Obj*和ref_cnt_ptr,即使还有Obj*存在,当最后一个ref_cnt_ptr消失时,对象也可能被隐式删除。
因此,我考虑创建一个explicit_delete_ref_cnt_ptr。也就是说,一个引用计数指针,在显式删除例程中才会执行删除操作。将其用于现有代码知道对象生命周期的唯一位置,以及我新代码中保持对象存活更长时间的位置。
随着explicit_delete_ref_cnt_ptr的操作而增加和减少引用计数。
但是在explicit_delete_ref_cnt_ptr析构函数中看到引用计数为零时,不要释放。
只有在类似于显式删除的操作中看到引用计数为零时,才进行释放。例如,在以下内容中:
template<typename T> class explicit_delete_ref_cnt_ptr { 
 private: 
   T* ptr;
   int rc;
   ...
 public: 
   void delete_if_rc0() {
      if( this->ptr ) {
        this->rc--;
        if( this->rc == 0 ) {
           delete this->ptr;
        }
        this->ptr = 0;
      }
    }
 };

好的,类似这样。如果一个引用计数指针类型在其析构函数中不自动删除指向的对象,这有点不寻常。但这似乎可以使裸指针和引用计数指针混合使用更加安全。

但到目前为止,还没有需要使用delete this的情况。

但是,随后我想到:如果所指向的对象知道它正在被引用计数,例如计数在对象内部(或其他表格中),则delete_if_rc0例程可以成为指向对象的方法,而不是(智能)指针的方法。

class Pointee { 
 private: 
   int rc;
   ...
 public: 
   void delete_if_rc0() {
        this->rc--;
        if( this->rc == 0 ) {
           delete this;
        }
      }
    }
 };

实际上,这个方法完全不需要成为类的成员方法,而是可以作为自由函数使用:

map<void*,int> keepalive_map;
template<typename T>
void delete_if_rc0(T*ptr) {
        void* tptr = (void*)ptr;
        if( keepalive_map[tptr] == 1 ) {
           delete ptr;
        }
};

(顺便说一句,我知道代码不太对——如果我添加所有细节,它会变得不那么易读,所以我就像这样保留了它。)

-1

只要对象在堆中,删除操作是合法的。 你需要确保对象只存在于堆中。 唯一的方法是将析构函数设置为受保护的 - 这样 delete 操作只能从类内部调用,因此你需要一个方法来确保删除。


2
请注意,将dtor设置为protected并不能确保对象仅使用new运算符创建。它可能是malloc+operator new()或其他方式,在这种情况下,delete this;会导致未定义的行为。 - Ayxan Haqverdili
1
auto foo = new Foo[1]; 现在 delete this; 应该是 delete[] this;。如果有 auto foo = new Foo[2];,那么 foo[1] 不能使用 delete[] this;delete this;。重点是,delete this; 需要一定的纪律性,如果犯了错误,编译器无法帮助强制执行。程序员可以这样做,只需要小心谨慎即可。 - Eljay
@Eljay,这表明这种情况只可能发生在实施了这种纪律的环境中。例如,创建和删除将在基础设施的一个地方实施。 - Swift - Friday Pie
在使用placement new时,任何delete都会导致未定义行为。因为这将需要访问析构函数来创建临时对象,从而暗示了对象的创建。如果程序员使用了placement new,我们必须至少给予他们一些合理性的信用。 - Swift - Friday Pie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接