ferraith
ferraith

Reputation: 929

How to implement RAII if the resource acquisition could fail

I would like to implement a class with the help of RAII. The resources should be acquired in the constructor, but it's possible that the acquistition failed. I'll give an example in the following using FILE:

class file {
public:
    file(const char* filename) {
        file_ = fopen(filename, "w+");
        if(!file_) {
          // Okay
        }
        else {
          // ERROR
        }
    }

    ~file() {
        if (fclose(file_)) {
           // ERROR
        }
    }

    void write(const char* str) {
        if (EOF == fputs(str, file_)) {
            throw runtime_error("file write failure");
        }
    }
private:
    FILE* file_;
};

So, what's the best way to handle an error which is occurred if fopen returns NULL? Because it's the constructor I can't return also NULL.

I hope somebody can give me a hint how to handle such errors!

Thank you, best regards,

Flasher

Upvotes: 2

Views: 1746

Answers (2)

Alexandre C.
Alexandre C.

Reputation: 56956

The only way a constructor can report failure is by throwing an exception.

Destructors, to the contrary, must not throw exceptions (if a destructor throws during stack unwinding, std::terminate is called, which ends the program by default).

If destruction fails, you can

  • Swallow errors silently
  • Abort the program
  • Log the error and do either of the above.

If you use RAII correctly, exceptions can traverse your code without damage.

Example here:

#include <cerrno>
#include <cstring>
#include <sstream>

file::file(const char* filename) 
{
    file_ = fopen(filename, "w+");

    if (!file_)
    {
        std::ostringstream os;
        os << "Cannot open " << filename << ": "
           << std::strerror(errno);

        throw std::runtime_error(os.str());
    }
}

file::~file()
{
    fclose(file_);
}

Note that this code has many bugs: the fclose function can fail, and the failure may or may not be related to the closing (eg. some write errors are only reported when flushing at the close system call on POSIX systems). Please use iostreams for file I/O in C++, since they provide convenient abstractions over those concerns.

Upvotes: 6

James Kanze
James Kanze

Reputation: 153929

Despite the name, RAII doesn't necessarily involve resource aquisition; std::shared_ptr doesn't do the new, for example. The name is historical, and the pattern really concerns cleanup.

In the case of File, of course, the "correct" answer is to use std::ifstream and std::ofstream, and be done with it. And very important (at least for output): RAII can only be used for exceptional clean-up, since you have to verify that the close has correctly finished before leaving the block in the normal case. As a general rule, if the close fails, you want to delete the file (and return EXIT_FAILURE from main, or do something that will make the error apparent, and prevent further code from executing on the assumption that the data were written). So the only time it would be acceptable to defer the close to a destructor is in a case where you've already found an error, and are going to delete the file as part of cleanup anyway.

Generally, output obeys a transaction model, more than an RAII one; I tend to wrap my std::ofstream in an OutputFile class, with a commit() function which closes the file, and marks the class as committed if (and only if) the close succeeds; if the destructor is called and the file hasn't been committed, the destructor closes the file, then deletes it. (Presumably some higher level will catch the exception and convert it into a return EXIT_FAILURE in main.)

Also, IO is a bit exceptional in general. Normally, if you cannot create an object, for whatever reason, the constructor should raise an exception; you don't want "zombie" objects floating around, which can't be used. In the case of IO, however, you have to deal with the fact that the object can become invalid and unusable after construction, even when construction succeeds. Since you have to constantly check their status anyway (after each read on input, and after close on output), it's often appropriate to just set some internal flag in the object, which you test.

Upvotes: 1

Related Questions