![]() |
1
177
从析构函数中抛出异常是危险的。
这主要归结为: 任何危险的(即可能引发异常的)都应该通过公共方法(不一定直接)来完成。然后,类的用户可以通过使用公共方法和捕获任何潜在的异常来潜在地处理这些情况。 然后,析构函数将通过调用这些方法(如果用户没有显式地这样做)来完成对象,但会捕获并删除抛出的任何异常(在尝试修复问题之后)。 所以实际上,您将责任传递给用户。如果用户能够纠正异常,他们将手动调用适当的函数并处理任何错误。如果对象的用户不担心(因为对象将被销毁),那么析构函数将被留下来处理业务。 一个例子:STD:FFSH close()方法可能会引发异常。 如果文件已打开,析构函数将调用close(),但要确保任何异常不会从析构函数中传播出去。 因此,如果文件对象的用户希望对与关闭文件相关联的问题进行特殊处理,他们将手动调用close()并处理任何异常。另一方面,如果他们不在乎,那么析构函数将被留下来处理这种情况。 Scott Myers在他的《有效C++》一书中有一篇关于这门学科的优秀文章。 编辑:
显然也在“更有效的C++”中。
|
![]() |
2
49
抛出析构函数可能导致崩溃,因为此析构函数可能作为“堆栈展开”的一部分调用。 堆栈展开是在引发异常时发生的过程。 在此过程中,将终止自“Try”以来一直推到堆栈中直到引发异常的所有对象--将调用它们的析构函数。 在这个过程中,不允许另一个异常抛出,因为一次不能处理两个异常,因此,这将引发调用abort(),程序将崩溃,控件将返回操作系统。 |
![]() |
3
43
我们必须 区分 这里不是盲目跟随 一般的 建议 具体的 病例。 请注意以下内容 忽视 对象容器的问题,以及面对容器内的多个对象时要做什么。(而且可以部分忽略,因为有些对象不适合放入容器中。) 当我们将类分成两种类型时,整个问题变得更容易思考。一个类DTOR可以有两个不同的职责:
如果我们以这种方式看待这个问题,那么我认为可以这样说:(r)语义不应该引起DTOR的异常,因为有a)我们对此无能为力;b)许多自由资源操作甚至不提供错误检查,例如。
具有(c)语义的对象,如需要成功刷新其数据的文件对象,或在DTOR中执行提交的(“范围保护”)数据库连接属于不同类型:我们 可以 对错误(在应用程序级别)做些什么,我们真的不应该像什么都没有发生一样继续。
如果我们遵循raii路径,并允许对象在其驱动程序中具有(c)语义,那么我们还必须考虑这样的驱动程序可以抛出的奇怪情况。因此,您不应该将这些对象放入容器中,而且程序仍然可以
关于错误处理(提交/回滚语义)和异常,有一个很好的说法 Andrei Alexandrescu : Error Handling in C++ / Declarative Control Flow (举行) NDC 2014 )
在细节部分,他解释了愚蠢的图书馆如何实现
(我应该注意到 others 也有类似的想法。) 虽然谈话的重点不在于从老师那里扔东西,但它展示了一种可以使用的工具 今天 为了摆脱 problems with when to throw 从一个Dor。
在
UPD 17:C++的17个STD特性是
|
![]() |
4
19
关于从析构函数抛出的真正问题是,“调用程序能用这个做什么?”对于这个例外,您实际上可以做什么有用的事情来抵消从析构函数抛出所产生的危险吗?
如果我毁灭了
|
![]() |
5
12
它很危险,但从可读性/代码可理解性的角度来看,它也没有意义。 在这种情况下你要问的是
什么应该捕获异常?打电话给foo的应该吗?或者foo应该处理它?为什么foo的调用者应该关心foo内部的一些对象?可能有一种语言定义这一点的方式是有意义的,但它将是不可读的,很难理解。 更重要的是,对象的内存在哪里?对象所拥有的内存在哪里?它是否仍然被分配(表面上是因为析构函数失败)?还要考虑对象在 堆栈空间 很明显,不管怎样它都消失了。 那么考虑一下这个案子
当删除obj3失败时,我该如何以保证不会失败的方式实际删除?这是我的记忆! 现在考虑在第一个代码片段中,对象自动消失,因为它在堆栈上,而对象3在堆上。因为指向object3的指针不见了,所以你有点像sol。内存泄漏。 现在一个安全的方法是
也看到这个 FAQ |
![]() |
6
11
从ISO草案C++(ISO/IEC JTC 1/SC 22 N 4411) 因此,析构函数通常应该捕获异常,而不是让它们从析构函数中传播出去。
|
![]() |
7
7
您的析构函数可能正在其他析构函数链中执行。抛出一个没有被即时调用程序捕获的异常可能会使多个对象处于不一致的状态,从而导致更多的问题,然后忽略清理操作中的错误。 |
![]() |
8
5
其他人都解释了为什么投掷毁灭者是可怕的…你能怎么办?如果正在执行可能失败的操作,请创建一个单独的公共方法来执行清理,并可以抛出任意异常。在大多数情况下,用户将忽略这一点。如果用户想要监视清理的成功/失败,他们可以简单地调用显式清理例程。 例如:
|
![]() |
9
5
作为对主要答案的补充,这些答案是好的、全面的和准确的,我想对您所引用的文章发表评论,即“在析构函数中抛出异常并不是那么糟糕”。 本文采用了“抛出异常的备选方案是什么”这一行,并列出了每个备选方案的一些问题。这样做的结果是,因为我们找不到一个无问题的替代方案,所以我们应该继续抛出异常。 问题在于,它列出的与备选方案相关的问题中,没有一个比异常行为更糟糕,让我们记住,异常行为是“程序的未定义行为”。作者的一些反对意见包括“在美学上丑陋”和“鼓励不良风格”。现在你想要哪一个?一个风格不好的程序,或者表现出未定义行为的程序? |
![]() |
10
3
我所在的团队认为,在许多情况下,在析构函数中抛出“范围保护”模式非常有用,特别是在单元测试中。但是,请注意,在C++ 11中,抛出析构函数会导致调用
Andrzej Krzemie_ ski在“投掷的毁灭者”这一主题上有一篇很好的文章:
他指出C++ 11有一个机制来重写默认值。
最后,如果您确实决定抛出析构函数,那么应该始终注意双异常的风险(在堆栈因异常而被释放时抛出)。这会导致
|
![]() |
11
2
A:有几种选择:
但是,扔面包片好吗? 我同意上面大多数的观点,即抛掷最好避免在析构函数中,在可能的地方。但有时候你最好接受它的发生,并处理好它。我选择上面的3个。 有一些奇怪的情况 好主意 从析构函数中抛出。 比如“必须检查”的错误代码。这是从函数返回的值类型。如果调用方读取/检查包含的错误代码,则返回的值将自动销毁。 但是 ,如果返回值超出范围时未读取返回的错误代码,则会引发一些异常, 从它的析构函数 . |
![]() |
12
1
我目前遵循的策略(很多人都这么说)是类不应该主动地从其析构函数中抛出异常,而是应该提供一个公共的“close”方法来执行可能失败的操作… …但是我相信容器类型类的析构函数,像向量一样,不应该掩盖从它们包含的类中抛出的异常。在本例中,我实际上使用了一个递归调用自身的“free/close”方法。是的,我递归地说。有一种方法可以解决这种疯狂。异常传播依赖于存在一个堆栈:如果发生单个异常,那么其余的析构函数都将继续运行,而挂起的异常将在例程返回后传播,这很好。如果出现多个异常,那么(取决于编译器)第一个异常将传播,或者程序将终止,这是正常的。如果出现了如此多的异常,以至于递归溢出了堆栈,那么就出现了严重的错误,有人会发现它,这也是可以的。就我个人而言,我错误的一面是错误的爆发,而不是被隐藏、秘密和阴险。 关键是容器保持中立,由所包含的类决定它们在从析构函数中抛出异常时的行为或行为是否不当。 |
![]() |
13
1
MartinBA(上面)是正确的——您为发布和提交逻辑设计了不同的架构。 释放:你应该容忍任何错误。您正在释放内存、关闭连接等。系统中的任何其他人都不应该再看到这些内容,而您正在将资源交回操作系统。如果在这里看起来您需要真正的错误处理,它可能是您的对象模型中设计缺陷的结果。 提交:在这里,您需要与std::lock_guard等东西为互斥提供的相同类型的raii包装器对象。对于那些您根本不将提交逻辑放在DTOR中的对象。您有一个专门的API,然后包装对象,这些对象将在DTORS中提交它并在那里处理错误。记住,您可以在析构函数中捕获异常,这很好;它发出的异常是致命的。这还允许您通过构建一个不同的包装器来实现策略和不同的错误处理(例如,std::unique_lock与std::lock_guard),并确保您不会忘记调用提交逻辑——这是将其放在DTOR第一位的唯一半途而废的正当理由。 |
![]() |
14
0
设置报警事件。通常,警报事件是清理对象时通知故障的更好形式。 |
![]() |
15
0
与构造函数不同,抛出异常是指示对象创建成功的有用方法,不应在析构函数中抛出异常。 在堆栈展开过程中,如果从析构函数抛出异常,则会出现此问题。如果发生这种情况,编译器将处于不知道是继续堆栈展开过程还是处理新异常的情况。最终结果是您的程序将立即终止。 因此,最好的做法就是完全避免在析构函数中使用异常。改为将消息写入日志文件。 |
![]() |
16
0
主要问题是:你不能 不能失败 . 失败到底意味着什么?如果向数据库提交事务失败,并且失败(回滚失败),那么数据的完整性会怎么样? 因为析构函数是为正常和异常(失败)路径调用的,所以它们本身不能失败,否则我们将“失败”。 这是一个概念上的难题,但通常的解决方法是找到一种确保失败不会失败的方法。例如,数据库可能会在提交到外部数据结构或文件之前写入更改。如果事务失败,则可以丢弃文件/数据结构。然后,它必须确保从外部结构/文件提交更改是不会失败的原子事务。
对我来说,最合适的解决方案是编写非清理逻辑,这样清理逻辑就不会失败。例如,如果您试图创建一个新的数据结构来清理现有的数据结构,那么您可能会寻求提前创建该辅助结构,这样我们就不再需要在析构函数中创建它。 诚然,说起来容易做起来难,但这是我看到的唯一正确的方式。有时我认为应该有能力为远离异常路径的正常执行路径编写独立的析构函数逻辑,因为有时析构函数感觉有点像它们通过尝试处理这两个路径而拥有双倍的责任(例如,需要显式解除的范围保护;如果它们能够ld将异常破坏路径与非异常破坏路径区分开来)。 然而,最终的问题是我们不能失败,这是一个很难在所有情况下完美解决的概念设计问题。如果你不太习惯于复杂的控制结构,大量的微小物体相互作用,而是用稍微大一点的方式来模拟你的设计(例如:粒子系统用一个析构函数来破坏整个粒子系统,而不是每个粒子一个单独的非平凡的析构函数),那就容易多了。当您在这种更粗糙的层次上建模您的设计时,可以处理的非平凡析构函数更少,而且还可以承担确保析构函数不会失败所需的任何内存/处理开销。 自然,这是最简单的解决方案之一,就是减少使用析构函数的次数。在上面的粒子例子中,也许在销毁/移除一个粒子时,应该做一些可能由于任何原因而失败的事情。在这种情况下,不需要通过粒子的DTOR调用这样的逻辑,而可以在异常路径中执行,相反,当粒子系统 移除 粒子。移除粒子可能总是在非异常路径中进行。如果系统被破坏,它可能只会清除所有粒子,而不会干扰可能失败的单个粒子移除逻辑,而可能失败的逻辑仅在粒子系统正常执行期间执行,当它移除一个或多个粒子时。 如果避免使用非平凡的析构函数处理大量的微小对象,通常会出现类似这样的解决方案。当你陷入一团混乱之中,似乎不可能成为例外的时候,安全性就是当你被许多微小的物体缠住,这些物体都有不平凡的数据终端。 如果指定它的任何东西(包括应继承其基类的noexcept规范的虚拟函数)试图调用任何可能引发的东西,那么nothrow/noexcept实际上被转换为编译器错误将有很大帮助。这样,如果我们无意中编写了一个可能抛出的析构函数,我们就能够在编译时捕获所有这些东西。 |
![]() |
Post Self · std::是否可以退出泄漏内存? 8 年前 |
![]() |
OneRaynyDay · 在这种情况下,我应该使用智能指针吗? 8 年前 |
![]() |
jcai · 对象的ctor和dtor必须在同一个线程上吗? 8 年前 |
![]() |
Stradigos · 防止解构宏中定义的匿名变量,直到作用域结束 8 年前 |
![]() |
user877329 · 通用句柄类 9 年前 |