[docs] Add more Error documentation to the Programmer's Manual.

This patch updates some of the existing Error examples, expands on the
documentation for handleErrors, and includes new sections that cover
a number of helpful utilities and common error usage idioms.

llvm-svn: 285122
This commit is contained in:
Lang Hames 2016-10-25 21:19:30 +00:00
parent efd5643419
commit 03a88ccba3
1 changed files with 356 additions and 48 deletions

View File

@ -337,25 +337,28 @@ Failure values are constructed using ``make_error<T>``, where ``T`` is any class
that inherits from the ErrorInfo utility, E.g.:
.. code-block:: c++
class MyError : public ErrorInfo<MyError> {
class BadFileFormat : public ErrorInfo<BadFileFormat> {
public:
MyError(std::string Msg) : Msg(Msg) {}
void log(OStream &OS) const override { OS << "MyError - " << Msg; }
static char ID;
private:
std::string Msg;
std::string Path;
BadFileFormat(StringRef Path) : Path(Path.str()) {}
void log(raw_ostream &OS) const override {
OS << Path << " is malformed";
}
std::error_code convertToErrorCode() const override {
return make_error_code(object_error::parse_failed);
}
};
char MyError::ID = 0; // In MyError.cpp
char FileExists::ID; // This should be declared in the C++ file.
Error bar() {
if (checkErrorCondition)
return make_error<MyError>("Error condition detected");
// No error - proceed with bar.
// Return success value.
Error printFormattedFile(StringRef Path) {
if (<check for valid format>)
return make_error<InvalidObjectFile>(Path);
// print file contents.
return Error::success();
}
@ -375,34 +378,56 @@ success, enabling the following idiom:
For functions that can fail but need to return a value the ``Expected<T>``
utility can be used. Values of this type can be constructed with either a
``T``, or an ``Error``. Expected<T> values are also implicitly convertible to
boolean, but with the opposite convention to Error: true for success, false for
error. If success, the ``T`` value can be accessed via the dereference operator.
If failure, the ``Error`` value can be extracted using the ``takeError()``
method. Idiomatic usage looks like:
boolean, but with the opposite convention to ``Error``: true for success, false
for error. If success, the ``T`` value can be accessed via the dereference
operator. If failure, the ``Error`` value can be extracted using the
``takeError()`` method. Idiomatic usage looks like:
.. code-block:: c++
Expected<float> parseAndSquareRoot(IStream &IS) {
float f;
IS >> f;
if (f < 0)
return make_error<FloatingPointError>(...);
return sqrt(f);
Expected<FormattedFile> openFormattedFile(StringRef Path) {
// If badly formatted, return an error.
if (auto Err = checkFormat(Path))
return std::move(Err);
// Otherwise return a FormattedFile instance.
return FormattedFile(Path);
}
Error foo(IStream &IS) {
if (auto SqrtOrErr = parseAndSquartRoot(IS)) {
float Sqrt = *SqrtOrErr;
Error processFormattedFile(StringRef Path) {
// Try to open a formatted file
if (auto FileOrErr = openFormattedFile(Path)) {
// On success, grab a reference to the file and continue.
auto &File = *FileOrErr;
// ...
} else
return SqrtOrErr.takeError();
} else // On error, extract the Error value and return it.
return FileOrErr.takeError();
}
All Error instances, whether success or failure, must be either checked or
moved from (via std::move or a return) before they are destructed. Accidentally
discarding an unchecked error will cause a program abort at the point where the
unchecked value's destructor is run, making it easy to identify and fix
violations of this rule.
If an ``Expected<T>`` value is in success mode then the ``takeError()`` method
will return a success value. Using this fact, the above function can be
rewritten as:
.. code-block:: c++
Error processFormattedFile(StringRef Path) {
// Try to open a formatted file
auto FileOrErr = openFormattedFile(Path);
if (auto Err = FileOrErr.takeError())
// On error, extract the Error value and return it.
return Err;
// On success, grab a reference to the file and continue.
auto &File = *FileOrErr;
// ...
}
This second form is often more readable for functions that involve multiple
``Expected<T>`` values as it limits the indentation required.
All ``Error`` instances, whether success or failure, must be either checked or
moved from (via ``std::move`` or a return) before they are destructed.
Accidentally discarding an unchecked error will cause a program abort at the
point where the unchecked value's destructor is run, making it easy to identify
and fix violations of this rule.
Success values are considered checked once they have been tested (by invoking
the boolean conversion operator):
@ -428,22 +453,305 @@ been activated:
.. code-block:: c++
auto Err = canFail(...);
if (auto Err2 =
handleErrors(std::move(Err),
[](std::unique_ptr<MyError> M) {
// Try to handle 'M'. If successful, return a success value from
// the handler.
if (tryToHandle(M))
return Error::success();
handleErrors(
processFormattedFile(…),
[](const BadFileFormat &BFF) {
report(“Unable to process “ + BFF.Path + “: bad format”);
},
[](const FileNotFound &FNF) {
report("File not found " + FNF.Path);
});
// We failed to handle 'M' - return it from the handler.
// This value will be passed back from catchErrors and
// wind up in Err2, where it will be returned from this function.
return Error(std::move(M));
})))
return Err2;
The ``handleErrors`` function takes an error as its first argument, followed by
a variadic list of "handlers", each of which must be a callable type (a
function, lambda, or class with a call operator) with one argument. The
``handleErrors`` function will visit each handler in the sequence and check its
argument type against the dynamic type of the error, running the first handler
that matches. This is the same process that is used for catch clauses in C++
exceptions.
Since the list of handlers passed to ``handleErrors`` may not cover every error
type that can occur, the ``handleErrors`` function also returns an Error value
that must be checked or propagated. If the error value that is passed to
``handleErrors`` does not match any of the handlers it will be returned from
handleErrors. Idiomatic use of ``handleErrors`` thus looks like:
.. code-block:: c++
if (auto Err =
handleErrors(
processFormattedFile(...),
[](const BadFileFormat &BFF) {
report(“Unable to process “ + BFF.Path + “: bad format”);
},
[](const FileNotFound &FNF) {
report("File not found " + FNF.Path);
}))
return Err;
In cases where you truly know that the handler list is exhaustive the
``handleAllErrors`` function can be used instead. This is identical to
``handleErrors`` except that it will terminate the program if an unhandled
error is passed in, and can therefore return void. The ``handleAllErrors``
function should generally be avoided: the introduction of a new error type
elsewhere in the program can easily turn a formerly exhaustive list of errors
into a non-exhaustive list, risking unexpected program termination. Where
possible, use handleErrors and propagate unknown errors up the stack instead.
StringError
"""""""""""
Many kinds of errors have no recovery strategy, the only action that can be
taken is to report them to the user so that the user can attempt to fix the
environment. In this case representing the error as a string makes perfect
sense. LLVM provides the ``StringError class for this purpose. It takes two
arguments: A string error message, and an equivalent ``std::error_code`` for
interoperability:
.. code-block:: c++
make_error<StringError>("Bad executable",
make_error_code(errc::executable_format_error"));
If you're certain that the error you're building will never need to be converted
to a ``std::error_code`` you can use the ``inconvertibleErrorCode()`` function:
.. code-block:: c++
make_error<StringError>("Bad executable", inconvertibleErrorCode());
This should be done only after careful consideration. If any attempt is made to
convert this error to a ``std::error_code`` it will trigger immediate program
termination. Unless you are certain that your errors will not need
interoperability you should look for an existing ``std::error_code`` that you
can convert to, and even (as painful as it is) consider introducing a new one as
a stopgap measure.
Interoperability with std::error_code and ErrorOr
"""""""""""""""""""""""""""""""""""""""""""""""""
Many existing LLVM APIs use ``std::error_code`` and its partner ``ErrorOr<T>``
(which plays the same role as ``Expected<T>``, but wraps a ``std::error_code``
rather than an ``Error``). The infectious nature of error types means that an
attempt to change one of these functions to return ``Error`` or ``Expected<T>``
instead often results in an avalanche of changes to callers, callers of callers,
and so on. (The first such attempt, returning an ``Error`` from
MachOObjectFiles constructor, was abandoned after the diff reached 3000 lines,
impacted half a dozen libraries, and was still growing).
To solve this problem, the ``Error``/``std::error_code`` interoperability requirement was
introduced. Two pairs of functions allow any ``Error`` value to be converted to a
``std::error_code``, any ``Expected<T>`` to be converted to an ``ErrorOr<T>``, and vice
versa:
.. code-block:: c++
std::error_code errorToErrorCode(Error Err);
Error errorCodeToError(std::error_code EC);
template <typename T> ErrorOr<T> expectedToErrorOr(Expected<T> TOrErr);
template <typename T> Expected<T> errorOrToExpected(ErrorOr<T> TOrEC);
Using these APIs it is easy to make surgical patches that update individual
functions from ``std::error_code`` to ``Error``, and from ``ErrorOr<T>`` to
``Expected<T>``.
Returning Errors from error handlers
""""""""""""""""""""""""""""""""""""
Error recovery attempts may themselves fail. For that reason, ``handleErrors``
actually recognises three different forms of handler signature:
.. code-block:: c++
// Error must be handled, no new errors produced:
void(UserDefinedError &E);
// Error must be handled, new errors can be produced:
Error(UserDefinedError &E);
// Original error can be inspected, then re-wrapped and returned (or a new
// error can be produced):
Error(std::unique_ptr<UserDefinedError> E);
Any error returned from a handler will be returned from the ``handleErrors``
function so that it can be handled itself, or propagated up the stack.
Using ExitOnError to simplify tool code
"""""""""""""""""""""""""""""""""""""""
Library code should never call ``exit`` for a recoverable error, however in tool
code (especially comamnd line tools) this can be a reasonable approach. Calling
``exit`` upon encountering an error dramatically simplifies control flow as the
error no longer needs to be propagated up the stack. This allows code to be
written in straight-line style, as long as each fallible call is wrapped in a
check and call to exit. The ``ExitOnError``` class supports this pattern by
providing call operators that inspect ``Error`` values, stripping the error away
in the success case and logging to ``stderr`` then exiting in the failure case.
To use this class, declare a global ``ExitOnError`` variable in your program:
.. code-block:: c++
ExitOnError ExitOnErr;
Calls to fallible functions can then be wrapped with a call to ``ExitOnErr``,
turning them into non-failing calls:
.. code-block:: c++
Error mayFail();
Expected<int> mayFail2();
void foo() {
ExitOnErr(mayFail());
int X = ExitOnErr(mayFail2());
}
On failure, the errors log message will be written to ``stderr``, optionally
preceded by a string “banner” that can be set by calling the setBanner method. A
mapping can also be supplied from ``Error`` values to exit codes using the
``setExitCodeMapper`` method:
int main(int argc, char *argv[]) {
ExitOnErr.setBanner(std::string(argv[0]) + “ error:”);
ExitOnErr.setExitCodeMapper(
[](const Error &Err) {
if (Err.isA<BadFileFormat>())
return 2;
return 1;
});
Use ``ExitOnError`` in your tool code where possible as it can greatly improve
readability.
Fallible constructors
"""""""""""""""""""""
Some classes require resource acquisition or other complex initialization that
can fail during construction. Unfortunately constructors cant return errors,
and having clients test objects after theyre constructed to ensure that theyre
valid is error prone as its all too easy to forget the test. To work around
this, use the named constructor idiom and return an ``Expected<T>``:
.. code-block:: c++
class Foo {
public:
static Expected<Foo> Create(Resource R1, Resource R2) {
Error Err;
Foo F(R1, R2, Err);
if (Err)
return std::move(Err);
return std::move(F);
}
private:
Foo(Resource R1, Resource R2, Error &Err) {
ErrorAsOutParameter EAO(&Err);
if (auto Err2 = R1.acquire()) {
Err = std::move(Err2);
return;
}
Err = R2.acquire();
}
};
Here, the named constructor passes an ``Error`` by reference into the actual
constructor, which the constructor can then use to return errors. The
``ErrorAsOutParameter`` utility sets the ``Error`` value's checked flag on entry
to the constructor so that the error can be assigned to, then resets it on exit
to force the client (the named constructor) to check the error.
By using this idiom, clients attempting to construct a Foo receive either a
well-formed Foo or an Error, never an object in an invalid state.
Propagating and consuming errors based on types
"""""""""""""""""""""""""""""""""""""""""""""""
In some contexts, certain types of error are known to be benign. For example,
when walking an archive, some clients may be happy to skip over badly formatted
object files rather than terminating the walk immediately. Skipping badly
formatted objects could be achieved using an elaborate handler method, But the
Error.h header provides two utilities that make this idiom much cleaner: the
type inspection method, ``isA``, and the ``consumeError`` function:
.. code-block:: c++
Error walkArchive(Archive A) {
for (unsigned I = 0; I != A.numMembers(); ++I) {
auto ChildOrErr = A.getMember(I);
if (auto Err = ChildOrErr.takeError())
if (Err.isA<BadFileFormat>())
consumeError(std::move(Err))
else
return Err;
auto &Child = *ChildOrErr;
// do work
}
return Error::success();
}
Concatenating Errors with joinErrors
""""""""""""""""""""""""""""""""""""
In the archive walking example above ``BadFileFormat`` errors are simply
consumed and ignored. If the client had wanted report these errors after
completing the walk over the archive they could use the ``joinErrors`` utility:
.. code-block:: c++
Error walkArchive(Archive A) {
Error DeferredErrs = Error::success();
for (unsigned I = 0; I != A.numMembers(); ++I) {
auto ChildOrErr = A.getMember(I);
if (auto Err = ChildOrErr.takeError())
if (Err.isA<BadFileFormat>())
DeferredErrs = joinErrors(std::move(DeferredErrs), std::move(Err));
else
return Err;
auto &Child = *ChildOrErr;
// do work
}
return DeferredErrs;
}
The ``joinErrors`` routine builds a special error type called ``ErrorList``,
which holds a list of user defined errors. The ``handleErrors`` routine
recognizes this type and will attempt to handle each of the contained erorrs in
order. If all contained errors can be handled, ``handleErrors`` will return
``Error::success()``, otherwise ``handleErrors`` will concatenate the remaining
errors and return the resulting ``ErrorList``.
Building fallible iterators and iterator ranges
"""""""""""""""""""""""""""""""""""""""""""""""
The archive walking examples above retrieve archive members by index, however
this requires considerable boiler-plate for iteration and error checking. We can
clean this up considerably by using ``Error`` with the "fallible iterator"
pattern. The usual C++ iterator patterns do not allow for failure on increment,
but we can incorporate support for it by having iterators hold an Error
reference through which they can report failure. In this pattern, if an
increment operation fails the failure is recorded via the Error reference and
the iterator value is set to the end of the range in order to terminate the
loop. This ensures that the dereference operation is safe anywhere that an
ordinary iterator dereference would be safe (i.e. when the iterator is not equal
to end). Where this pattern is followed (as in the ``llvm::object::Archive``
class) the result is much cleaner iteration idiom:
.. code-block:: c++
Error Err;
for (auto &Child : Ar->children(Err)) {
// Use Child - we only enter the loop when its valid.
}
// Check Err after the loop to ensure it didnt break due to an error.
if (Err)
return Err;
.. _function_apis: