Reputation: 6340
What are the best practices for catching all errors during file IO in C++? More specifically, what are the best practices for dealing with errors that could potentially arise with ios objects? For example, the following program reads a file from disk and prints it:
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
#include <exception>
#include <stdexcept>
// Defines a custom exception
struct MyException : public std::exception {
std::string s;
MyException(std::string s_) : s(s_) {};
const char * what () const throw () {
return ("MyException: " + s).c_str();
}
};
// Prints out nested exceptions
void print_exception(std::exception const & e, size_t const level = 0) {
std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch(const std::exception& e) {
print_exception(e, level+1);
} catch(...) {}
}
// Read the specified filename into a string
std::string read_into_string(std::string const & fname) {
// Create the file stream
std::ifstream fin;
fin.exceptions(std::ifstream::failbit | std::ifstream::badbit);
// Open the file
try {
fin.open(fname.c_str());
} catch(...) {
std::throw_with_nested(MyException(
"Unable to open the file: " + fname));
}
// Make sure to close out the file if there's a problem
try {
// Create the string stream
std::stringstream sin;
sin.exceptions(std::ifstream::failbit | std::ifstream::badbit);
// Put the file stream into a string stream
try {
sin << fin.rdbuf();
} catch(...) {
std::throw_with_nested(MyException(
"Error when pusing the file stream into the string stream"));
}
// Turn the string stream into a string
std::string str;
try {
str = sin.str();
} catch(...) {
std::throw_with_nested(MyException(
"Error converting the string stream into a string"));
}
// Close out the file
fin.close();
// Return the string;
return str;
} catch(...) {
// Close out the file
fin.close();
// Rethrow the exception
throw;
}
}
int main() {
try {
std::string str(read_into_string("file.txt"));
std::cout << str;
} catch(const std::exception& e) {
print_exception(e);
}
}
However, it seems very, very heavy. Basically, it seems like we have to check every time we touch an ios object since something could go wrong and it would help to know exactly where. In addition, the above code contains multiple file closes, one what everything works and one where there's an exception, which is undesirable. Finally, I didn't check the error status of the other ios objects such as cout, but technically since they're ios objects, couldn't they set a bad or fail bit as well that should be trapped? Do the string streams need to be closed out in case of an error?
Really, the core question is: what are the best practices for dealing with errors that could potentially arise with ios objects?
Upvotes: 5
Views: 2643
Reputation: 16016
It's not common practice to enable .exceptions()
on an I/O stream in C++. Most likely you learned some other language where they taught you to use exceptions for everything you can. Don't.
It' perfectly easy to handle errors on streams without exceptions: the stream will change from being truthy to being falsy. Additionally, unless you reset the failure bits, any operation on a falsy stream will have absolutely no effect.
Additionally, there is a method for dumping an entire input stream into an output stream.
// Read the specified filename into a string
std::string read_into_string(std::string const & fname) {
// Create the file stream
std::ifstream fin(fname.c_str());
std::ostringstream oss;
oss << fin.rdbuf();
if (!fin) throw MyException();
return oss.str();
}
However, you might want to rethink your need for input in a single stream. Usually I find a sequence of lines to be much more useful.
Upvotes: 4
Reputation: 69854
You can reduce the pain a little because:
you can call exceptions() on the stream after you've opened it
stream.close() is implicit in the destructor
-
std::string read_into_string(std::string const & fname) {
// Create the file stream
std::ifstream fin(fname.c_str());
try {
fin.exceptions(std::ifstream::failbit | std::ifstream::badbit);
} catch(...) {
std::throw_with_nested(MyException(
"Unable to open the file: " + fname));
}
// Create the string stream
std::stringstream sin;
try {
sin.exceptions(std::ifstream::failbit | std::ifstream::badbit);
sin << fin.rdbuf();
} catch(...) {
std::throw_with_nested(MyException(
"Error when pusing the file stream into the string stream"));
}
// this try is very pedantic, you probably don't need it since the only exception
// here will be std::bad_alloc
try {
return sin.str();
} catch(...) {
std::throw_with_nested(MyException(
"Error converting the string stream into a string"));
}
// RAII takes care of closing all the file handles for you
}
However most people would write the function more like this:
std::string read_into_string(std::string const & fname) {
try {
std::ifstream fin(fname.c_str());
fin.exceptions(std::ifstream::failbit | std::ifstream::badbit);
std::stringstream sin;
sin.exceptions(std::ifstream::failbit | std::ifstream::badbit);
sin << fin.rdbuf();
return sin.str();
} catch(...) {
std::throw_with_nested(MyException(
std::string("problem with file ") + fname));
}
}
in addition, MyException should probably be more like this:
struct MyException : public std::runtime_error {
MyException(const std::string& s_) : std::runtime_error(std::string("MyException:) +s_) {};
};
Because this way it's derived from runtime_error (which states what it actually is), and you're not returning a dangerous pointer into a temporary in the what() implementation.
Upvotes: 2