RAII way to get errors that are caught during destruction
Asked Answered
A

2

5

In a typical example of RAII for File I/O on Wikipedia, any errors that occur when closing the file are swallowed:

#include <iostream>
#include <string> 
#include <fstream>
#include <stdexcept>

void write_to_file (const std::string & message) {
    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed when leaving scope (regardless of exception)
}

It seems there is no way to determine if an error occurred when file is automatically closed; obviously one can only call file.rdstate() while file is on scope.

I could call file.close() manually and then check for an error, but I would have to do that at every place I return from the scope, which defeats the purpose of RAII.

Some have commented that only unrecoverable errors like file system corruption can occur within the destructor, but I don't believe that's true because AFAIK the destructor flushes the file before closing it, and recoverable errors could occur while flushing.

So is there a common RAII way to get errors that occur during destruction? I read that throwing exceptions from destructors is dangerous so that doesn't sound like the right approach.

The simplest way I can think of is to register a callback function that the destructor will call if any errors occur during destruction. Surprisingly it doesn't seem like there is an event for this supported by ios_base::register_callback. That seems like a major oversight, unless I misunderstand something.

But perhaps a callback is the most common way to get notified of errors during destruction in modern class designs?

I assume calling an arbitrary function in a destructor is also dangerous, but perhaps wrapping the call in a try/catch block is completely safe.

Apostrophize answered 20/2, 2018 at 8:23 Comment(10)
You have a classic XY problem. A file not being able to close often means an unrecoverable error, i.e filesystem corruption. How would you handle that error or recover from it? The logical thing to do is simply to bubble the exception to a global logger.Ruskin
What destruction? You don't have a class here. And what's the mutex for?Jammiejammin
@Ruskin in this case I want to at least show the user an error dialog stating that something went wrong when writing to the file.Apostrophize
@JiveDadson okay I got rid of the mutex from the code I copied since it's not relevant. No I don't have a class of my own here, but std::ofstream closes its file upon destruction and I'm asking about how I would handle a file close error if I design a similar class myself.Apostrophize
@Ruskin also, is all data written to file guaranteed to be flushed by the time its destructor is called? Because if not, I'm assuming that the destructor also flushes the output before closing the file, during which a recoverable error could occur.Apostrophize
You could create a wrapper class that forwards the needed operators/members to the contained file writer (the one that you can't control). Then when the wrapper is destroyed you can check if an error occurred when closing the file and act accordingly. RAII over RAIIAccomplished
@Ruskin according to https://mcmap.net/q/861912/-ofstream-doesn-39-t-flush, the destructor should be flushing, so recoverable I/O errors could occur during the destructor.Apostrophize
@Accomplished that's a good idea if I need to check for errors on ofstream itself at least, thanks!Apostrophize
I guess this piece of mine would be relevant: #130617Lynea
File streams uses std::basic_filebuf<...> whose destructor calls close which indeed flushes the buffer.Acoustic
M
4

You might partially handle the case of failure in destructor:

class Foo {
public:
    Foo() : count(std::uncaught_exceptions()) {}
    ~Foo() noexcept(false)
    {
        if (std::uncaught_exceptions() != count) {
            // ~Foo() called during stack unwinding
            // Cannot throw exception safely.
        } else {
            // ~Foo() called normally
            // Can throw exception
        }
    }
private:
    int count;
};
Manrope answered 20/2, 2018 at 9:27 Comment(4)
I wonder why there isn't a language feature to automatically swallow exceptions thrown by the destructor during stack unwinding?Apostrophize
Back in C++98, we were not able to implement this conditionally throwing d'tor (except by accessing platform internals). Now, we can, but should we? See this GotW section The Wrong Solution: Why the Approach Is Immoral :-)Wed
@Andy: I wonder why we don't have way to throw std::nested_exception from unwinding :-/Manrope
@ArneVogel: std::uncaught_exceptions() allows to really handle the case contrary to old std::uncaught_exception(). But I mitigate the immorality: an automatic commit/rollback seems nice. His "right solution" is mostly to not use RAII (Manually call Close()) :-( Sad that we cannot send several exceptions...Manrope
A
2

If you have specific code handling any errors from the file closure when you unwind, you could add another level of abstraction...

class MyFileHandler {
    std::ofstream& f_;
public:
    MyFileHandler(std::ofstream& f) : f_(f) {}
    ~MyFileHandler() {
        f_.close();
        // handle errors as required...
    }
    // copy and assignments elided for brevity, but should well be deleted
};

void write_to_file (const std::string & message) {
    // try to open file
    std::ofstream file("example.txt");
    MyFileHandler fileCloser(file);

    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed when leaving scope (regardless of exception)
}

Depending on your use case, you could embed the std::ofstream in the class.

Akilahakili answered 20/2, 2018 at 9:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.