Search code examples
c++exceptiondestructoriostreamraii

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


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.

Solution

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