You are right to distinguish the two kinds of failure, indeed they are subtly different.
Failure in the constructor: The file may not exist, may not have read-permissions, contain invalid/unparsable data, etc.
Just throw an exception. Half-built objects are the shortest way to bug ridden programs.
A class should have an invariant that the constructor establishes and that all methods maintain. If the constructor cannot establish the invariant, then the object is unusable and the best way to report this, in C++, is to throw an exception so that the language ensures the half-built object will not be used.
If people suggest that you might want an invalid state, remind them of the Single Responsibility Principle: your class already has a specific responsibility (its invariant), those who wish for more can encapsulate it in a class dedicated to provide optionality. Off my head, boost::optional
and std::unique_ptr
are both excellent choices.
Failure in the regular method: The file may already exist, there may not be write access, too little sales data available to create a pie chart, etc.
Unfortunately, you failed to distinguish between two cases:
- methods that only ever read the instance
- methods that also modify the instance
For all methods, you need to choose your error reporting strategy. My advice is that exceptions are exceptional. If failure is deemed to be exceptional (network link down when it's up 99.99% of the time), then an exception is fine. On the other hand, if failure is expected, generally depending on the input such as a find
method or in your case a write
method to a specified file, then you want to give the user the chance to react appropriately.
There are at least two ways left, after exceptions have been ruled out:
- returning a code (
bool
, enum
) indicating whether the operation went well or not
- asking the user to provide an error policy that will be invoked in case of issue
The error policy might be as simple as an enum
(skip, retry-once, throw) or as complicated as a full blown Strategy
with various methods.
Furthermore, nobody says that your method may only have one error reporting mechanism. For example, you might choose:
- to invoke an error policy if the file already exists (user-provided after all, they might want to switch to a different name)
- to throw if the disk cannot be accessed (the hardware is generally expected to work!)
Finally, on top of this, methods that also modify the instance have to worry about maintaing the invariant that the constructor established. If a method may screw up the invariant, then it's no invariant at all, and your users should quake in fear each time the class is used... The general wisdom is to perform all operations that might throw before starting modifying the object.
A simplistic (but so easy) implementation is the copy and swap idiom: internally copy the object, perform the operations on the copy, and at the end of the method swap the copy's state with the current object's state. If anything goes wrong the copy is corrupted and immediately discarded during stack unwinding, leaving the object untouched.
For more on this topic, you might want to read about Exceptions Guarantees. What I described is a typical implementation of the Strong Exception Guarantee method, similar to databases transactions (all or nothing).