为什么C ++中没有“最终”构造?


57

C ++中的异常处理仅限于try / throw / catch。与Object Pascal,Java,C#和Python不同,即使在C ++ 11中,该finally构造也尚未实现。

我已经看到很多有关“异常安全代码”的C ++文献。Lippman写道,异常安全代码是一个重要但高级而又困难的话题,超出了他的Primer的讨论范围-这似乎意味着安全代码不是C ++的基础。赫伯·萨特(Herb Sutter)在他的Exceptional C ++中为该主题投入了10章!

但是在我看来,如果finally实现了该构造,尝试编写“异常安全代码”时遇到的许多问题都可以很好地解决,从而使程序员可以确保即使在发生异常的情况下也可以恢复程序到安全,稳定,无泄漏的状态,接近资源分配和潜在问题代码的地步。作为一个经验丰富的Delphi和C#程序员,我使用try ..最终像大多数使用这些语言的程序员一样,在我的代码中广泛地进行了阻塞。

考虑到C ++ 11中实现的所有“风声”,我惊讶地发现“最终”仍然不存在。

那么,为什么finally从未在C ++中实现该构造呢?实际上,这不是一个很难理解或高级的概念,并且可以帮助程序员编写“异常安全代码”。


25
为什么最后不呢?因为您释放了析构函数中的东西,当对象(或智能指针)离开作用域时,析构函数会自动触发。析构函数优于finally {},因为它将工作流程与清除逻辑分开。就像您不想调用free()那样,以垃圾收集的语言使工作流程混乱。
mike30 2013年


8
问一个问题:“为什么finally在C ++中没有,为什么使用了异常处理技术?” 是有效的,并且是该网站的主题。我认为现有的答案很好地涵盖了这一点。将其变成“关于C ++设计人员不finally值得包括的理由吗?”的讨论。和“应该finally添加到C ++吗?” 并针对问题的评论进行讨论,每个答案都不符合此问答网站的模型。
乔什·凯利

2
如果最后,您已经有关注点分离了:主代码块在这里,清理关注点在这里得到了解决。
卡兹(Kaz)

2
@Kaz。区别是隐式清理与显式清理。析构函数为您提供自动清理的功能,类似于从堆栈弹出时如何清除普通的旧基元。您无需进行任何明确的清理调用,而可以专注于您的核心逻辑。想象一下,如果必须在try / finally中清理堆栈分配的原语,那将是多么令人费解。隐式清理效果更好。类语法与匿名函数的比较不相关。尽管通过将一流的函数传递给释放句柄的函数可以集中进行手动清理。
mike30

Answers:


56

作为对@Nemanja答案的一些补充评论(由于它引用了Stroustrup,因此实际上是您所能获得的最佳答案):

这实际上只是了解C ++的原理和惯用语的问题。以您的操作为例,该操作在持久性类上打开数据库连接,并且必须确保在引发异常时关闭该连接。这是一个异常安全问题,适用于任何带有异常的语言(C ++,C#,Delphi ...)。

在使用try/ 的语言中finally,代码可能看起来像这样:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

简单明了。但是,有一些缺点:

  • 如果该语言没有确定性的析构函数,则始终必须编写该finally块,否则会浪费资源。
  • 如果DoRiskyOperation不止是一个方法调用-如果我在try块中有一些处理要做-那么该Close操作可能会与该Open操作相去甚远。我无法在购置文件旁边写上我的清理记录。
  • 如果我需要获取多个资源,然后以异常安全的方式释放它们,那么我可以在try/ finally块深处获得几层。

C ++方法如下所示:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

这完全解决了该finally方法的所有缺点。它有一些缺点,但相对较小:

  • 您很有可能需要自己编写ScopedDatabaseConnection该类。但是,这是一个非常简单的实现-仅4到5行代码。
  • 它涉及到创建一个额外的局部变量-基于您对“不断创建和销毁类以依赖其析构函数来清理您的混乱情况的意见,这是非常可怜的”的评论,您显然不喜欢它-但是一个好的编译器会优化排除额外的局部变量所涉及的任何额外工作。良好的C ++设计在很大程度上取决于这类优化。

就个人而言,考虑到这些优点和缺点,我发现RAII是一种更可取的技术finally。你的旅费可能会改变。

最后,由于RAII在C ++中是一个非常成熟的习惯用法,并且为了减轻开发人员编写大量Scoped...类的负担,因此像ScopeGuardBoost.ScopeExit这样的库可以促进这种确定性清除。


8
C#具有该using语句,该语句自动清除实现该IDisposable接口的所有对象。因此,尽管有可能将其弄错,但很容易将其弄对。
罗伯特·哈维

18
必须编写一个全新的类来处理临时状态更改,使用由编译器通过try/finally构造实现的设计习惯,因为编译器不会公开try/finally构造,并且访问它的唯一方法是通过基于类的构造设计成语,不是“优势”;这是抽象反转的定义。
梅森·惠勒

15
@MasonWheeler-嗯,我不是说必须写一个新类是一个优势。我说这是一个缺点。在资产,不过,我更喜欢RAII到使用finally。就像我说的,您的里程可能会有所不同。
乔什·凯利

7
@JoshKelley:“好的C ++设计在很大程度上取决于这类优化。” 编写无关紧要的代码然后依赖编译器优化的方法是Good Design吗?IMO是良好设计的对立面。好的设计的基本要素是简洁易读的代码。更少的调试,更少的维护等等等。您应该编写大量的代码,然后依靠编译器将其全部删除-IMO毫无意义!
矢量

14
@Mikey:那么在代码库中复制清除代码(或必须进行清除的事实)是“简洁”和“易于阅读”的吗?使用RAII,您只需编写一次这样的代码,它就会自动应用于所有地方。
曼卡斯

54

为什么不是C ++提供了“终于”建设?Bjarne的Stroustrup的C ++风格和手法常见问题

因为C ++支持几乎总是更好的替代方法:“资源获取是初始化”技术(TC ++ PL3第14.4节)。基本思想是用本地对象表示资源,以便本地对象的析构函数将释放该资源。这样,程序员就不会忘记释放资源。


5
但是关于C ++的技术没有什么,是吗?您可以使用对象,构造函数和析构函数的任何语言来进行RAII。尽管 Strousup在说什么,但是RAII仅存在并不意味着该finally结构永远永远无用,这是一个很棒的技术。事实证明,在C ++中编写“异常安全代码”很重要。哎呀,C#同时拥有析构函数和,并且被使用了。finally
Tacroy

28
@Tacroy:C ++是极少数具有确定性析构函数的主流语言之一。C#“析构函数”对此没有用,您需要手动编写“ using”块才能使用RAII。
Nemanja Trifunovic

15
@Mikey,您的答案是:“为什么C ++不提供“最终”构造?” 直接来自Stroustrup自己。你还能要求什么呢?这就是为什么。

5
@Mikey如果您担心代码表现良好,特别是不会泄漏资源,则在抛出异常时,您担心异常安全性/试图编写异常安全代码。您只是没有这么说,而且由于可用的工具不同,因此实现方式也有所不同。但这正是C ++人们在讨论异常安全性时谈论的话题。

19
@Kaz:我只需要记住在析构函数中执行一次清理,然后从那时起我就使用该对象。我需要记住,每次使用分配的操作时,都要在finally块中进行清理。
deworde 2013年

19

C ++之所以没有,finally是因为C ++中不需要它。 finally用于执行某些代码,而不管是否发生异常,这几乎总是某种清理代码。在C ++中,此清理代码应位于相关类的析构函数中,并且就像finally块一样,将始终调用该析构函数。使用析构函数进行清理的惯用法称为RAII

在C ++社区中,可能会更多地讨论“异常安全”代码,但是在具有异常的其他语言中,它几乎同样重要。“异常安全”代码的全部要点是,您考虑如果您调用的任何函数/方法中都发生异常,则代码以什么状态保留。
在C ++中,“异常安全”代码更为重要,因为C ++没有自动垃圾收集功能来处理由于异常而被孤立的对象。

在C ++社区中对异常安全性进行更多讨论的原因可能还来自于这样一个事实,即在C ++中,您必须更加了解可能出问题的地方,因为该语言中的默认安全网较少。


2
注意:请不要说C ++具有确定性的析构函数。对象Pascal / Delphi也具有确定性的析构函数,但也支持“最终”,这是出于很好的理由,我在下面的第一批评论中对此做了解释。
矢量

13
@Mikey:鉴于从未有人提议将其添加finally到C ++标准中,因此我认为可以肯定地说C ++社区不认为the absence of finally存在问题。确实有的大多数语言都finally缺乏C ++所具有的一致的确定性破坏能力。我看到Delphi兼有两者,但我对它的历史了解得不够多,以至于不知道首先出现在哪儿。
Bart van Ingen Schenau,

3
Dephi不支持基于堆栈的对象-仅支持基于堆的对象以及堆栈上的对象引用。因此,在适当时显式调用析构函数等需要“最终”。
矢量

2
C ++有很多多余的东西可以说是不需要的,所以这不是正确的答案。
卡兹(Kaz)

15
在过去的二十多年里,我一直在使用这种语言,并且与其他使用该语言的人一起工作,但我从未遇到过一位工作过的C ++程序员,他说“我真的希望这种语言具有finally”。如果我可以访问它,我将永远无法回忆起任何可以简化的任务。
Gort机器人,

12

其他人则讨论了RAII作为解决方案。这是一个完美的解决方案。但这并没有真正解决为什么它们没有添加finally得很好的原因,因为这是人们普遍希望的事情。答案是对C ++的设计和开发更根本的:在C ++的整个开发过程中,涉及这些技术的人员都强烈反对引入设计功能,而这些功能可以使用其他功能来实现,而不必大惊小怪,特别是在需要引入这些功能的情况下可能使旧代码不兼容的新关键字。由于RAII提供了一种功能强大的替代方法,finally并且您finally无论如何实际上都可以在C ++ 11中进行滚动,因此几乎没有要求。

您需要做的就是创建一个类Finally,该类在其析构函数中调用传递给其构造函数的函数。然后,您可以执行以下操作:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

通常,大多数本机C ++程序员将更喜欢设计简洁的RAII对象。


3
您在lambda中缺少参考捕获。应该Finally atEnd([&] () { database.close(); });还应该是,我认为以下更好:({ Finally atEnd(...); try {...} catch(e) {...} }我将终结器从try块中取出,使其在catch块之后执行。)
Thomas Eding 2014年

2

您可以使用“陷阱”模式-即使您不想使用try / catch块。

将一个简单的对象放在所需的范围内。在该对象的析构函数中放入“最终”逻辑。无论如何,当堆栈解开时,将调用对象的析构函数,您将得到糖果。


1
这不回答这个问题,并简单地证明,最后是不是一个坏主意毕竟...
矢量

2

好吧,您可以finally使用Lambdas进行一些改进,从而可以很好地进行编译(当然,请使用没有RAII的示例,而不是最好的代码):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

看到这篇文章


-2

我不确定我是否同意RAII是的超集的说法finally。RAII的致命弱点很简单:例外。RAII是用析构函数实现的,而在C ++中,抛弃析构函数总是错误的。这意味着当需要清除代码时,您将无法使用RAII。finally另一方面,如果得到了实施,则没有理由认为将其扔掉是不合法的finally

考虑这样的代码路径:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

如果有的话,finally我们可以写:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

但是我无法找到使用RAII获得等效行为的方法。

如果有人知道如何用C ++做到这一点,我对答案很感兴趣。我什至对某些依赖的东西感到满意,例如,强制所有继承自具有某个特殊功能或某些功能的类的异常。


1
在第二个示例中,如果complex_cleanup可以抛出,则可能会遇到两个未捕获的异常同时发生的情况,就像使用RAII /析构函数一样,而C ++拒绝这样做。如果您希望看到原始的异常,complex_cleanup则应防止任何异常,就像RAII /析构函数一样。如果您希望complex_cleanup看到例外,那么我认为您可以使用嵌套的try / catch块-尽管这是一个切线并且很难放入注释中,所以值得单独提出一个问题。
2015年

我想使用RAII来获得与第一个示例相同的行为,并且更加安全。推定finally块中的抛出显然与catchWRT 正在进行中的异常-not call 中的块抛出相同std::terminate。问题是“为什么finally在C ++中不行?” 答案都说“您不需要... RAII FTW!” 我的观点是,RAII适用于诸如内存管理之类的简单情况,但是在解决异常问题之前,它需要太多的思考/开销/关注/重新设计才能成为通用解决方案。
MadScientist 2015年

3
我理解您的观点-可能会引发析构函数的一些合法问题-但这很少见。说RAII +异常有未解决的问题,或者说RAII不是通用解决方案,根本不符合大多数C ++开发人员的经验。
2015年

1
如果发现自己需要在析构函数中引发异常,那么您就在做错什么-可能在不需要它们的其他地方使用了指针。
矢量

1
这太涉及评论了。提出一个问题:您如何使用RAII模型在C ++中处理这种情况...似乎不起作用... 再次,您应该直接输入注释:键入@和您正在谈论的成员的姓名在您的评论开头。当您在自己的帖子上发表评论时,您会收到所有通知,但其他人则不会,除非您向他们发送评论。
矢量
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.