If I want to load an object from a file, there are a number of things that can go wrong. Thus, one needs a way of handling errors when doing so. In some languages, like haskell, one can return a Maybe
object that might contain the newly created object. In C++
, although it is in principle possible to create such a convenience class, it is usually not the most straight-forward way to do things. Below I list various ways errors could be handled when loading an object from a file in C++
.
- Return a pointer:
Object* load_from_file(const char* filename);
By returning a pointer, we can return nullptr
in case a problem occured while loading the object.
- Take a reference to a default constructed object and return
false
if a problem occured:
bool load_from_file(const char* filename, Object& obj);
Using this method, a separate initialization method needs to be implemented for the object, which will be called after loading it from the file.
- Throw an exception inside the
load_from_file
function:
Object load_from_file(const char* filename);
Generally, I believe, throwing an exception in such a case might be bad, as this is not really an "exceptional" case.
- Load data to an intermediate
struct
:
struct ObjectData{
//data
};
bool load_from_file(const char* filename, ObjectData& data);
//example use:
ObjectData data;
if(load_from_file("path_to_some_file", data)){
Object(data);
//etc...
}
The difference here with the second method is that you do not need to create an initialization method for Object
.
Generally, what is the preferred method for the problem described above, or which method is preferred in which cases?
2 Answers 2
Don't confuse "exception" with "rare"
The main point of exceptions and exception handling is keeping different types of code apart:
- The code that provides the main functionality (the "happy path" code)
- Everything else: The code that handles all those 973 other cases where not everything has worked out wonderfully and precisely to your main functionality's liking.
Pulling the code for the not-quite-happy statements out of your happy path code makes the latter nicely simple and understandable. That's what exception handling is about, really (plus keeping the other 973 cases apart from one another as well; plus making sure error handling of errors that occurred during error handling will happen reliably as well).
So your formulation "there are a number of things that can go wrong" is a good indication that an exception is probably the most maintainable-in-the-long-run approach for your case. And that remains true even if the exceptions are more frequent than the happy path case.
-
1Exceptions are used differently in Python and in C++, and "it is easier to ask for forgiveness than for permission" does not apply everywhere. Properly deallocating resources when exceptions are encountered can be difficult (note that C++ doesn't have garbage collection like Python), and some people have just given up writing exception-safe C++ code (!). Furthermore, the design of some languages (such as C++) makes exceptions very expensive, so they have to be avoided in performance-critical sections anyway.amon– amon2014年12月02日 15:40:54 +00:00Commented Dec 2, 2014 at 15:40
-
2If you use RAII correctly and no raw pointers, there is no problem you can get into with exceptionsNikko– Nikko2014年12月02日 16:04:39 +00:00Commented Dec 2, 2014 at 16:04
-
This is just a personal preference, but I try to keep my functions as pure as possible. That's why I use exceptions only for "exceptional" cases.Grieverheart– Grieverheart2014年12月03日 12:53:38 +00:00Commented Dec 3, 2014 at 12:53
Generally, what is the preferred method for the problem described above, or which method is preferred in which cases?
The prefered method looks like this:
class Object { ... };
std::istream& operator>>(std::istream& in, Object& obj) { /* classic "in" operator*/ }
Client code (1):
Object o;
if(std::cin >> o) {
// do something with o
}
Client code (2):
Object o;
std::cin.exceptions(std::ios::failbit);
std::cin >> o; // will throw on failure
// do something with o
It is up to your implementation of the input operator, to set the failbit for in
(which may or may not throw an exception), in case the values read from the stream are valid for the data types, but not valid for being placed into the Object
instance.
This is prefered, because:
- it is flexible wrt the stream type (with this code, you can just as well use a std::stringstream instance and serialize your data to a string)
- it doesn't impose the error handling strategy (client code gets to decide if an exception should be thrown or not)
- it minimizes knowledge cf. Law of Demeter (the loading function doesn't need to know what a file - or a file path - is)
it integrates and scales properly with the built-in C++ i/o support; that means, you can easily do this:
Object a; int b; std::string word; std::cin >> a >> b >> word; // a is read the same as any int or std::string
Downsides:
- unless you are careful on reading, errors may slip by (consider my first example, without the if
as an example).
Edit: An extra advantage of writing i/o code with i/o-stream operators, is that you get free integration with other code that uses std::i/o-streams. For example, after you define your operator, you could use boost::lexical_cast with Object
instances:
#include <boost/lexical_cast.hpp>
std::ostringstream buffer;
Object a = { /* data here */ };
buffer << a;
auto b = boost::lexical_cast<Object>(buffer.str());
-
So, if I understand correctly, you suggest that the second method I listed is the preferred one, only you propose using STL streams instead?Grieverheart– Grieverheart2014年12月02日 17:19:24 +00:00Commented Dec 2, 2014 at 17:19
nullptr
return value.optional
in the C++ world. boost has a battle-tested implementation and Library TS1 has it, with the hope that it will be in C++17