我有一个10线小函数,可使用std::ofstream将一些数据写入文件。我没有在函数末尾显式调用.close(),但是由于在样式和详细程度方面更好地显式调用它,因此代码审查失败。我知道显式调用.close()没有什么害处,但是在return语句表明对RAII缺乏理解或信任之前显式调用它会不会造成危害?

C ++标准说:


§27.8.1.2

virtual ~ basic_filebuf ();

[3]效果:销毁class basic_filebuf<charT,traits>的对象。调用close()


我证明在函数末尾调用.close()是多余的和/或不必要的吗?

bool SomeClass::saveData()
{
    std::ofstream saveFile(m_filename);

    if (!saveFile.is_open())
        return false;

    saveFile << m_member1 << std::endl;
    saveFile << m_member2 << std::endl;

    saveFile.close(); // passed review only with this line
    return true;
}


如果无法打开文件进行写入,则该函数仅应返回false

评论

如果审阅者需要保证可以自动调用close,那么C ++可能不是最佳的语言选择。 “冗长”的原因尤其令人震惊。

“代码审查失败,因为最好是出于……冗长的原因而明确地对其进行调用” –请最后解释一下,这没有任何意义。
显然,您的审阅者的风格与标准库的标准设计冲突。

@Konrad:冗长地说,我的意思是我们正在关闭文件,即使知道它在析构函数中仍然会关闭,因为它表明我们知道我们已经完成了它。在某些情况下,我们打开文件的时间很长;因此我们有一条规则,即明确关闭每个流。

@dreamlax:但是冗长如何成为优势?我同意可能是明确的,有时这需要冗长-但这始终是两者之间的权衡。我从未见过这样的情况,冗长本身就是优势。这就是我的意思,“毫无意义”。

#1 楼

我会提出完全相反的说法。

明确关闭流可能不是您想要的。这是因为当您在流中时,可能会引发异常。因此,当您显式关闭文件流时,这表示您既要关闭流又要显式处理关闭流时可能导致的任何错误(异常或错误位)(或者可能是您说这是否失败,我想要快速失败(允许杀死应用程序的例外)。您应该只让析构函数进行关闭。这是因为析构函数将捕获并丢弃任何异常,从而使代码正常流动。处理文件关闭时,这通常是您想要做的(如果关闭失败有关系吗?)。

评论


\ $ \ begingroup \ $
“如果关闭失败有关系吗?”对于输出流,我认为可能是这样,因为输出可能会一直缓冲到调用close()为止。
\ $ \ endgroup \ $
–罗迪
2011-2-2在10:40



\ $ \ begingroup \ $
@Roddy:但是您无法采取任何措施阻止失败。报告此失败可能很重要,但是不能采取其他有意义的措施。
\ $ \ endgroup \ $
–康拉德·鲁道夫(Konrad Rudolph)
2011年2月2日14:17

\ $ \ begingroup \ $
@Konrad。同意您不能阻止它的失败(但这是正确的,但有许多例外)。如果用户在GUI应用程序中选择“保存...”,然后close()抛出,则通知用户并允许他重试保存(也许到其他卷)可能非常有意义。
\ $ \ endgroup \ $
–罗迪
2011-2-2 15:11



\ $ \ begingroup \ $
@Roddy:授予。但是有一点不同:确实会发生此错误吗?我不记得曾经见过类似的东西(除非您过早地拔掉USB驱动器等。。。)
\ $ \ endgroup \ $
–康拉德·鲁道夫(Konrad Rudolph)
11年2月2日在15:16

\ $ \ begingroup \ $
@Roddy:当然,有时候确实很重要,并且您想报告它(GUI应用程序)。那么您将显式使用close()并检查错误。不管您是否关心天气都是完全根据情况而定,只有开发人员才能在使用时决定。在上面的示例中,听起来好像没有进行任何检查,因此他们不在乎。
\ $ \ endgroup \ $
–马丁·约克
2011年2月2日在17:19

#2 楼

假设fstream对象是函数的局部对象,我倾向于反对这一观点。人们需要习惯于让RAII发挥作用,并且关闭fstream对象属于该标题。不能完成有用功能的额外代码几乎总是一个糟糕的主意。它不仅无用,而且还会使所需的内容模糊不清,并且(最糟糕的)在任何情况下都根本无法实施-仅考虑功能正常退出的人们确实需要停止并意识到在他们向C ++添加异常处理的那一刻,规则发生了根本性的变化。您需要从RAII(或类似方法)的角度进行考虑,以确保在退出作用域时进行清理-明确关闭文件,释放内存等均不合格。

评论


\ $ \ begingroup \ $
抱歉,您的假设是正确的,我忘了提到fstream对于该函数而言是本地的。
\ $ \ endgroup \ $
–dreamlax
2011年2月2日,下午5:24

\ $ \ begingroup \ $
我有点想问“如果我们要手动执行析构函数,那么有什么意义...”,但我知道这不会走太远。我只是为了满足代码审查而提出了明确的要求,但我感到非常不愿意。
\ $ \ endgroup \ $
–dreamlax
2011年2月2日,下午6:41

\ $ \ begingroup \ $
@dreamlax:它为您提供了灵活性。在大多数情况下,您不必关心close()是否有效(您无能为力(除了记录信息之外)),因此析构函数可以完美地工作。但是在这种情况下,您可以选择手动调用close()并检查关闭是否有效(如果没有发送该2AM SMS以支持告诉他们文件系统已关闭且需要立即更换),则可以选择这种方式。
\ $ \ endgroup \ $
–马丁·约克
2011-2-2在8:09



\ $ \ begingroup \ $
重新。例外:下一步(无效)的逻辑步骤是放入try / catch构造,以便在引发异常时仍会调用不必要的“ close()”函数。
\ $ \ endgroup \ $
–罗迪
11年2月2日在10:54

\ $ \ begingroup \ $
项目经理给出的一个原因是,在文件句柄稀缺的压力条件下,我们需要在处理完文件后立即关闭它们。通过创建一个规则,无论功能多么琐碎,您都关闭所有流,从而迫使开发人员考虑对象的生存期。
\ $ \ endgroup \ $
–dreamlax
2011-2-3在2:54

#3 楼

这里有中间立场。审阅者希望使用明确的close()“作为样式和详细程度的问题”的原因是,没有它,他们无法仅通过阅读代码来判断您是否打算这样做,或者您是否完全忘记了它而只是幸运。至少在一开始,他们的自负还很可能因为没有注意到或记住close()将被析构函数调用而受到挫伤。添加一个析构函数调用close()的注释不是一个坏主意。有点免费,但是如果您的同事现在需要澄清和/或保证,那么几年以后,也很可能会有一名随机的维护者,尤其是如果您的团队不执行大量文件I / O的时候。 br />

评论


\ $ \ begingroup \ $
不应有任何“幸运”的事情。 C ++中的资源。应该有“公正的工作”。这就是RAII的设计方式,以及正确使用后的工作方式。
\ $ \ endgroup \ $
–塞巴斯蒂安·雷德尔(Sebastian Redl)
14年2月24日在16:28

\ $ \ begingroup \ $
我不同意。这是C ++,如果您需要注释来提醒您fstream会自行关闭,则您不是C ++程序员。
\ $ \ endgroup \ $
– jliv902
2014年3月25日22:30在

#4 楼

我同意Loki的回答:显式调用close和让析构函数调用关闭之间的区别是,析构函数将隐式捕获(即隐藏)由close引发的任何异常。 (不传播异常),因为如果已经抛出异常,则可以调用它;并在第一个异常期间引发第二个异常是致命的(因此,所有析构函数都应避免引发异常)。

与Loki不同,我会辩称您确实要显式调用close,正是因为您确实想要异常从接近可见。例如,也许数据很重要,并且您希望将其写入磁盘。可能是磁盘已满,将<<输出操作符写入了内存中的高速缓存,没有人注意到磁盘已满,直到close隐式调用了flush。不允许返回false,因为false被定义为意味着无法打开文件。 IMO唯一可以做的明智/安全的事情就是抛出异常。

由调用者捕获任何异常;这是由调用者捕获的。不会抛出异常,这可以确保关闭成功并将数据安全地写入操作系统。

#5 楼

我被这个撕了。你是绝对正确的。但是,如果编码标准要求显式调用close(),或者这样做是一群人的共识,那么您就无能为力了。如果我是你,我会顺其自然。争论这样的事情是徒劳的。

评论


\ $ \ begingroup \ $
小组达成共识(即开发团队)的问题。这通常是错误的,因为该组被最大声的声音所左右。为了使小组达成共识,小组必须足够大,以便大声的个人可以被很多人的正确决定所抵消(这就是SO之所以起作用的原因(大声的人可能会获得最初的投票,但最终通常会得到正确的答案) (最终)))。
\ $ \ endgroup \ $
–马丁·约克
2011年2月2日于7:57

\ $ \ begingroup \ $
@Martin:很好的嵌套括号。
\ $ \ endgroup \ $
– GManNickG
2011年2月3日,0:32

\ $ \ begingroup \ $
@grokus我同意Tux-D的观点。但是,我不相信委员会的设计/审查。如果有人有权决定这些类型的标准,则需要以其他方式展示他们或证明他们不正确。此策略将完全取决于个人。在大型会议中唯一会发生的事情是争论,直到调解员介入并最终为您做出决定后,音量最终会增加。对此的最佳响应是构建一个应用程序,在该应用程序中调用.close()并抛出该异常。显示它杀死了应用程序。然后给他们一个选择,处理所有错误或使用RAII。
\ $ \ endgroup \ $
– Andrew T Finnell
2011-9-4 13:51



#6 楼

我相信您会一并提出两个问题。您应该使用异常还是返回值?

如果您的公司不允许例外,那么必须为您的项目全局设置fstream::exceptions()。而且您也必须询问错误标志。

如果公司不允许RAII,则不要使用C ++。如果使用RAII,则将吞噬析构函数中抛出的任何异常。这听起来很糟糕,确实如此。但是,我与其他人一致认为,RAII是可行的方法,因为此处的任何错误处理都是徒劳的,并且会创建无法读取的代码。这是因为“冲洗”不会执行您可能认为的操作。它指示操作系统代表您执行此操作。操作系统会在认为方便时执行此操作。然后,当操作刷新(可能在函数返回后一分钟)时,在硬件级别可能会发生类似的情况。磁盘可能具有SSD缓存,稍后将其刷新到旋转磁盘(可能在晚上不那么忙时发生)。最后,数据在磁盘上结束。但是故事还没有结束。数据可能已经正确保存,但是磁盘由于多种可能的原因而被破坏。因此,如果RAII在事务上对您来说不够安全,那么无论如何您都需要进入较低的API级别,即使那样也不是完美的。很抱歉成为这里的聚会警察。

#7 楼

您还需要检查写入操作(<<)是否成功。因此,无需检查is_open(),而只需完成整个操作系列,然后在最后检查failbit即可:正如所写的那样,更喜欢使用'\n'而不是std::endl(如我所示),然后让close()一次性全部写入-这可以显着提高速度,特别是当要写入大量行时。

#8 楼

阅读完问题和答案后,我得出的结论是,评论可以发挥作用。我最近读过这个Q / A猜数字,但有关的评论和被接受的答案使我对这里的情况有所了解。使用注释来解释原因,让代码解释如何。在每种情况下,我都可以类似地使用您的函数作为示例:然后至少这样有人会知道您打算调用或忽略它,为什么!编写良好的代码是如何以及不需要注释的方法。