First, as others have stated, things are not that clear cut in C++, IMHO mostly because the requirements and restraints are somewhat more varied in C++ than other languages, esp. C# and Java, that have "similar" exception issues.
I'll expose on the std::stof example:
passing an empty string to std::stof (will throw invalid_argument) not
a programming error
The basic contract, as I see it, of this function is that it tries to convert it's argument to a float, and any failure to do so is reported by an exception. Both possible exceptions are derived from logic_error
but not in the sense of programmer-error but in the sense of "the input cannot, ever, be converted to a float".
Here, one may say a logic_error
is used to indicate that, given that (runtime) input, it's always an error to try to convert it - but it's the job of the function to determine that and tell you (via exception).
Side note: In that view, a runtime_error
could be viewed as something that, given the same input to a function, could theoretically succeed for different runs. (e.g. a file operation, DB access, etc.)
Further Side Note: The C++ regex library chose to derive it's error from runtime_error
although there are cases where it could be classified the same as here (invalid regex pattern).
This just shows, IMHO, that grouping into logic_
or runtime_
error is pretty fuzzy in C++ and doesn't really help much in the general case(*) -- if you need to handle specific errors, you probably need to catch lower than the two.
(*): That's not to say that a single piece of code shouldn't be consistent, but whether you throw runtime_
or logic_
or custom_
somethings is really not that important, I think.
To comment on both stof
and bitset
:
Both functions take strings as argument, and in both cases it is:
- non-trivial to check for the caller whether a given string is valid (e.g. worst case you'd have to replicate the function logic; in the case of bitset, it's not immediately clear whether empty string is valid, so let the ctor decide)
- It is already the responsibility of the function to "parse" the string, so it already has to validate the string, so it makes sense that it reports an error to "use" the string uniformly (and in both cases this is an exception).
rule that comes up frequently with exceptions is "only use exceptions
in exceptional circumstances". But how is a library function supposed
to know which circumstances are exceptional?
This statement has, IMHO, two roots:
Performance: If a function is called in a critical path, and the "exceptional" case is not exceptional, i.e. a significant amount of passes will involve throwing an exception, then paying every time for the exception-unwinding-machinery doesn't make sense, and may be too slow.
Locality of error handling: If a function is invoked and the exception is immediately caught and processed, then there is little point in throwing an exception, as the error handling will be more verbose with the catch
than with an if
.
Example:
float readOrDefault;
try {
readOrDefault = stof(...);
} catch(std::exception&) {
// discard execption, just use default value
readOrDefault = 3.14f; // 3.14 is the default value if cannot be read
}
Here's where functions like TryParse
vs. Parse
come into play: One version for when the local code expects the parsed string to be valid, one version when the local code assumes that it is actually expected (i.e. non exceptional) that parsing will fail.
Indeed, stof
is just (defined as) a wrapper around strtof
, so if you don't want exceptions, use that one.
So, how am I supposed to decide if I should use exceptions or not for
a particular function?
IMHO, you have two cases:
"Library" like function (reused often in different contexts): You basically can't decide. Possibly provide both versions, maybe one that reports an error and a wrappper one that converts the returned error into an exception.
"Application" function (specific for a blob of application code, may be reused some, but is constrained by the apps error handling style, etc.): Here, it should often be pretty clear cut. If the code path(s) calling the functions handle exceptions in a sane and useful way, use exceptions to report any(but see below) error. If the application code is more easily read and written for an error return style, by all means use that.
Of course there'll be places in between - just use what needs and remember YAGNI.
Last, I think I should get back to the FAQ statement,
Do not use throw to indicate a coding error in usage of a function.
Use assert or other mechanism to either send the process into a
debugger or to crash the process ...
I subscribe to this for all errors that are clear indication that something is severely messed up or that the calling code clearly didn't know what it was doing.
But when this is appropriate is often highly application specific, hence see above library domain vs. application domain.
This falls back on the question on whether and how to validate calling preconditions, but I won't go into that, answer already too long :-)