From 03a88ccba340bd7a7c83b988443a144de09c91a6 Mon Sep 17 00:00:00 2001 From: Lang Hames Date: Tue, 25 Oct 2016 21:19:30 +0000 Subject: [PATCH] [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 --- llvm/docs/ProgrammersManual.rst | 404 +++++++++++++++++++++++++++++++++++----- 1 file changed, 356 insertions(+), 48 deletions(-) diff --git a/llvm/docs/ProgrammersManual.rst b/llvm/docs/ProgrammersManual.rst index 4a981e5..cc6d3f8 100644 --- a/llvm/docs/ProgrammersManual.rst +++ b/llvm/docs/ProgrammersManual.rst @@ -337,25 +337,28 @@ Failure values are constructed using ``make_error``, where ``T`` is any class that inherits from the ErrorInfo utility, E.g.: .. code-block:: c++ - - class MyError : public ErrorInfo { + class BadFileFormat : public ErrorInfo { 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; - char MyError::ID = 0; // In MyError.cpp + BadFileFormat(StringRef Path) : Path(Path.str()) {} - Error bar() { - if (checkErrorCondition) - return make_error("Error condition detected"); + 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); + } + }; - // No error - proceed with bar. + char FileExists::ID; // This should be declared in the C++ file. - // Return success value. + Error printFormattedFile(StringRef Path) { + if () + return make_error(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`` utility can be used. Values of this type can be constructed with either a ``T``, or an ``Error``. Expected 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 parseAndSquareRoot(IStream &IS) { - float f; - IS >> f; - if (f < 0) - return make_error(...); - return sqrt(f); + Expected 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`` 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`` 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 M) { - // Try to handle 'M'. If successful, return a success value from - // the handler. - if (tryToHandle(M)) - return Error::success(); - - // 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; + handleErrors( + processFormattedFile(…), + [](const BadFileFormat &BFF) { + report(“Unable to process “ + BFF.Path + “: bad format”); + }, + [](const FileNotFound &FNF) { + report("File not found " + FNF.Path); + }); + +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("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("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`` +(which plays the same role as ``Expected``, 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`` +instead often results in an avalanche of changes to callers, callers of callers, +and so on. (The first such attempt, returning an ``Error`` from +MachOObjectFile’s 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`` to be converted to an ``ErrorOr``, and vice +versa: + +.. code-block:: c++ + + std::error_code errorToErrorCode(Error Err); + Error errorCodeToError(std::error_code EC); + + template ErrorOr expectedToErrorOr(Expected TOrErr); + template Expected errorOrToExpected(ErrorOr TOrEC); + + +Using these APIs it is easy to make surgical patches that update individual +functions from ``std::error_code`` to ``Error``, and from ``ErrorOr`` to +``Expected``. +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 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 mayFail2(); + + void foo() { + ExitOnErr(mayFail()); + int X = ExitOnErr(mayFail2()); + } + +On failure, the error’s 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()) + 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 can’t return errors, +and having clients test objects after they’re constructed to ensure that they’re +valid is error prone as it’s all too easy to forget the test. To work around +this, use the named constructor idiom and return an ``Expected``: + +.. code-block:: c++ + + class Foo { + public: + +
 static Expected 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()) + 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()) + 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 it’s valid. + } + // Check Err after the loop to ensure it didn’t break due to an error. + if (Err) + return Err; .. _function_apis: -- 2.7.4