为调用者恢复 C++ 流的异常掩码

Restoring a C++ stream's exception mask for caller

我正在编写一个 C++ 函数,它将 std::istream 作为参数并从中读取以解码图像。解码图像时,如果在读取过程中发生某些错误,我希望流抛出异常。这样,我就不必在解码逻辑的其余部分穿插检查错误标志,从而使我的代码更简单。

因为调用者可能会在没有启用异常的情况下传递流,所以我的函数将启用它们。我想在函数退出时恢复流的初始异常掩码。我只是在学习 C++,并试图用惯用的方式来处理这样的情况——我通常会在 finally 块中抛出清理代码:

Image::Image(std::istream& stream) {
        std::ios_base::iostate originalExceptionSettings = stream.exceptions();
        
        try {
                // Enable exceptions.
                stream.exceptions(
                        std::ios_base::badbit | 
                        std::ios_base::failbit | 
                        std::ios_base::eofbit);

                // Do some stuff that might cause stream to throw...
        }
        finally { // If only!
                // Restore settings that the caller expects.
                stream.exceptions(originalExceptionSettings);
        }
}

我看到有人说 finally 类型的清理代码应该像 RAII 一样处理。我想我可以创建一个 class 来包装这个责任,它保存了一个指向流的指针和原始异常掩码。它的析构函数会将原始异常掩码恢复到流中。

// Something like this...
StreamExceptionMaskMemo::StreamExceptionMaskMemo(std::istream* stream)
{
        this->originalExceptionSettings = stream->exceptions();
        this->stream = stream;
}

StreamExceptionMaskMemo::~StreamExceptionMaskMemo()
{
        // Could throw!
        this->stream->exceptions(this->originalExceptionSettings);
}

虽然这对我的情况有效,但使用此 class 存储异常掩码 指定应抛出异常 以用于禁用 异常的函数。如果 class 以这种方式使用,析构函数将重新启用异常,这会立即抛出当前状态暗示的任何异常。

我意识到这有点做作 - 调用者可能不会再使用混乱的流 - 但我仍然对如何解决以下问题感到困惑:

  1. 如何编写使用调用者提供的流的代码,知道该流可能具有完全不同的行为,具体取决于异常掩码是什么?
  2. 如何编写可能抛出的清理代码?如果我使用 finally 我可以捕获从 finally 块中抛出的异常并做一些适当的事情。但是,如果我将清理责任推给另一个 class 以允许 RAII,我必须要么让它的析构函数抛出异常,要么吞下它们。

如果调用者想要流异常,旧掩码将不会启用异常,所以旧掩码的恢复不会抛出任何异常。没问题。

如果调用者确实想要流异常,那么如果流状态与调用者想要的异常相匹配,恢复将抛出异常。同样,这不是问题。

因此,唯一真正的问题是可能从 RAII 析构函数内部抛出异常,这通常非常糟糕(但 小心)。

在这种情况下,我建议不要使用 RAII。一个简单的 try/catch 就足够了(try/finally 不可移植),例如:

Image::Image(std::istream& stream) {
    std::ios_base::iostate originalExceptionSettings = stream.exceptions();
        
    try {
        // Enable exceptions (may throw immediately!)
        stream.exceptions(
            std::ios_base::badbit | 
            std::ios_base::failbit | 
            std::ios_base::eofbit);

        // Do some stuff that might cause stream to throw...
    }
    catch (const std::exception &ex) {
        // error handling as needed...

        // if not a stream error, restore settings that
        // the caller expects, then re-throw the original error
        if (!dynamic_cast<const std::ios_base::failure*>(&ex))
        {
            stream.exceptions(originalExceptionSettings);
            throw;
        }
    }
    catch (...) {
        // error handling as needed...

        // Unknown error but not a stream error, restore settings
        // that the caller expects, then re-throw the original error
        stream.exceptions(originalExceptionSettings);
        throw;
    }

    // restore settings that the caller expects (may throw what caller wants!)
    stream.exceptions(originalExceptionSettings);
}