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.

Andy
  • 7,885
  • 5
  • 55
  • 61
  • 1
    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. – user167921 Feb 20 '18 at 08:48
  • What destruction? You don't have a class here. And what's the mutex for? – Jive Dadson Feb 20 '18 at 08:48
  • @user167921 in this case I want to at least show the user an error dialog stating that *something* went wrong when writing to the file. – Andy Feb 20 '18 at 08:50
  • @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. – Andy Feb 20 '18 at 08:52
  • @user167921 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. – Andy Feb 20 '18 at 08:55
  • 1
    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 RAII – amc176 Feb 20 '18 at 08:58
  • @user167921 according to https://stackoverflow.com/questions/3113229/ofstream-doesnt-flush, the destructor should be flushing, so recoverable I/O errors could occur during the destructor. – Andy Feb 20 '18 at 08:58
  • @amc176 that's a good idea if I need to check for errors on `ofstream` itself at least, thanks! – Andy Feb 20 '18 at 09:00
  • 1
    I guess this piece of mine would be relevant: https://stackoverflow.com/questions/130117/throwing-exceptions-out-of-a-destructor/4098662#4098662 – Martin Ba Feb 20 '18 at 09:02
  • 1
    File streams uses [`std::basic_filebuf<...>`](http://en.cppreference.com/w/cpp/io/basic_filebuf) whose [destructor](http://en.cppreference.com/w/cpp/io/basic_filebuf/%7Ebasic_filebuf) calls [`close`](http://en.cppreference.com/w/cpp/io/basic_filebuf/close) which indeed flushes the buffer. – Some programmer dude Feb 20 '18 at 09:23

2 Answers2

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;
};
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • I wonder why there isn't a language feature to automatically swallow exceptions thrown by the destructor during stack unwinding? – Andy Feb 20 '18 at 09:34
  • 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](http://www.gotw.ca/gotw/047.htm) :-) – Arne Vogel Feb 20 '18 at 16:40
  • @Andy: I wonder why we don't have way to throw `std::nested_exception` from unwinding :-/ – Jarod42 Feb 20 '18 at 16:49
  • @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... – Jarod42 Feb 20 '18 at 17:09
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.

Niall
  • 30,036
  • 10
  • 99
  • 142