使用RAII写入文件结束标记

Use RAII for writing end of file marker?

本文关键字:结束 文件 RAII 使用      更新时间:2023-10-16

我正在创建一种文件格式,我想在其中向文件中写入一条明确的消息,指示编写器已运行完成。我过去在生成文件时遇到过问题,生成程序崩溃,文件在我没有意识到的情况下被截断,因为如果没有明确的标记,读取程序就无法检测到文件不完整。

所以我有一个用来写这些文件的类。现在,如果你有一个"打开"操作和一个"关闭"操作,你想使用RAII,所以我会把代码写在析构函数中的文件结束标记。这样用户就不会忘记。但是,在由于抛出异常而导致写入未完成的情况下,析构函数仍将运行——在这种情况下,我们不想写入消息,这样读者就会知道文件不完整。

这似乎是任何时候都可能发生的"提交"操作。您想要RAII,这样您就不会忘记提交,但当发生异常时,您也不想提交。这里的诱惑是使用std::uncaught_exceptions,但我认为这是一种代码气味。

通常的解决方案是什么?只是要求人们记住吗?我担心每次有人试图使用我的API时,这都会成为一个绊脚石。

解决这个问题的一种方法是实现一个简单的框架系统,在该系统中,您可以定义一个只有在写入结束时才能完全填充的头。包括一个SHA256散列,使标头对验证文件内容有用。这通常比必须读取文件末尾的字节要方便得多。

在实现方面,您编写一个故意将某些字段置零的标头,在通过哈希方法提供数据的同时编写有效负载的内容,然后查找回标头并用最终值重写它。该文件一开始处于明显无效的状态,如果所有操作都已完成,则最终仅有效

您可以将所有这些封装在一个流句柄中,该句柄处理实现细节,因此就调用代码而言,它只是打开一个常规文件。如果标头不完整或无效,您的读取版本将引发异常。

对于您的示例,如果添加一个commit方法,RAII似乎可以正常工作,该方法是类的用户在完成对文件的写入时调用的。

class MyFileFormat {
public:
    MyFileFormat() : committed_(false) {}
    ~MyFileFormat() {
        if (committed_) {
            // write the completion footer (I hope this doesn't throw!)
        }
        // close the underlying stream...
    }
    bool open(const std::string& path) {
        committed_ = false;
        // open the underlying stream...
    }
    bool commit() {
        committed_ = true;
    }
};

完成后,用户有责任调用commit,但至少可以确保资源已关闭。

有关此类情况的更通用模式,请查看ScopeGuards。

ScopeGuards会将清理的责任从类中移出,并且可以用于在ScopeGuard超出范围并在被明确解除之前被销毁的情况下指定任意的"清理"回调。在您的情况下,您可以扩展这个想法,以支持失败清理(例如关闭文件句柄)和成功清理(例如写入完成页脚和关闭文件句柄的回调)。

我通过写入临时文件处理过类似的情况。即使要附加到文件,也要附加到该文件的临时副本。

在析构函数中,您可以检查std::uncaught_exception()来决定是否应该将临时文件移动到其预期位置。