Home > Mobile >  Restoring a C stream's exception mask for caller
Restoring a C stream's exception mask for caller

Time:04-22

I am writing a C function that takes a std::istream as an argument and reads from it to decode an image. When decoding the image, I want the stream to throw exceptions if some error occurs during reading. That way, I don't have to intersperse checking the error flags with the rest of my decoding logic, making my code simpler.

Because the caller might pass a stream without exceptions enabled, my function will enable them. I would like to restore the stream's initial exception mask when the function exits. I am just learning C and am trying to wrap my head around the idiomatic way to handle situations like this - where I'd normally toss cleanup code in a finally block:

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);
        }
}

I've seen people say that finally-type cleanup code should be handled like RAII. I guess I could create a little class to wrap this responsibility, which saves a pointer to the stream and the original exception mask. Its destructor would restore the original exception mask to the stream.

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

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

While that will work in my case, it would be problematic to use this class to store an exception mask that specifies exceptions should be thrown for use in a function that disables exceptions. If the class was used that way, the destructor would reenable the exceptions, which immediately throws whatever exceptions are implied by the current state.

I realize this is a bit contrived - probably the caller won't use a messed up stream anymore - but I am still confused about how I'd address the following problems:

  1. How do I write code that uses a stream supplied by a caller, knowing that the stream could have totally different behavior depending on what the exception mask is?
  2. How do you write cleanup code that could throw? If I was using a finally I could catch an exception thrown from within in the finally block and do something appropriate. But if I am spinning off the responsibility for cleanup to another class to allow RAII, I have to either let its destructor throw exceptions, or swallow them.

CodePudding user response:

If the caller doesn't want stream exceptions, the old mask will not enable exceptions, so the restoration of the old mask will not throw any exception. Not a problem.

If the caller does want stream exceptions, then the restoration will throw an exception if the stream state matches what the caller wants an exception for. Again, not a problem.

So, the only real problem is the possibility of throwing an exception from inside an RAII destructor, which is generally very bad (but can be done with care).

In this case, I would suggest just not using RAII. A simple try/catch will suffice (try/finally is not portable), eg:

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);
}
  • Related