Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do C++ standard file streams not follow RAII conventions more closely?

Why do C++ Standard Library streams use open()/close() semantics decoupled from object lifetime? Closing on destruction might still technically make the classes RAII, but acquisition/release independence leaves holes in scopes where handles can point to nothing but still need run-time checks to catch.

Why did the library designers choose their approach over having opening only in constructors that throw on a failure?

void foo() {   std::ofstream ofs;   ofs << "Can't do this!\n"; // XXX   ofs.open("foo.txt");    // Safe access requires explicit checking after open().   if (ofs) {     // Other calls still need checks but must be shielded by an initial one.   }    ofs.close();   ofs << "Whoops!\n"; // XXX }  // This approach would seem better IMO: void bar() {   std_raii::ofstream ofs("foo.txt"); // throw on failure and catch wherever   // do whatever, then close ofs on destruction ... } 

A better wording of the question might be why access to a non-opened fstream is ever worth having. Controlling open file duration via handle lifetime does not seem to me to be a burden at all, but actually a safety benefit.

like image 446
Jeff Avatar asked Sep 02 '14 13:09

Jeff


1 Answers

Although the other answers are valid and useful, I think the real reason is simpler.

The iostreams design is much older than a lot of the Standard Library, and predates wide use of exceptions. I suspect that in order to be compatible with existing code, the use of exceptions was made optional, not the default for failure to open a file.

Also, your question is only really relevant to file streams, the other types of standard stream don't have open() or close() member functions, so their constructors don't throw if a file can't be opened :-)

For files, you may want to check that the close() call succeeded, so you know if the data got written to disk, so that's a good reason not to do it in the destructor, because by the time the object is destroyed it is too late to do anything useful with it and you almost certainly don't want to throw an exception from the destructor. So an fstreambuf will call close in its destructor, but you can also do it manually before destruction if you want to.

In any case, I don't agree that it doesn't follow RAII conventions...

Why did the library designers choose their approach over having opening only in constructors that throw on a failure?

N.B. RAII doesn't mean you can't have a separate open() member in addition to a resource-acquiring constructor, or you can't clean up the resource before destruction e.g. unique_ptr has a reset() member.

Also, RAII doesn't mean you must throw on failure, or an object can't be in an empty state e.g. unique_ptr can be constructed with a null pointer or default-constructed, and so can also point to nothing and so in some cases you need to check it before dereferencing.

File streams acquire a resource on construction and release it on destruction - that is RAII as far as I'm concerned. What you are objecting to is requiring a check, which smells of two-stage initialization, and I agree that is a bit smelly. It doesn't make it not RAII though.

In the past I have solved the smell with a CheckedFstream class, which is a simple wrapper that adds a single feature: throwing in the cosntructor if the stream couldn't be opened. In C++11 that's as simple as this:

struct CheckedFstream : std::fstream {   CheckedFstream() = default;    CheckedFstream(std::string const& path, std::ios::openmode m = std::ios::in|std::ios::out)   : fstream(path, m)   { if (!is_open()) throw std::ios::failure("Could not open " + path); } }; 
like image 181
Jonathan Wakely Avatar answered Oct 24 '22 20:10

Jonathan Wakely