mirror of
https://github.com/boostorg/leaf.git
synced 2026-02-20 14:52:14 +00:00
3338 lines
116 KiB
Plaintext
3338 lines
116 KiB
Plaintext
:last-update-label!:
|
||
:icons: font
|
||
:prewrap!:
|
||
:source-highlighter: coderay
|
||
:stylesheet: zajo.css
|
||
= LEAF
|
||
Low-latency Error Augmentation Framework for C++11
|
||
:toclevels: 3
|
||
:toc: left
|
||
:toc-title:
|
||
:sourcedir: .
|
||
|
||
[abstract]
|
||
== Abstract
|
||
|
||
LEAF is a {CPP}11 error handling library. Features:
|
||
|
||
====
|
||
* Header-only, no dependencies.
|
||
|
||
* No dynamic memory allocations.footnote:[Except when error objects are transported between threads, see <<capture_result>>/<<capture_exception>>.]
|
||
|
||
* Any error-related object of any movable type is efficiently delivered to the correct error handler.
|
||
|
||
* Compatible with `std::error_code`, `errno` and any other error code type.
|
||
|
||
* Support for multi-thread programming.
|
||
|
||
* Can be used with or without exception handling.
|
||
|
||
[.text-right]
|
||
https://github.com/zajo/leaf[GitHub] | <<introduction,Introduction>> | <<techniques>> | <<rationale,Design Rationale>>
|
||
====
|
||
|
||
[[introduction]]
|
||
== Five Minute Introduction
|
||
|
||
For demonstration, we'll implement two versions of the same program: one using error codes to handle errors, and one using exception handling.
|
||
|
||
[[introduction-result]]
|
||
=== Using Error Codes
|
||
|
||
We'll write a short but complete program that reads a text file in a buffer and prints it to `std::cout`, using LEAF to handle errors.
|
||
|
||
Let's jump ahead and start with the `main` function: it will call functions as needed and handle all the errors that occur. Did I say *all* the errors? I did, so we'll use `leaf::handle_all`. It has the following signature:
|
||
|
||
[source,c++]
|
||
----
|
||
template <class TryBlock, class... Handler>
|
||
<<deduced-type>> handle_all( TryBlock && try_block, Handler && ... handler );
|
||
----
|
||
|
||
`TryBlock` is a function type, almost always a lambda. It is required to return `leaf::result<T>`, which holds a value of type `T` or else it communicates a failure.
|
||
|
||
The first thing `handle_all` does is invoke `try_block`. If the returned object indicates success, `handle_all` returns the contained value; otherwise it calls a suitable error handling function from the `handler...` list.
|
||
|
||
We'll see later just what kind of a `TryBlock` will our `main` function pass to `handle_all`, but first, let's look at the juicy error-handling part:
|
||
|
||
[source,c++]
|
||
----
|
||
int main( int argc, char const * argv[ ] )
|
||
{
|
||
return leaf::handle_all(
|
||
|
||
[&]() -> leaf::result<int>
|
||
{
|
||
// The TryBlock code goes here, we'll see it later
|
||
},
|
||
|
||
[ ](leaf::match<error_code, input_file_open_error>, <1>
|
||
leaf::match<leaf::e_errno, ENOENT>,
|
||
leaf::e_file_name const & fn)
|
||
{
|
||
std::cerr << "File not found: " << fn.value << std::endl;
|
||
return 1;
|
||
},
|
||
|
||
[ ](leaf::match<error_code, input_file_open_error>, <2>
|
||
leaf::e_errno const & errn,
|
||
leaf::e_file_name const & fn)
|
||
{
|
||
std::cerr << "Failed to open " << fn.value << ", errno=" << errn << std::endl;
|
||
return 2;
|
||
},
|
||
|
||
[ ](leaf::match<error_code, input_file_size_error, input_file_read_error, input_eof_error>, <3>
|
||
leaf::e_errno const & errn,
|
||
leaf::e_file_name const & fn)
|
||
{
|
||
std::cerr << "Failed to access " << fn.value << ", errno=" << errn << std::endl;
|
||
return 3;
|
||
},
|
||
|
||
[ ](leaf::match<error_code, cout_error>, <4>
|
||
leaf::e_errno const & errn)
|
||
{
|
||
std::cerr << "Output error, errno=" << errn << std::endl;
|
||
return 4;
|
||
},
|
||
|
||
[ ](leaf::match<error_code, bad_command_line>) <5>
|
||
{
|
||
std::cout << "Bad command line argument" << std::endl;
|
||
return 5;
|
||
},
|
||
|
||
[ ](leaf::error_info const & unmatched) <6>
|
||
{
|
||
std::cerr <<
|
||
"Unknown failure detected" << std::endl <<
|
||
"Cryptic diagnostic information follows" << std::endl <<
|
||
unmatched;
|
||
return 6;
|
||
}
|
||
);
|
||
}
|
||
----
|
||
|
||
<1> This handler will be called if the error includes: +
|
||
pass:[•] an object of type `error_code` equal to `input_file_open_error`, and +
|
||
pass:[•] an object of type `leaf::e_errno` that has `.value` equal to `ENOENT`, and +
|
||
pass:[•] an object of type `leaf::e_file_name`.
|
||
|
||
<2> This handler will be called if the error includes: +
|
||
pass:[•] an object of type `error_code` equal to `input_file_open_error`, and +
|
||
pass:[•] an object of type `leaf::e_errno` (regardless of its `.value`), and +
|
||
pass:[•] an object of type `leaf::e_file_name`.
|
||
|
||
<3> This handler will be called if the error includes: +
|
||
pass:[•] an object of type `error_code` equal to any of `input_file_size_error`, `input_file_read_error`, `input_eof_error`, and +
|
||
pass:[•] an object of type `leaf::e_errno` (regardless of its `.value`), and +
|
||
pass:[•] an object of type `leaf::e_file_name`.
|
||
|
||
<4> This handler will be called if the error includes: +
|
||
pass:[•] an object of type `error_code` equal to `cout_error`, and +
|
||
pass:[•] an object of type `leaf::e_errno` (regardless of its `.value`),
|
||
|
||
<5> This handler will be called if the error includes an object of type `error_code` equal to `bad_command_line`.
|
||
|
||
<6> This last handler matches any error: it prints diagnostic information to help debug logic errors in the program, since it failed to match an appropriate error handler to the error condition it encountered.
|
||
|
||
Now, reading and printing a file may not seem like a complex job, but let's split it into several functions, each communicating failures using `result<T>`:
|
||
|
||
[source,c++]
|
||
----
|
||
//Parse the command line, return the file name.
|
||
leaf::result<char const *> parse_command_line( int argc, char const * argv[ ] );
|
||
|
||
//Open a file for reading.
|
||
leaf::result<std::shared_ptr<FILE>> file_open( char const * file_name );
|
||
|
||
//Return the size of the file.
|
||
leaf::result<int> file_size( FILE & f );
|
||
|
||
//Read size bytes from f into buf.
|
||
leaf::result<void> file_read( FILE & f, void * buf, int size );
|
||
----
|
||
|
||
For example, let's look at `file_open`:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::result<std::shared_ptr<FILE>> file_open( char const * file_name )
|
||
{
|
||
if( FILE * f = fopen(file_name,"rb") )
|
||
return std::shared_ptr<FILE>(f,&fclose);
|
||
else
|
||
return leaf::new_error( input_file_open_error, leaf::e_errno{errno} );
|
||
}
|
||
----
|
||
|
||
If `fopen` succeeds, we return a `shared_ptr` which will automatically call `fclose` as needed. If `fopen` fails, we report an error by calling `new_error`, which takes any number of error objects to send with the error. In this case we pass the system `errno` (LEAF defines `struct e_errno {int value;}`), and our own error code value, `input_file_open_error`.
|
||
|
||
Here is our complete error code `enum`:
|
||
|
||
[source,c++]
|
||
----
|
||
enum error_code
|
||
{
|
||
bad_command_line = 1,
|
||
input_file_open_error,
|
||
input_file_size_error,
|
||
input_file_read_error,
|
||
input_eof_error,
|
||
cout_error
|
||
};
|
||
----
|
||
|
||
Looks good, but how does LEAF know that this `enum` represents error codes and not, say, types of cold cuts sold at Bay Cities Italian Deli? It doesn't, unless we tell it:
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template<> struct is_error_type<error_code>: std::true_type { };
|
||
|
||
} }
|
||
----
|
||
|
||
We're now ready to look at the `TryBlock` we'll pass to `handle_all`. It does all the work, bails out if it encounters an error:
|
||
|
||
[source,c++]
|
||
----
|
||
int main( int argc, char const * argv[ ] )
|
||
{
|
||
return leaf::handle_all(
|
||
|
||
[&]() -> leaf::result<int>
|
||
{
|
||
leaf::result<char const *> file_name = parse_command_line(argc,argv);
|
||
if( !file_name )
|
||
return file_name.error();
|
||
....
|
||
----
|
||
|
||
Stop the presses! What's this, if "error" return "error"? There is a better way: we'll use `LEAF_AUTO`. It takes a `result<T>` and bails out in case of a failure (control leaves the calling function), otherwise defines a local variable to access the `T` value stored in the `result` object.
|
||
|
||
This is what our `TryBlock` really looks like:
|
||
|
||
[source,c++]
|
||
----
|
||
int main( int argc, char const * argv[ ] )
|
||
{
|
||
return leaf::handle_all(
|
||
|
||
[&]() -> leaf::result<int> <1>
|
||
{
|
||
LEAF_AUTO(file_name, parse_command_line(argc,argv)); <2>
|
||
|
||
auto propagate = leaf::preload( leaf::e_file_name{file_name} ); <3>
|
||
|
||
LEAF_AUTO(f, file_open(file_name)); <4>
|
||
|
||
LEAF_AUTO(s, file_size(*f)); <4>
|
||
|
||
std::string buffer( 1 + s, '\0' );
|
||
LEAF_CHECK(file_read(*f, &buffer[0], buffer.size()-1)); <4>
|
||
|
||
std::cout << buffer;
|
||
std::cout.flush();
|
||
if( std::cout.fail() ) <5>
|
||
return leaf::new_error( cout_error, leaf::e_errno{errno} );
|
||
|
||
return 0;
|
||
},
|
||
|
||
.... // The list of error handlers goes here
|
||
|
||
); <7>
|
||
}
|
||
----
|
||
|
||
<1> Our `TryBlock` returns a `result<int>`. In case of success, it will hold `0`, which will be returned from `main` to the OS.
|
||
<2> If `parse_command_line` returns an error, we forward that error to `handle_all` (which invoked us) verbatim. Otherwise, `LEAF_AUTO` get us a local variable `file_name` to access the `char const *` result.
|
||
<3> From now on, all errors escaping this scope will automatically communicate the (now successfully parsed from the command line) file name (LEAF defines `struct e_file_name {std::string value;}`). It's as if every time one of the following functions reports an error, `preload` says "wait, put this `e_file_name` thing with the error, it's important!"
|
||
<4> Call more functions, forward each failure to the caller...
|
||
<5> ...but this is slightly different: we didn't get a failure via `result<T>` from another function, this is our own error we've detected! We return a `new_error`, passing the `cout_error` error code and the system `errno` (LEAF defines `struct e_errno {int value;}`).
|
||
<6> This concludes the `handle_all` arguments -- as well as our program!
|
||
|
||
Nice and simple! Writing the `TryBlock`, we concentrate on the "no errors" code path -- if we encounter any error we just return it to `handle_all` for processing. Well, that's if we're being good and using RAII for automatic clean-up -- which we are, `shared_ptr` will automatically close the file for us.
|
||
|
||
TIP: The complete program from this tutorial is available https://github.com/zajo/leaf/blob/master/example/print_file_result.cpp?ts=3[here]. The https://github.com/zajo/leaf/blob/master/example/print_file_eh.cpp?ts=3[other] version of the same program uses exception handling to report errors (see <<introduction-eh,below>>).
|
||
|
||
'''
|
||
|
||
[[introduction-eh]]
|
||
=== Using Exception Handling
|
||
|
||
And now, we'll write the same program that reads a text file in a buffer and prints it to `std::cout`, this time using exceptions to report errors. First, we need to define our exception class hierarchy:
|
||
|
||
[source,c++]
|
||
----
|
||
struct print_file_error : virtual std::exception { };
|
||
struct command_line_error : virtual print_file_error { };
|
||
struct bad_command_line : virtual command_line_error { };
|
||
struct input_error : virtual print_file_error { };
|
||
struct input_file_error : virtual input_error { };
|
||
struct input_file_open_error : virtual input_file_error { };
|
||
struct input_file_size_error : virtual input_file_error { };
|
||
struct input_file_read_error : virtual input_file_error { };
|
||
struct input_eof_error : virtual input_file_error { };
|
||
----
|
||
|
||
NOTE: To avoid ambiguities in the dynamic type conversion which occur when catching a base type, it is generally recommended to use virtual inheritance in exception type hierarchies.
|
||
|
||
Again, we'll split the job into several functions, each communicating failures by throwing exceptions:
|
||
|
||
[source,c++]
|
||
----
|
||
//Parse the command line, return the file name.
|
||
char const * parse_command_line( int argc, char const * argv[ ] );
|
||
|
||
//Open a file for reading.
|
||
std::shared_ptr<FILE> file_open( char const * file_name );
|
||
|
||
//Return the size of the file.
|
||
int file_size( FILE & f );
|
||
|
||
//Read size bytes from f into buf.
|
||
void file_read( FILE & f, void * buf, int size );
|
||
----
|
||
|
||
The `main` function brings everything together and handles all the exceptions that are thrown, but instead of using `try`, it will use the function template `leaf::try_`, which has the following signature:
|
||
|
||
[source,c++]
|
||
----
|
||
template <class TryBlock, class... Handler>
|
||
<<deduced-type>> try_( TryBlock && try_block, Handler && ... handler );
|
||
----
|
||
|
||
`TryBlock` is a function type, almost always a lambda; `try_` simply returns the value returned by the `try_block`, catching any exception it throws, in which case `try_` calls a suitable error handling function from the `handler...` list.
|
||
|
||
Let's look at the `TryBlock` our `main` function passes to `try_`:
|
||
|
||
[source,c++]
|
||
----
|
||
int main( int argc, char const * argv[ ] )
|
||
{
|
||
std::cout.exceptions(std::ostream::failbit | std::ostream::badbit); <1>
|
||
|
||
return leaf::try_(
|
||
|
||
[&] <2>
|
||
{
|
||
char const * file_name = parse_command_line(argc,argv); <3>
|
||
|
||
auto propagate = leaf::preload( leaf::e_file_name{file_name} ); <4>
|
||
|
||
std::shared_ptr<FILE> f = file_open( file_name );
|
||
|
||
std::string buffer( 1+file_size(*f), '\0' );
|
||
file_read(*f,&buffer[0],buffer.size()-1);
|
||
|
||
auto propagate2 = leaf::defer([ ] { return leaf::e_errno{errno}; } ); <5>
|
||
std::cout << buffer;
|
||
std::cout.flush();
|
||
|
||
return 0;
|
||
},
|
||
|
||
.... <6>
|
||
|
||
); <7>
|
||
}
|
||
----
|
||
|
||
<1> Configure `std::cout` to throw on error.
|
||
<2> Except if it throws, our `TryBlock` returns `0`, which will be returned from `main` to the OS.
|
||
<3> If any of the functions we call throws, `try_` will find an appropriate handler to invoke. We'll look at that later.
|
||
<4> From now on, all exceptions escaping this scope will automatically communicate the (now successfully parsed from the command line) file name (LEAF defines `struct e_file_name {std::string value;}`). It's as if every time one of the following functions throws an exception, `preload` says "wait, put this `e_file_name` thing with the exception, it's important!"
|
||
<5> `defer` is similar to `preload`, but instead of the error object, it takes a function that returns it. From this point on, when an exception escapes this scope, `defer` will call the passed function and include the returned `e_errno` with the exception (LEAF defines `struct e_errno {int value;}`).
|
||
<6> List of error handlers goes here. We'll see that later.
|
||
<7> This concludes the `try_` arguments -- as well as our program!
|
||
|
||
As it is always the case when using exception handling, as long as our `TryBlock` is exception-safe, we can concentrate on the "no errors" code path. Of course, our `TryBlock` is exception-safe, since `shared_ptr` will automatically close the file for us in case an exception is thrown.
|
||
|
||
Now let's look at the second part of the call to `try_`, which lists the error handlers:
|
||
|
||
[source,c++]
|
||
----
|
||
int main( int argc, char const * argv[ ] )
|
||
{
|
||
std::cout.exceptions(std::ostream::failbit | std::ostream::badbit); <1>
|
||
|
||
return leaf::try_(
|
||
[&]
|
||
{
|
||
.... <2>
|
||
},
|
||
|
||
[ ](leaf::catch_<input_file_open_error>, <3>
|
||
leaf::match<leaf::e_errno,ENOENT>,
|
||
leaf::e_file_name const & fn)
|
||
{
|
||
std::cerr << "File not found: " << fn.value << std::endl;
|
||
return 1;
|
||
},
|
||
|
||
[ ](leaf::catch_<input_file_open_error>, <4>
|
||
leaf::e_errno const & errn,
|
||
leaf::e_file_name const & fn )
|
||
{
|
||
std::cerr << "Failed to open " << fn.value << ", errno=" << errn << std::endl;
|
||
return 2;
|
||
},
|
||
|
||
[ ](leaf::catch_<input_error>, <5>
|
||
leaf::e_errno const & errn,
|
||
leaf::e_file_name const & fn )
|
||
{
|
||
std::cerr << "Failed to access " << fn.value << ", errno=" << errn << std::endl;
|
||
return 3;
|
||
},
|
||
|
||
[ ](leaf::catch_<std::ostream::failure>, <6>
|
||
leaf::e_errno const & errn )
|
||
{
|
||
std::cerr << "Output error, errno=" << errn << std::endl;
|
||
return 4;
|
||
},
|
||
|
||
[ ](leaf::catch_<bad_command_line>) <7>
|
||
{
|
||
std::cout << "Bad command line argument" << std::endl;
|
||
return 5;
|
||
},
|
||
|
||
[ ](leaf::error_info const & unmatched) <8>
|
||
{
|
||
std::cerr <<
|
||
"Unknown failure detected" << std::endl <<
|
||
"Cryptic diagnostic information follows" << std::endl <<
|
||
unmatched;
|
||
return 6;
|
||
} );
|
||
}
|
||
----
|
||
|
||
<1> Configure `std::cout` to throw on error.
|
||
|
||
<2> This is the `TryBlock` from the previous listing; if it throws, `try_` will catch the exception, then consider the error handlers that follow, in order, and it will call the first one that can deal with the error:
|
||
|
||
<3> This handler will be called if: +
|
||
pass:[•] an `input_file_open_error` exception was caught, with +
|
||
pass:[•] an object of type `leaf::e_errno` that has `.value` equal to `ENOENT`, and +
|
||
pass:[•] an object of type `leaf::e_file_name`.
|
||
|
||
<4> This handler will be called if: +
|
||
pass:[•] an `input_file_open_error` exception was caught, with +
|
||
pass:[•] an object of type `leaf::e_errno` (regardless of its `.value`), and +
|
||
pass:[•] an object of type `leaf::e_file_name`.
|
||
|
||
<5> This handler will be called if: +
|
||
pass:[•] an `input_error` exception was caught (which is a base type), with +
|
||
pass:[•] an object of type `leaf::e_errno` (regardless of its `.value`), and +
|
||
pass:[•] an object of type `leaf::e_file_name`.
|
||
|
||
<6> This handler will be called if: +
|
||
pass:[•] an `std::ostream::failure` exception was caught, with +
|
||
pass:[•] an object of type `leaf::e_errno` (regardless of its `.value`),
|
||
|
||
<7> This handler will be called if a `bad_command_line` exception was caught.
|
||
|
||
<8> If `try_` fails to find an appropriate handler, it will re-throw the exception. But this is the `main` function which should handle all exceptions, so this last handler matches any error and prints diagnostic information, to help debug logic errors.
|
||
|
||
To conclude this introduction, let's look at one of the error-reporting functions that our `TryBlock` calls, for example `file_open`:
|
||
|
||
[source,c++]
|
||
----
|
||
std::shared_ptr<FILE> file_open( char const * file_name )
|
||
{
|
||
if( FILE * f = fopen(file_name,"rb") )
|
||
return std::shared_ptr<FILE>(f,&fclose);
|
||
else
|
||
throw leaf::exception( input_file_open_error(), leaf::e_errno{errno} );
|
||
}
|
||
----
|
||
|
||
If `fopen` succeeds, it returns a `shared_ptr` which will automatically call `fclose` as needed. If `fopen` fails, we throw the exception object returned by `leaf::exception`, which takes as its first argument an exception object, followed by any number of error objects to send with it. In this case we pass the system `errno` (LEAF defines `struct e_errno {int value;}`). The returned object can be caught as `input_file_open_error`.
|
||
|
||
NOTE: `try_` works with any exception, not only exceptions thrown using `leaf::exception`.
|
||
|
||
TIP: The complete program from this tutorial is available https://github.com/zajo/leaf/blob/master/example/print_file_eh.cpp?ts=3[here]. The https://github.com/zajo/leaf/blob/master/example/print_file_result.cpp?ts=3[other] version of the same program does not use exception handling to report errors (see the <<introduction-result,previous introduction>>).
|
||
|
||
[[synopsis]]
|
||
== Synopsis
|
||
|
||
=== Reporting Errors
|
||
|
||
==== `error.hpp`
|
||
include::{sourcedir}/synopses/error.adoc[]
|
||
|
||
'''
|
||
|
||
==== `common.hpp`
|
||
include::{sourcedir}/synopses/common.adoc[]
|
||
|
||
'''
|
||
|
||
==== `result.hpp`
|
||
include::{sourcedir}/synopses/result.adoc[]
|
||
|
||
[.text-right]
|
||
<<get_error_code>> | <<get_error_id-error_code>>
|
||
|
||
'''
|
||
|
||
==== `throw.hpp`
|
||
include::{sourcedir}/synopses/throw.adoc[]
|
||
|
||
'''
|
||
|
||
==== `exception_to_result.hpp`
|
||
include::{sourcedir}/synopses/exception_to_result.adoc[]
|
||
|
||
[.text-right]
|
||
<<exception_to_result>>
|
||
|
||
'''
|
||
|
||
==== `preload.hpp`
|
||
include::{sourcedir}/synopses/preload.adoc[]
|
||
|
||
'''
|
||
|
||
=== Error Handling
|
||
|
||
==== `handle.hpp`
|
||
include::{sourcedir}/synopses/handle.adoc[]
|
||
|
||
'''
|
||
|
||
==== `try.hpp`
|
||
include::{sourcedir}/synopses/try.adoc[]
|
||
|
||
'''
|
||
|
||
=== Multi-Thread Programming
|
||
|
||
==== `capture_result.hpp`
|
||
include::{sourcedir}/synopses/capture_result.adoc[]
|
||
|
||
[.text-right]
|
||
<<capture_result>>
|
||
|
||
'''
|
||
|
||
==== `capture_exception.hpp`
|
||
include::{sourcedir}/synopses/capture_exception.adoc[]
|
||
|
||
[.text-right]
|
||
<<capture_exception>>
|
||
|
||
[[reference]]
|
||
== Common API
|
||
|
||
[[e_objects]]
|
||
=== E-Objects
|
||
|
||
[[is_error_type]]
|
||
==== `is_error_type`
|
||
|
||
With LEAF, users can efficiently associate with errors or with exceptions any number of values that pertain to a failure. These values may be of any no-throw movable type `E` for which `<<is_error_type,is_error_type>><E>::value` is `true`. The expectation is that this template will be specialized as needed for e.g. all user-defined error enums.
|
||
|
||
Throughout this text, types for which `is_error_type` is `true` are called E-types. Objects of those types are called error objects or E-objects.
|
||
|
||
The main `is_error_type` template is defined so that `is_error_type<E>::value` is `true` when `E` is:
|
||
|
||
* `std::error_code`,
|
||
* `boost::system::error_code`,
|
||
* `std::exception_ptr`,
|
||
* any type `E` for which `std::is_base_of<std::exception, E>::value` is `true`,
|
||
* any type which defines an accessible data member `value`.
|
||
|
||
Often, error values that need to be communicated are of generic types (e.g. `std::string`). Such values should be enclosed in a C-`struct` that acts as their compile-time identifier and gives them semantic meaning. Examples:
|
||
|
||
[source,c++]
|
||
----
|
||
struct e_input_name { std::string value; };
|
||
struct e_output_name { std::string value; };
|
||
|
||
struct e_minimum_temperature { float value; };
|
||
struct e_maximum_temperature { float value; };
|
||
----
|
||
|
||
By convention, the enclosing C-`struct` names use the `e_` prefix.
|
||
|
||
'''
|
||
|
||
[[propagation]]
|
||
==== Propagation
|
||
|
||
"To propagate" an E-object is to associate it with a particular <<error_id>>, making it available to functions that handle that error.
|
||
|
||
More formally, when an E-object is propagated, it is immediately moved to available storage in a `handle_some`, a `handle_all` or a `try_` scope currently active in the calling thread, where it becomes uniquely associated with a specific <<error_id>> -- or discarded if storage is not available; see <<handle_some>>.
|
||
|
||
Various LEAF functions take a list of E-objects to propagate. As an example, if a function `copy_file` that takes the name of the input file and the name of the output file as its arguments detects a failure, it could communicate an error code `ec`, plus the two relevant file names using <<new_error>>:
|
||
|
||
[source,c++]
|
||
----
|
||
return leaf::new_error( ec, e_input_name{n1}, e_output_name{n2} );
|
||
----
|
||
|
||
'''
|
||
|
||
[[diagnostic_information]]
|
||
==== Diagnostic Information
|
||
|
||
LEAF is able to automatically generate diagnostic messages that include information about all E-objects available to error handlers. For this purpose, it needs to be able to print objects of user-defined E-types.
|
||
|
||
To do this, LEAF attempts to bind an unqualified call to `operator<<`, passing a `std::ostream` and the E-object. If that fails, it will also attempt to bind `operator<<` that takes the `.value` of the E-object. If that also doesn't compile, the E-object will not appear in diagnostic messages, though LEAF will still print its type.
|
||
|
||
Even with E-types that define a printable `.value`, the user may still want to overload `operator<<` for the enclosing `struct`, e.g.:
|
||
|
||
[source,c++]
|
||
----
|
||
struct e_errno
|
||
{
|
||
int value;
|
||
|
||
friend std::ostream & operator<<( std::ostream & os, e_errno const & e )
|
||
{
|
||
return os << "errno = " << e.value << ", \"" << strerror(e.value) << '"';
|
||
}
|
||
};
|
||
----
|
||
|
||
The `e_errno` type above is designed to hold `errno` values. The defined `operator<<` overload will automatically include the output from `strerror` when `e_errno` values are printed (LEAF defines `e_errno` in `<boost/leaf/common.hpp>`, together with other commonly-used error types).
|
||
|
||
TIP: These automatically-generated diagnostic messages are developer-friendly, but not user-friendly. Therefore, `operator<<` overloads for E-types should only print technical information in English, and should not attempt to localize strings or to format a user-friendly message; this should be done in error-handling functions specifically designed for that purpose.
|
||
|
||
'''
|
||
|
||
[[e_api_function]]
|
||
==== `e_api_function`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
struct e_api_function {char const * value;};
|
||
|
||
} }
|
||
----
|
||
|
||
|
||
The `e_api_function` type is designed to capture the name of the API function that failed. For example, if you're reporting an error from `fread`, you could use `leaf::e_api_function {"fread"}`.
|
||
|
||
WARNING: The passed value is stored as a C string (`char const *`), so `value` should only be initialized with a string literal.
|
||
|
||
'''
|
||
|
||
[[e_file_name]]
|
||
==== `e_file_name`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
struct e_file_name {std::string value;};
|
||
|
||
} }
|
||
----
|
||
|
||
When a file operation fails, you could use `e_file_name` to store the name of the file.
|
||
|
||
'''
|
||
|
||
[[e_errno]]
|
||
==== `e_errno`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
struct e_errno
|
||
{
|
||
int value;
|
||
friend std::ostream & operator<<( std::ostream & os, e_errno const & err );
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
To capture `errno`, use `e_errno`. When printed in automatically-generated diagnostic messages, `e_errno` objects use `strerror` to convert the `errno` code to string.
|
||
|
||
'''
|
||
|
||
[[e_LastError]]
|
||
==== `e_LastError`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
namespace windows
|
||
{
|
||
struct e_LastError
|
||
{
|
||
unsigned value;
|
||
friend std::ostream & operator<<( std::ostream & os, e_LastError const & err );
|
||
};
|
||
}
|
||
|
||
} }
|
||
----
|
||
|
||
`e_LastError` is designed to communicate `GetLastError()` values on Windows.
|
||
|
||
'''
|
||
|
||
[[e_at_line]]
|
||
==== `e_at_line`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
struct e_at_line { int value; };
|
||
|
||
} }
|
||
----
|
||
|
||
`e_at_line` can be used to communicate the line number when reporting errors (for example parse errors) about a text file.
|
||
|
||
'''
|
||
|
||
[[e_type_info_name]]
|
||
==== `e_type_info_name`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
struct e_type_info_name { char const * value; };
|
||
|
||
} }
|
||
----
|
||
|
||
`e_type_info_name` is designed to store the return value of `std::type_info::name`.
|
||
|
||
'''
|
||
|
||
[[e_source_location]]
|
||
==== `e_source_location`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
struct e_source_location
|
||
{
|
||
char const * const file;
|
||
int const line;
|
||
char const * const function;
|
||
|
||
friend std::ostream & operator<<( std::ostream & os, e_source_location const & x );
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
The <<LEAF_ERROR>>, <<LEAF_EXCEPTION>> and <<LEAF_THROW>> macros capture `pass:[__FILE__]`, `pass:[__LINE__]` and `pass:[__FUNCTION__]` into a `e_source_location` object.
|
||
|
||
'''
|
||
|
||
[[error_id]]
|
||
=== `error_id`
|
||
|
||
include::{sourcedir}/synopses/error.adoc[]
|
||
|
||
The `error_id` type derives publicly from `std::error_code`. It does not add any data members, so objects of type `error_id` are as efficient as objects of type `std::error_code`.
|
||
|
||
Values of type `error_id` identify an error across the entire program. They can be copied, moved, assigned to, and compared to other `error_id` or `std::error_code` objects.
|
||
|
||
'''
|
||
|
||
[[error_id::error_id]]
|
||
==== Constructors
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
error_id::error_id() noexcept = default;
|
||
|
||
template <class... E>
|
||
error_id::error_id( std::error_code const & ec, E && ... e ) noexcept;
|
||
|
||
template <class... E>
|
||
error_id::error_id( std::error_code && ec, E && ... e ) noexcept;
|
||
|
||
struct e_original_ec { std::error_code value; };
|
||
|
||
} }
|
||
----
|
||
|
||
A default-initialized `error_id` object does not represent an error condition. It compares equal to any other default-initialized `error_id` or default-initialized `std::error_code` object.
|
||
|
||
All other `error_id` objects use a `std::error_category` of unspecified type. All such objects -- as well as their `std::error_code` slice -- have special semantics recognized by <<handle_all>>, <<handle_some>> or <<try_>>. Of course, these functions are able to deal with any other `std::error_code`.
|
||
|
||
[TIP]
|
||
--
|
||
The special `error_id` semantics are encoded in the `std::error_code` value. To check if a given `std::error_code` has these semantics, use <<is_error_id>>.
|
||
|
||
The purpose of the `error_id` type is to reflect these semantics in the {CPP} type system; that is, we can say:
|
||
|
||
[source,c++]
|
||
----
|
||
void f( leaf::error_id const & ec )
|
||
{
|
||
assert(is_error_id(ec) || !ec);
|
||
}
|
||
----
|
||
--
|
||
|
||
Typically, users create new `error_id` objects by invoking <<new_error>>. The constructors that take `std::error_code` have the following effects:
|
||
|
||
* If `ec.value()` is `0`, the `std::error_code` subobject of `*this` is default-initialized;
|
||
* Otherwise, if `<<is_error_id,is_error_id>>(ec)` is `true`, the `std::error_code` subobject of `*this` is initialized by copying or moving from `ec`;
|
||
* Otherwise:
|
||
** `*this` is initialized by the value returned by <<new_error>>, followed by `propagate(e_original_ec{ec}, std::forward<E>(e)...)`;
|
||
** `ec` is discarded.
|
||
|
||
TIP: In a handler passed to <<handle_some>>, <<handle_all>> or <<try_>>, the original `ec` value passed to the `error_id` `std::error_code` constructors can be recovered by taking an argument of type `e_original_ec`.
|
||
|
||
'''
|
||
|
||
[[new_error]]
|
||
==== `new_error`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... E>
|
||
error_id new_error( E && ... e ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: :: `<<is_error_type,is_error_type>><E>::value` must be `true` for each `E`.
|
||
|
||
Effects: :: Each of the `e...` objects is <<propagation,propagated>> and uniquely associated with the returned value.
|
||
|
||
Returns: :: A new `error_id` value, which is unique across the entire program.
|
||
|
||
Postcondition: :: `ec.value()!=0`, where `ec` is the returned `error_id`.
|
||
|
||
'''
|
||
|
||
[[next_error]]
|
||
==== `next_error`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
error_id next_error() noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: The `error_id` value which will be returned the next time <<new_error>> is invoked from the calling thread.
|
||
|
||
This function can be used to associate E-objects with the next `error_id` value to be reported. Use with caution, only when restricted to reporting errors via specific third-party types, incompatible with LEAF -- for example when reporting an error from a C callback. As soon as control exits this critical path, you should create a <<new_error>> (which will be equal to the `error_id` object returned by the earlier call to `next_error`).
|
||
|
||
'''
|
||
|
||
[[last_error]]
|
||
==== `last_error`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
error_id last_error() noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: The `error_id` value returned the last time <<new_error>> was invoked from the calling thread.
|
||
|
||
'''
|
||
|
||
[[propagate]]
|
||
==== `propagate`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... E>
|
||
error_id error_id::propagate( E && ... e ) const noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Effects: :: Each of the `e...` objects is <<propagation,propagated>> and uniquely associated with `*this`.
|
||
|
||
Returns: :: `*this`.
|
||
|
||
'''
|
||
|
||
[[preload]]
|
||
=== `preload`
|
||
|
||
.#include <boost/leaf/preload.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... E>
|
||
<<unspecified-type>> preload( E && ... e ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: :: `<<is_error_type,is_error_type>><E>::value` must be `true` for each `E`.
|
||
|
||
Effects: :: All `e...` objects are forwarded and stored into the returned object of unspecified type, which should be captured by `auto` and kept alive in the calling scope. When that object is destroyed:
|
||
* If <<new_error>> was invoked (by the calling thread) since the object returned by `preload` was created, the stored `e...` objects are <<propagation,propagated>> and become uniquely associated with the _last_ such `leaf::error_id`;
|
||
* Otherwise, if `std::unhandled_exception()` returns `true`, the stored `e...` objects are propagated and will become uniquely associated with the _first_ `leaf::error_id` created later on;
|
||
* Otherwise, the stored `e...` objects are discarded.
|
||
|
||
'''
|
||
|
||
[[defer]]
|
||
=== `defer`
|
||
|
||
.#include <boost/leaf/preload.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... F>
|
||
<<unspecified-type>> defer( F && ... f ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: :: Each of `f~i~` in `f...` must be a function that does not throw exceptions, takes no arguments and returns an object of a no-throw movable type `E~i~` for which `<<is_error_type,is_error_type>><E~i~>::value` is `true`.
|
||
|
||
Effects: :: All `f...` objects are forwarded and stored into the returned object of unspecified type, which should be captured by `auto` and kept alive in the calling scope. When that object is destroyed:
|
||
+
|
||
--
|
||
* If <<new_error>> was invoked (by the calling thread) since the object returned by `defer` was created, each of the stored `f...` is called, and each returned object is <<propagation,propagated>> and becomes uniquely associated with the _last_ such `leaf::error_id`;
|
||
* Otherwise, if `std::unhandled_exception()` returns `true`, each of the stored `f...` is called, and each returned object is propagated and will become uniquely associated with the _first_ `leaf::error_id` created later on;
|
||
--
|
||
+
|
||
The stored `f...` objects are discarded.
|
||
|
||
'''
|
||
|
||
[[error_info]]
|
||
=== `error_info`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
class error_info
|
||
{
|
||
//Constructors unspecified
|
||
|
||
public:
|
||
|
||
leaf::error_id const & error() const noexcept;
|
||
|
||
bool exception_caught() const noexcept;
|
||
|
||
std::exception const * exception() const noexcept;
|
||
|
||
friend std::ostream & operator<<( std::ostream & os, error_info const & x );
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
Handlers passed to <<handle_some>>, <<handle_all>> or <<try_>> may take an argument of type `error_info const &` to receive information about the error.
|
||
|
||
The `error` member function returns the program-wide unique <<error_id>> of the error.
|
||
|
||
The `exception_caught` member function returns `true` if the handler that received `*this` is being invoked by <<try_>>, `false` if invoked by <<handle_some>> or <<handle_all>>.
|
||
|
||
The `exception` member function returns a pointer to the `std::exception` subobject of the exception caught by `try_`, or `0` if that exception could not be converted to `std::exception`. It is illegal to call `exception` unless `exception_caught()` is `true`.
|
||
|
||
The `operator<<` overload prints diagnostic information about each E-object currently stored in any active <<handle_some>>, <<handle_all>> or <<try_>> scope, but only if it is associated with the <<error_id>> returned by `error()`.
|
||
|
||
'''
|
||
|
||
[[diagnostic_info]]
|
||
=== `diagnostic_info`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
class diagnostic_info
|
||
{
|
||
//Constructors unspecified
|
||
|
||
public:
|
||
|
||
friend std::ostream & operator<<( std::ostream & os, diagnostic_info const & x );
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
Handlers passed to <<handle_some>>, <<handle_all>> or <<try_>> may take an argument of type `diagnostic_info const &` if they need to print diagnostic information about the error.
|
||
|
||
The message printed by `operator<<` includes the message printed by `error_info`, followed by basic information about E-objects that were communicated to LEAF (to be associated with the error) for which there was no storage available in any active <<handle_some>>, <<handle_all>> or <<try_>> scope (these E-objects were discarded by LEAF, because no handler needed them).
|
||
|
||
The additional information is limited the type name of the first such E-object, as well as their total count.
|
||
|
||
'''
|
||
|
||
[[verbose_diagnostic_info]]
|
||
=== `verbose_diagnostic_info`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
class verbose_diagnostic_info
|
||
{
|
||
//Constructors unspecified
|
||
|
||
public:
|
||
|
||
friend std::ostream & operator<<( std::ostream & os, verbose_diagnostic_info const & x );
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
Handlers passed to <<handle_some>>, <<handle_all>> or <<try_>> may take an argument of type `verbose_diagnostic_info const &` if they need to print diagnostic information about the error.
|
||
|
||
The message printed by `operator<<` includes the message printed by `error_info`, followed by information about E-objects that were communicated to LEAF (to be associated with the error) for which there was no storage available in any active <<handle_some>>, <<handle_all>> or <<try_>> scope (these E-objects were discarded by LEAF, because no handler needed them).
|
||
|
||
The additional information includes the types and the values of all such E-objects.
|
||
|
||
WARNING: Using `verbose_diagnostic_info` will likely allocate memory dynamically.
|
||
|
||
'''
|
||
|
||
[[failed]]
|
||
=== `failed`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class R>
|
||
class failed
|
||
{
|
||
R value;
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
Handlers passed to <<handle_some>> or <<handle_all>> may take an argument of type `failed<R>`, where `R` is the type returned by the `try_block` passed to <<handle_some>> or <<handle_all>>, if they need to use the returned object (which indicated a failure).
|
||
|
||
== Noexcept API
|
||
|
||
[[result_requirements]]
|
||
|
||
=== "`result<T>`" Requirements
|
||
|
||
The error-handling functionality provided by <<handle_some>>, <<handle_all>> and <<try_>> -- including the ability to <<propagation,propagate>> error objects of arbitrary types -- is compatible with any external "`result<T>`" type R, as long as it satisfies the following requirements:
|
||
|
||
* An object `r` of type `R` can be initialized with a value "`T`" to indicate success, or with a `std::error_code` to indicate a failure;
|
||
* `bool(r)` is `true` if `r` indicates success, which means that it is valid to call `r.value()` to recover the "`T`" value, and `false` otherwise, in which case it is valid to call `r.error()` to recover the `std::error_code`.
|
||
|
||
If possible, the `std::error_code` should be of type <<error_id>> (which derives publicly from `std::error_code`), however this is not a requirement.
|
||
|
||
Naturally, the provided <<result>> class template satisfies these requirements. In addition, it allows error objects to be transported across thread boundaries.
|
||
|
||
'''
|
||
|
||
[[result]]
|
||
=== `result`
|
||
|
||
include::{sourcedir}/synopses/result.adoc[]
|
||
|
||
The `result<T>` type can be returned by functions which produce a value of type `T` but may fail doing so.
|
||
|
||
Requirements: :: `T` must be movable, and its move constructor may not throw.
|
||
|
||
Invariant: :: A `result<T>` object is in one of three states:
|
||
* Value state, in which case it contains an object of type `T`, and `<<result::value,value>>`/`<<result::value,operator*>>` can be used to access the contained value.
|
||
* Error state, in which case it contains an object of type <<error_id>>, and calling `<<result::value,value>>`/`<<result::value,operator*>>` throws `leaf::<<bad_result,bad_result>>`.
|
||
* Error-capture state, which is the same as the Error state, but in addition to the <<error_id>> object, it holds captured E-objects.
|
||
|
||
'''
|
||
|
||
[[result::result]]
|
||
==== Constructors
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class T>
|
||
result<T>::result() noexcept;
|
||
|
||
template <class T>
|
||
result<T>::result( T && v ) noexcept;
|
||
|
||
template <class T>
|
||
result<T>::result( T const & v );
|
||
|
||
template <class T>
|
||
result<T>::result( leaf::error_id const & err ) noexcept;
|
||
|
||
template <class T>
|
||
result<T>::result( result && ) noexcept;
|
||
|
||
template <class T>
|
||
result<T>::result( result const & );
|
||
|
||
template <class T>
|
||
template <class U>
|
||
result<T>::result( result<U> && ) noexcept;
|
||
|
||
template <class T>
|
||
template <class U>
|
||
result<T>::result( result<U> const & );
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: :: `T` must be movable, and its move constructor may not throw.
|
||
|
||
Effects: ::
|
||
|
||
Establishes the `result<T>` invariant:
|
||
* To get a `result<T>` in <<result,Value state>>, initialize it with an object of type `T` or use the default constructor.
|
||
* To get a `result<T>` in <<result,Error state>>, initialize it with an <<error_id>> object.
|
||
* To get a `result<T>` in <<result,Error-capture state>>, call <<capture_result>>.
|
||
|
||
Throws: ::
|
||
* Initializing the `result<T>` in Value state may throw, depending on which constructor of `T` is invoked;
|
||
* Copying a `result<T>` in Value state throws any exceptions thrown by the `T` copy constructor;
|
||
* Other constructors do not throw.
|
||
|
||
TIP: A `result` that is in value state converts to `true` in boolean contexts. A `result` that is not in value state converts to `false` in boolean contexts.
|
||
|
||
'''
|
||
|
||
[[result::operator_eq]]
|
||
==== `operator=`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class T>
|
||
result<T> & result<T>::operator=( result && ) noexcept;
|
||
|
||
template <class T>
|
||
result<T> & result<T>::operator=( result const & );
|
||
|
||
template <class T>
|
||
template <class U>
|
||
result<T> & result<T>::operator=( result<U> && ) noexcept;
|
||
|
||
template <class T>
|
||
template <class U>
|
||
result<T> & result<T>::operator=( result<U> const & );
|
||
|
||
} }
|
||
----
|
||
|
||
Effects: :: Destroys `*this`, then re-initializes it using the appropriate `result<T>` constructor. Basic exception-safety guarantee.
|
||
|
||
'''
|
||
|
||
[[result::operator_bool]]
|
||
==== Conversion to `bool`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class T>
|
||
result<T>::operator bool() const noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: If `*this` is in <<result,value state>>, returns `true`, otherwise returns `false`.
|
||
|
||
'''
|
||
|
||
[[result::value]]
|
||
==== `value`/`operator*`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class T>
|
||
T const & result<T>::value() const;
|
||
|
||
template <class T>
|
||
T & result<T>::value();
|
||
|
||
template <class T>
|
||
T const & result<T>::operator*() const;
|
||
|
||
template <class T>
|
||
T & result<T>::operator*();
|
||
|
||
} }
|
||
----
|
||
|
||
Effects: :: If `*this` is in <<result,value state>>, returns a reference to the stored value, otherwise throws `leaf::<<bad_result,bad_result>>`.
|
||
|
||
'''
|
||
|
||
[[result::error]]
|
||
==== `error`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class T>
|
||
template <class... E>
|
||
error_id result<T>::error( E && ... e ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
This member function is designed for use in `return` statements in functions that return `result<T>` (or `leaf::<<error_id,error_id>>`) to return an error to the caller.
|
||
|
||
Returns: ::
|
||
* If `*this` is in <<result,value state>>, returns `<<new_error,new_error>>(std::forward<E>(e...))`, which creates a new error (as opposed to forwarding an existing error);
|
||
* If `*this` is in <<result,error-capture state>>, all captured E-objects are <<propagation,propagated>>, `*this` is converted to error state, and then
|
||
* If `*this` is in <<result,error state>>, returns `err.<<propagate,propagate>>(std::forward<E>(e...))`, where `err` is the <<error_id>> stored in `*this`.
|
||
|
||
'''
|
||
|
||
[[bad_result]]
|
||
==== `bad_result`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
struct bad_result: std::exception { };
|
||
|
||
} }
|
||
----
|
||
|
||
This exception is thrown by `<<result::value,value>>()`/`<<result::value,operator*>>()` if they're invoked for a `result` object that is in not in <<result,value state>>.
|
||
|
||
'''
|
||
|
||
[[handle_some]]
|
||
=== `handle_some`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class TryBlock, class... Handler>
|
||
decltype(std::declval<TryBlock>()())
|
||
handle_some( TryBlock && try_block, Handler && ... handler );
|
||
|
||
} }
|
||
----
|
||
|
||
The `handle_some` function template can be used to recognize and handle some errors, forwarding any unrecognized errors to the caller.
|
||
|
||
Requirements: ::
|
||
|
||
* The `try_block` must be a function callable with no arguments that returns a type `R` that satisfies the <<result_requirements>>;
|
||
* All `handler...` arguments must return (possibly different) types that can be used to initialize an object of the type `R`;
|
||
* Each of the `handler...` functions:
|
||
** may take arguments of <<e_objects,E-types>>, either by value or by `const &`, or as a `const *`;
|
||
** may take arguments, either by value or by `const &`, of the predicate type `<<match,match>><E>`, where `E` is an E-type;
|
||
** may take an <<error_info>> argument by `const &`;
|
||
** may take a <<diagnostic_info>> argument by `const &`;
|
||
** may take a <<verbose_diagnostic_info>> argument by `const &`;
|
||
** may take a `<<failed,failed>><R>` argument;
|
||
** may not take any other types of arguments.
|
||
|
||
Effects: ::
|
||
|
||
When `handle_some` is invoked, LEAF reserves space inside of the `handle_some` scope (using automatic storage duration) to store the E-objects it would pass to the appropriate `handler` in case the `try_block` indicates an error.
|
||
+
|
||
The list of E-types that `handle_some` needs to store internally is deduced automatically, using the following steps, in order:
|
||
+
|
||
--
|
||
* A type list is deduced by concatenating all argument types of all `handler...` functions;
|
||
* References and cv-qualifiers are stripped from each type in the list;
|
||
* The <<error_info>>, <<diagnostic_info>>, <<verbose_diagnostic_info>> types, and any instance of the `<<failed,failed>><R>` template, are removed from the list;
|
||
* Any duplicate types are removed.
|
||
--
|
||
+
|
||
Next, `handle_some` calls the `try_block` function, which generally would call other functions as needed. When any function needs to report an error, it passes one or more E-objects to LEAF. For example, a function that returns R can report an error with a call to <<new_error>> in a `return` expression:
|
||
+
|
||
[source,c++]
|
||
----
|
||
if( file-open-fails )
|
||
return leaf::new_error( error_code::file_open_error, e_file_name{fn} );
|
||
----
|
||
+
|
||
As each E-type object is passed to LEAF, it is immediately moved to the `handle_some` (or <<handle_all>>, or <<try_>>) scope highest in the call stack that has available storage for that type, where it is uniquely associated with a specific <<error_id>> (in this case, the value returned by `new_error`). Objects of E-types for which none of these scopes have available storage are discarded.
|
||
|
||
Return Value: ::
|
||
If `bool(r)` is `true`, where `r` is the object returned by the `try_block` function, `handle_some` returns `r`.
|
||
+
|
||
Otherwise, `handle_some` considers each of the `handler...` functions, in order, until it finds one that matches the reported `r.error()`. The first matching handler is invoked and its return value is used to initialize the return value of `handle_some`, which can indicate success if the handler was able to handle the error, or failure if it was not.
|
||
+
|
||
If the value `handle_some` is returning indicates success, all stored E-objects are discarded. Otherwise, each stored E-object is moved to appropriate storage available in `handle_some` (or <<handle_all>>, or <<try_>>) scopes higher up the call stack (each moved object retains its unique association with its specific `error_id` value). E-objects for which such storage is not available are discarded.
|
||
|
||
Handler Matching Procedure: ::
|
||
+
|
||
A `handler` matches the reported failure iff `handle_some` is able to produce values to pass as its arguments. As soon as it is determined that an argument value can not be produced, the current `handler` is dropped and the matching procedure continues with the next `handler`, if any.
|
||
+
|
||
Because each error object stored in the `handle_some` scope is associated with a specific unique <<error_id>> value, the first thing `handle_some` must do is call `r.error()` to obtain an error ID key to match error objects with:
|
||
+
|
||
--
|
||
* If `<<is_error_id,is_error_id>>(e)` is true, where `e` is the return value of `r.error()`, the error ID is encoded dynamically in `e` and is readily available,
|
||
* Otherwise, `handle_some` proceeds in a mode where only the following types of handler arguments can match: `error_info`, `diagnostic_info`, `verbose_diagnostic_info`, or `failed<R>`, where `R` is the return type of the `try_block`.
|
||
--
|
||
+
|
||
If `err` is the obtained error ID, each argument value `a`~i~ to be passed to the `handler` currently under consideration is produced as follows:
|
||
+
|
||
--
|
||
* If `a`~i~ is taken as `A`~i~ `const &` or by value:
|
||
** If an E-object of type `A`~i~, associated with `err`, is currently stored in the `handle_some` scope, `a`~i~ is initialized with a reference to the stored object; otherwise the handler is dropped.
|
||
+
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
....
|
||
return leaf::handle_some(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::e_file_name const& fn ) -> leaf::result<void> <1>
|
||
{
|
||
std::cerr << "File Name: \"" << fn.value << '"' << std::endl;
|
||
return { }; <3>
|
||
} );
|
||
----
|
||
<1> This handler will match only if there is an `e_file_name` associated with the error.
|
||
<2> Print the file name.
|
||
<3> Return success out of `handle_some`.
|
||
+
|
||
** If `A`~i~ is of the predicate type `<<match,match>><E>`, if an object of type `E`, associated with `err`, is currently stored in the `handle_some` scope, `a`~i~ is initialized with a reference to the stored object; otherwise the handler is dropped. The handler is also dropped if the expression `a`~i~`()` evaluates to `false`.
|
||
+
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
enum class errors
|
||
{
|
||
ec1=1,
|
||
ec2,
|
||
ec3
|
||
};
|
||
|
||
....
|
||
|
||
return leaf::handle_some(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::match<errors, errors::ec1> ) <1>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::errors ec ) <2>
|
||
{
|
||
....
|
||
} );
|
||
----
|
||
<1> This handler matches if the error includes an object of type `errors` with value `ec1`.
|
||
<2> This handler matches if the error includes an object of type `errors` regardless of its value.
|
||
+
|
||
* If `a`~i~ is of type `A`~i~ `const pass:[*]`, `handle_some` is always able to produce it: if an E-object of type `A`~i~, associated with `err`, is currently stored in the `handle_some` scope, `a`~i~ is initialized with the address of the stored object, otherwise it is initialized with `0`.
|
||
+
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
....
|
||
return leaf::handle_some(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::e_file_name const* fn ) -> leaf::result<void> <1>
|
||
{
|
||
if( fn ) <2>
|
||
std::cerr << "File Name: \"" << fn->value << '"' << std::endl;
|
||
return { }; <3>
|
||
} );
|
||
----
|
||
<1> This handler matches any error, because it takes `e_file_name` as a `const *` (and nothing by `const &`).
|
||
<2> If an `e_file_name` is available with the current error, print it.
|
||
<3> Return success out of `handle_some`.
|
||
+
|
||
* If `a`~i~ is of type `error_info const &`, `handle_some` is always able to produce it.
|
||
+
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
....
|
||
return leaf::handle_some(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::error_info const & info ) <1>
|
||
{
|
||
std::cerr << "Diagnostic Information:" << std::endl << info; <2>
|
||
return info.error(); <3>
|
||
} );
|
||
----
|
||
<1> This handler matches any error.
|
||
<2> Print diagnostic information.
|
||
<3> Return the error, which will be returned out of `handle_some` (in a `result<void>`).
|
||
+
|
||
* If `a`~i~ is of type `diagnostic_info const &`, `handle_some` is always able to produce it.
|
||
+
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
....
|
||
return leaf::handle_some(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::verbose_diagnostic_info const & info ) <1>
|
||
{
|
||
std::cerr << "Extended Diagnostic Information:" << std::endl << info; <2>
|
||
return info.error(); <3>
|
||
} );
|
||
----
|
||
<1> This handler matches any error.
|
||
<2> Print extended diagnostics, including limited information about dropped error objects.
|
||
<3> Return the error, which will be returned out of `handle_some` (in a `result<void>`).
|
||
+
|
||
* If `a`~i~ is of type `<<failed,failed>><R>`, where `R` is the type returned by the `try_block`, `handle_some` is always able to produce it.
|
||
+
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
....
|
||
return leaf::handle_some(
|
||
|
||
[ ]() -> result<void> <1>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::match<my_errors,ec1> ) -> result<void>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::failed<result<void>> && r ) -> result<void> <2>
|
||
{
|
||
assert(!r);
|
||
....
|
||
return { }; <3>
|
||
} );
|
||
----
|
||
<1> The `try_block` returns some `result<void>` (see <<result_requirements>>), which...
|
||
<2> ...presumably didn't match other handlers, so it is passed to this last handler for inspection/logging, and then...
|
||
<3> ...we return success out of `handle_some`.
|
||
+
|
||
* It is illegal to pass to `handle_some` a `handler` that takes any other argument types.
|
||
--
|
||
|
||
TIP: Because `handle_some` can always produce arguments of type `error_info const &`, `diagnostic_info const &`, `verbose_diagnostic_info const &` and `failed<R>`, a handler that only takes arguments of these types will match any error.
|
||
|
||
'''
|
||
|
||
[[handle_all]]
|
||
=== `handle_all`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class TryBlock, class... Handler>
|
||
typename std::remove_reference<decltype(std::declval<TryBlock>()().value())>::type
|
||
handle_all( TryBlock && try_block, Handler && ... handler );
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: ::
|
||
|
||
`handle_all` has the same requirements as <<handle_some>>, with the following change:
|
||
+
|
||
--
|
||
* If `r` is the object returned by the `try_block` and `T` is the type returned by `r.value()` (see <<result_requirements>>), all `handler...` arguments must be functions that return objects of (possibly different) types that can be used to initialize an object of type `T`.
|
||
--
|
||
And the following addition:
|
||
* At least one `handler` function (usually the last) must match any error. This is enforced at compile-time.
|
||
|
||
Return Value: ::
|
||
* If `bool(r)` is `true`, where `r` is the result object returned by the `try_block`, returns `r.value()`;
|
||
* Otherwise the value returned by the first matched (using the same matching procedure as <<handle_some>>) handler is used to initialize the return value (of type `T`) of `handle_all`.
|
||
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
int main()
|
||
{
|
||
return leaf::handle_all(
|
||
[ ]() -> leaf::result<int>
|
||
{
|
||
//do work, bail out returning leaf::error_id for any failure
|
||
....
|
||
|
||
std::cout << "Success!";
|
||
return 0;
|
||
},
|
||
[ ]
|
||
{
|
||
std::cerr << "Error!";
|
||
return 1;
|
||
} );
|
||
}
|
||
----
|
||
|
||
'''
|
||
|
||
[[match]]
|
||
=== `match`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class E, typename deduced-type<E>::type... V>
|
||
struct match
|
||
{
|
||
using type = typename deduced-type<E>::type;
|
||
type const & value;
|
||
|
||
explicit match( E const & e ) noexcept;
|
||
|
||
bool operator()() const noexcept;
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
NOTE: The `match` template is useful only as argument to a handler function passed to <<handle_some>> (or `handle_all` or `try_`).
|
||
|
||
Effects: ::
|
||
|
||
* If `E` defines an accessible data member `value`:
|
||
** The type of the parameter pack `V...` is deduced as `decltype(std::declval<E>().value)`;
|
||
** The `match` constructor initializes the `value` reference with `e.value`.
|
||
|
||
* Otherwise:
|
||
** The type of the parameter pack `V...` is deduced as `E`;
|
||
** The `match` constructor initializes the `value` reference with `e`.
|
||
|
||
The `match` template is a predicate function type: `operator()` returns `true` iff the expression `value == V~i~` is `true` for at least one `V`~i~ in `V...`.
|
||
|
||
.Example 1:
|
||
[source,c++]
|
||
----
|
||
struct error_code { int value; };
|
||
|
||
error_code e = {42};
|
||
|
||
match<error_code, 1> m1(e);
|
||
assert(!m1());
|
||
|
||
match<error_code, 42> m2(e);
|
||
assert(m2());
|
||
|
||
match<error_code, 1, 5, 42, 7> m3(e);
|
||
assert(m3());
|
||
|
||
match<error_code, 1, 3, -42> m4(e);
|
||
assert(!m4());
|
||
----
|
||
|
||
.Example 2:
|
||
[source,c++]
|
||
----
|
||
enum error_code { e1=1, e2, e3 };
|
||
|
||
error_code e = e2;
|
||
|
||
match<error_code, e1> m1(e);
|
||
assert(!m1());
|
||
|
||
match<error_code, e2> m2(e);
|
||
assert(m2());
|
||
|
||
match<error_code, e1, e2> m3(e);
|
||
assert(m3());
|
||
|
||
match<error_code, e1, e3> m4(e);
|
||
assert(!m4());
|
||
----
|
||
|
||
'''
|
||
|
||
[[capture_result]]
|
||
=== `capture_result`
|
||
|
||
.#include <boost/leaf/capture_result.hpp>
|
||
include::{sourcedir}/synopses/capture_result.adoc[]
|
||
|
||
The `capture_result` function can be used to transport E-objects <<propagation,propagated>> when `f` is invoked in one thread, to a different thread which handles errors through <<handle_some>> or <<handle_all>>.
|
||
|
||
Requirements: ::
|
||
* `F` must be a function type that returns a `<<result,result>><T>`;
|
||
* Each of the `E...` types must be an <<e_objects,E-type>>.
|
||
|
||
Returns: :: A function object `fw` of unspecified type, which acts as a wrapper for `f`, taking the same argument types and returning the same `result<T>` type.
|
||
+
|
||
When the caller invokes `fw` (presumably from a worker thread) with arguments `a...` of types `A...`:
|
||
+
|
||
--
|
||
* Like <<handle_some>>, `fw` reserves storage in its scope for objects of the `E...` types, then
|
||
* invokes `f(std::forward<A>(a...))`, which in general will call other functions as needed (any <<propagation,propagated>> objects of the `E...` types are captured in the reserved storage).
|
||
--
|
||
+
|
||
If the returned `result<T>` indicates success, it is forwarded to the caller; any E-objects stored in the `fw` scope are discarded.
|
||
+
|
||
Otherwise, all stored E-objects are moved to dynamically-allocated memory, which is used to initialize a `result<T>` object in <<result,error-capture state>>. This object is returned to the caller of `fw`.
|
||
+
|
||
The returned `result<T>` can safely cross thread boundaries (presumably sent back to the main thread). When it is returned from the `try_block` function passed to <<handle_some>> or <<handle_all>>, its contents are <<propagation,propagated>> (in that thread), and then error handling proceeds as usual.
|
||
|
||
'''
|
||
|
||
[[LEAF_ERROR]]
|
||
=== `LEAF_ERROR`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
#define LEAF_ERROR(...) <<unspecified>>
|
||
----
|
||
|
||
Effects: :: `LEAF_ERROR(e...)` is equivalent to `leaf::<<new_error,new_error>>(e...)`, except the current source location is automatically passed to `new_error` in a `<<common,e_source_location>>` object (in addition to all `e...` objects).
|
||
|
||
'''
|
||
|
||
[[LEAF_AUTO]]
|
||
=== `LEAF_AUTO`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
#define LEAF_AUTO(v,r)\
|
||
auto _r_##v = r;\
|
||
if( !_r_##v )\
|
||
return _r_##v.error();\
|
||
auto & v = *_r_##v
|
||
----
|
||
|
||
`LEAF_AUTO` is useful when calling a function that returns `result<T>` (other than `result<void>`), if the desired behavior is to forward any errors to the caller verbatim.
|
||
|
||
Example:
|
||
|
||
.Compute two int values, return their sum as a float, using LEAF_AUTO:
|
||
[source,c++]
|
||
----
|
||
leaf::result<int> compute_value();
|
||
|
||
leaf::result<float> add_values()
|
||
{
|
||
LEAF_AUTO(v1, compute_value());
|
||
LEAF_AUTO(v2, compute_value());
|
||
return v1 + v2;
|
||
}
|
||
----
|
||
|
||
Of course, we could write `add_value` without using `LEAF_AUTO`. This is equivalent:
|
||
|
||
.Compute two int values, return their sum as a float, without LEAF_AUTO:
|
||
----
|
||
leaf::result<float> add_values()
|
||
{
|
||
auto v1 = compute_value();
|
||
if( !v1 )
|
||
return v1.error();
|
||
|
||
auto v2 = compute_value();
|
||
if( !v2 )
|
||
return v2.error();
|
||
|
||
return *v1 + *v2;
|
||
}
|
||
----
|
||
|
||
'''
|
||
|
||
[[LEAF_CHECK]]
|
||
=== `LEAF_CHECK`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
#define LEAF_CHECK(r)\
|
||
{\
|
||
auto _r = r;\
|
||
if(!_r)\
|
||
return _r.error();\
|
||
}
|
||
----
|
||
|
||
`LEAF_CHECK` is useful when calling a function that returns `result<void>`, if the desired behavior is to forward any errors to the caller verbatim.
|
||
|
||
Example:
|
||
|
||
.Try to send a message, then compute a value, report errors using LEAF_CHECK:
|
||
[source,c++]
|
||
----
|
||
leaf::result<void> send_message( char const * msg );
|
||
|
||
leaf::result<int> compute_value();
|
||
|
||
leaf::result<int> say_hello_and_compute_value()
|
||
{
|
||
LEAF_CHECK(send_message("Hello!"));
|
||
return compute_value();
|
||
}
|
||
----
|
||
|
||
Equivalent implementation without `LEAF_CHECK`:
|
||
|
||
.Try to send a message, then compute a value, report errors without LEAF_CHECK:
|
||
----
|
||
leaf::result<float> add_values()
|
||
{
|
||
auto r = send_message("Hello!");
|
||
if( !r )
|
||
return r.error();
|
||
|
||
return compute_value();
|
||
}
|
||
----
|
||
|
||
== Exception Handling API
|
||
|
||
[[exception]]
|
||
=== `exception`
|
||
|
||
[source,c++]
|
||
.#include <boost/leaf/exception.hpp>
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class Ex, class... E>
|
||
<<unspecified>> exception( Ex && ex, E && ... e ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: ::
|
||
* `Ex` must derive from `std::exception`.
|
||
* For each `E`~i~ int `E...`, `<<is_error_type,is_error_type>><E~i~>::value` is `true`.
|
||
|
||
Returns: :: An object of unspecified type which derives publicly from `Ex` *and* from class <<error_id>> such that:
|
||
* its `Ex` subobject is initialized by `std::forward<Ex>(ex)`;
|
||
* its `error_id` subojbect is initialized by `<<new_error,new_error>>(std::forward<E>(e)...`).
|
||
|
||
TIP: If thrown, the returned object can be caught as `Ex &` or as `leaf::<<error_id,error_id>> &`.
|
||
|
||
NOTE: To automatically capture `pass:[__FILE__]`, `pass:[__LINE__]` and `pass:[__FUNCTION__]` with the returned object, use <<LEAF_EXCEPTION>> instead of `leaf::exception`.
|
||
|
||
'''
|
||
|
||
[[try_]]
|
||
=== `try_`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class TryBlock, class... Handler>
|
||
decltype(std::declval<TryBlock>()())
|
||
try_( TryBlock && try_block, Handler && ... handler );
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: ::
|
||
|
||
`try_` has the same requirements as <<handle_some>>, with the following change:
|
||
+
|
||
--
|
||
* The `try_block` is not required to -- and usually does not -- return a `result<T>` (because, presumably, all failures are communicated by throwing exceptions).
|
||
--
|
||
And the following addition:
|
||
* Each of the `handler...` functions may take arguments, either by value or by `const &`, of the predicate type `<<catch_,catch_>><Ex>`, where `Ex` is a type that derives from `std::exception`.
|
||
|
||
Effects: ::
|
||
|
||
`try_` works similarly to <<handle_some>> -- the difference is that `try_` catches exceptions thrown by the `try_block` function:
|
||
|
||
* If the `try_block` throws:
|
||
** `try_` attempts to match the <<error_id>> communicated with the caught exception with a `handler`. If that succeeds, `try_` forwards the return value from the matched `handler` to the caller;
|
||
** Otherwise, `try_` re-throws the caught exception.
|
||
* If the `try_block` returns a value `r`:
|
||
** if `r` is of a `result<T>` type (see <<result_requirements>>), `try_` proceeds the same as `handle_some`;
|
||
** otherwise it returns `r`.
|
||
|
||
Handler Matching Procedure: ::
|
||
|
||
Because each E-object stored in the `try_` scope (see <<handle_some>>) is uniquely associated with a specific <<error_id>>, `try_` needs to extract an `error_id` value from the caught exception in order to access any currently stored E-object (`handle_some` and `handle_all` do not have this challenge, since the <<error_id>> they need is readily available in the `<<result,result>><T>` object communicating the failure).
|
||
+
|
||
When throwing, users are encouraged to pass the exception object through the <<exception>> function template -- and `throw` the object it returns. This guarantees that the thrown exception transports a unique `error_id` value, just like `result<T>` does.
|
||
+
|
||
However, this isn't possible when we don't control the `throw` site, for example if the exception is thrown by a standard function. In this case, E-objects communicated to LEAF are associated with the `error_id` value returned by <<next_error>>, which is a preview of sorts, of the `error_id` value that would be returned by the next call to <<new_error>>.
|
||
+
|
||
Similarly, if the exception object caught by `try_` does not transports an `error_id` value, E-objects are looked up using the `error_id` value returned by <<next_error>> just before `try_` goes through its `handler`-matching search. If imperfect, this approach provides the needed association.
|
||
+
|
||
NOTE: <<get_error_id-exception>> implements the procedure described above.
|
||
+
|
||
With the `error_id` value thus obtained, the `handler`-matching procedure works the same as in <<handle_some>>, with the following addition to the way each argument value `a`~i~ to be passed to the `handler` currently under consideration is produced:
|
||
|
||
* If `a`~i~ is taken as `A`~i~ `const &` or by value:
|
||
** If `A`~i~ is of the predicate type `<<catch_,catch_>><Ex...>`, `a`~i~ is initialized with the caught `std::exception const &`. The handler is dropped if the expression `a`~i~`()` evaluates to `false`.
|
||
+
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
struct exception1: std::exception { };
|
||
struct exception2: std::exception { };
|
||
struct exception3: std::exception { };
|
||
|
||
....
|
||
|
||
return leaf::try_(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::catch_<exception1, exception2> ) <1>
|
||
{
|
||
....
|
||
},
|
||
|
||
[ ]( leaf::error_info const & info ) <2>
|
||
{
|
||
....
|
||
} );
|
||
----
|
||
<1> This handler matches if the exception caught by `try_` is either of type `exception1` or `exception2`.
|
||
<2> This handler matches any error. Use `info.<<error_info,exception>>()` to access any caught `std::exception` object.
|
||
|
||
'''
|
||
|
||
[[catch_]]
|
||
=== `catch_`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... Ex>
|
||
struct catch_
|
||
{
|
||
std::exception const & value;
|
||
|
||
explicit catch_( std::exception const & ex ) noexcept;
|
||
|
||
bool operator()() const noexcept;
|
||
};
|
||
|
||
} }
|
||
----
|
||
|
||
NOTE: The `catch_` template is useful only as argument to a handler function passed to <<try_>>.
|
||
|
||
Effects: ::
|
||
|
||
The `catch_` constructor initializes the `value` reference with `ex`.
|
||
|
||
The `catch_` template is a predicate function type: `operator()` returns `true` iff for at least one of `Ex`~i~ in `Ex...`, the expression `dynamic_cast<Ex~i~ const *>(&value) != 0` is `true`.
|
||
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
struct exception1: std::exception { };
|
||
struct exception2: std::exception { };
|
||
struct exception3: std::exception { };
|
||
|
||
exception2 x;
|
||
|
||
catch_<exception1> c1(x);
|
||
assert(!c1());
|
||
|
||
catch_<exception2> c2(x);
|
||
assert(c2());
|
||
|
||
catch_<exception1,exception2> c3(x);
|
||
assert(c3());
|
||
|
||
catch_<exception1,exception3> c4(x);
|
||
assert(!c4());
|
||
----
|
||
|
||
'''
|
||
|
||
[[get_error_id-exception]]
|
||
=== `get_error_id`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
error_id get_error_id( std::exception const & ex ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: If the dynamic type of `ex` derives from <<error_id>>, returns the `error_id` slice of `ex`. Otherwise, returns the result of <<next_error>>.
|
||
|
||
NOTE: Please read the documentation for <<try_>> to understand why `get_error_id` is needed.
|
||
|
||
TIP: Whenever possible, pass exceptions you throw through the <<exception>> function template (or use <<LEAF_THROW>>). This ensures that the thrown exceptions derive from <<error_id>>, which is always preferable to the <<next_error>> hack.
|
||
|
||
'''
|
||
|
||
[[exception_to_result]]
|
||
=== `exception_to_result`
|
||
|
||
include::{sourcedir}/synopses/exception_to_result.adoc[]
|
||
|
||
This function can be used to catch exceptions from a lower-level library and convert them to `<<result,result>><T>`.
|
||
|
||
Returns: :: If `f` returns `T`, `exception_to_result` returns `result<T>`.
|
||
|
||
Effects: :: Invokes `f` in a `try` block, catching each individual exception type `Ex`, by value, proceeding as follows:
|
||
+
|
||
[source,c++]
|
||
----
|
||
catch( Ex ex )
|
||
{
|
||
return leaf::new_error(std::move(ex));
|
||
}
|
||
----
|
||
+
|
||
A final catch-all is added, as follows:
|
||
+
|
||
[source,c++]
|
||
----
|
||
catch(...)
|
||
{
|
||
return leaf::new_error(std::current_exception());
|
||
}
|
||
----
|
||
|
||
[WARNING]
|
||
--
|
||
* Catching by value slices each individual exception object.
|
||
* The order of the types in the `Ex...` list is significant.
|
||
* `std::current_exception()` likely allocates memory dynamically.
|
||
* Handlers passed to <<handle_some>> / <<handle_all>> should take the converted-to-result exception objects by `const &` (whereas, in case exceptions are handled directly by <<try_>> handlers, you would use <<catch_>> instead).
|
||
--
|
||
|
||
Example:
|
||
|
||
[source,c++]
|
||
----
|
||
int compute_answer_throws();
|
||
|
||
//Call compute_answer, convert exceptions to result<int>
|
||
leaf::result<int> compute_answer()
|
||
{
|
||
return leaf::catch_exceptions<
|
||
ex_type1,
|
||
ex_type2>( compute_answer_throws() );
|
||
}
|
||
----
|
||
|
||
Later, the `ex_type1` and `ex_type2` exceptions can be processed by <<handle_some>> / <<handle_all>>:
|
||
|
||
[source,c++]
|
||
----
|
||
return leaf::handle_some(
|
||
|
||
[ ] -> leaf::result<void>
|
||
{
|
||
LEAF_AUTO(answer, compute_answer());
|
||
//Use answer
|
||
....
|
||
return { };
|
||
},
|
||
|
||
[ ]( ex_type1 const & ex1 )
|
||
{
|
||
//Handle ex_type1
|
||
....
|
||
return { };
|
||
},
|
||
|
||
[ ]( ex_type2 const & ex2 )
|
||
{
|
||
//Handle ex_type2
|
||
....
|
||
return { };
|
||
},
|
||
|
||
[ ]( std::exception_ptr const & p )
|
||
{
|
||
//Handle any other exception from compute_answer.
|
||
....
|
||
return { };
|
||
} );
|
||
----
|
||
|
||
'''
|
||
|
||
[[capture_exception]]
|
||
=== `capture_exception`
|
||
|
||
.#include <boost/leaf/capture_exception.hpp>
|
||
include::{sourcedir}/synopses/capture_exception.adoc[]
|
||
|
||
The `capture_exception` function can be used to transport E-objects <<propagation,propagated>> when `f` is invoked in one thread, to a different thread which handles errors through <<try_>>.
|
||
|
||
Requirements: ::
|
||
* `F` must be a function type;
|
||
* Each of the `E...` types must be an <<e_objects,E-type>>.
|
||
|
||
Returns: :: A function object `fw` of unspecified type, which acts as a wrapper for `f`, taking the same argument types and returning the same type as `f`.
|
||
+
|
||
When the caller invokes `fw` (presumably from a worker thread) with arguments `a...` of types `A...`:
|
||
+
|
||
--
|
||
* Like <<try_>>, `fw` reserves storage in its scope for objects of the `E...` types, then
|
||
* invokes `f(std::forward<A>(a...))`, which in general will call other functions as needed (any <<propagation,propagated>> objects of the `E...` types are captured in the reserved storage);
|
||
--
|
||
+
|
||
Except if `f` throws, any E-objects stored in the `fw` scope are discarded and the returned value is forwarded to the caller.
|
||
+
|
||
Otherwise, `fw` throws another exception object (of unspecified type) which holds all stored `E` objects and the original exception via `std::exception_ptr`.
|
||
+
|
||
If this new exception is thrown by the `try_block` function passed to <<try_>> (presumably after it was transported to the main thread), its contents are <<propagation,propagated>> (in that thread), and then error handling proceeds as usual.
|
||
|
||
'''
|
||
|
||
[[LEAF_EXCEPTION]]
|
||
=== `LEAF_EXCEPTION`
|
||
|
||
[source,c++]
|
||
.#include <boost/leaf/exception.hpp>
|
||
----
|
||
#define LEAF_EXCEPTION(...) <<unspecified>>
|
||
----
|
||
|
||
Effects: :: This is a variadic macro which forwards its arguments to the function template <<exception>>, in addition capturing `pass:[__FILE__]`, `pass:[__LINE__]` and `pass:[__FUNCTION__]`, in a `<<common,e_source_location>>` object.
|
||
|
||
'''
|
||
|
||
[[LEAF_THROW]]
|
||
=== `LEAF_THROW`
|
||
|
||
[source,c++]
|
||
.#include <boost/leaf/exception.hpp>
|
||
----
|
||
#define LEAF_THROW(...) throw LEAF_EXCEPTION(__VA_ARGS__)
|
||
----
|
||
|
||
Effects: :: Throws the exception object returned by <<LEAF_EXCEPTION>>.
|
||
|
||
[[techniques]]
|
||
== Programming Techniques
|
||
|
||
[[technique_preload]]
|
||
=== Preloading Error Objects
|
||
|
||
Consider the following exception type:
|
||
|
||
[source,c++]
|
||
----
|
||
class file_read_error: public std::exception
|
||
{
|
||
std::string file_name_;
|
||
|
||
public:
|
||
|
||
explicit file_read_error( std::string const & fn ): file_name_(fn) { }
|
||
|
||
std::string const & file_name() const noexcept { return file_name_; }
|
||
};
|
||
----
|
||
|
||
A catch statement that handles `file_read_error` exceptions:
|
||
|
||
[source,c++]
|
||
----
|
||
catch( file_read_error & e )
|
||
{
|
||
std::cerr << "Error reading \"" << e.file_name() << "\"\n";
|
||
}
|
||
----
|
||
|
||
Finally, a function that may throw `file_read_error` exceptions:
|
||
|
||
[source,c++]
|
||
----
|
||
void read_file( FILE * f ) {
|
||
....
|
||
size_t nr=fread(buf,1,count,f);
|
||
if( ferror(f) )
|
||
throw file_read_error(???); //File name not available here!
|
||
....
|
||
}
|
||
----
|
||
|
||
This is a problem: the `catch` needs a file name, but at the point of the `throw` a file name is not available (only a `FILE` pointer is). In general, the error might be detected in a library which can not assume that a meaningful name is available for any `FILE` it reads, even if a program that uses the library could reasonably make the same assumption.
|
||
|
||
Using LEAF, a file name may be associated with any exception after it has been thrown, while anything available at the point of the `throw` (e.g. `errno`) may be passed directly to the `leaf::<<exception,exception>>` function template:
|
||
|
||
[source,c++]
|
||
----
|
||
class file_open_error: public std::exception { };
|
||
class file_read_error: public std::exception { };
|
||
struct e_file_name { std::string value; };
|
||
struct e_errno { int value; };
|
||
|
||
void read_file( FILE * f )
|
||
{
|
||
....
|
||
size_t nr=fread( buf,1,count,f );
|
||
if( ferror(f) )
|
||
throw leaf::exception( file_read_error(), e_errno{errno} );
|
||
....
|
||
}
|
||
|
||
void process_file( char const * name )
|
||
{
|
||
auto propagate = leaf::preload( e_file_name{name} );
|
||
|
||
if( FILE * fp=fopen(name,"rt")) {
|
||
std::shared_ptr<FILE> f(fp,fclose);
|
||
....
|
||
read_file(fp); //throws on error
|
||
....
|
||
}
|
||
else
|
||
throw leaf::exception(file_open_error());
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
`<<exception,exception>>` | <<preload>>
|
||
|
||
The key is the call to `<<preload,preload>>`: it ensures that the passed `e_file_name` will be associated with any exception that escapes `process_file`. This is fully automatic, and works regardless of whether the exception is thrown later in the same function, or by `read_file`, or by some third-party function we call.
|
||
|
||
Now, exceptions thrown by `process_file` may be handled like this:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::try_(
|
||
|
||
[ ]
|
||
{
|
||
process_file("example.txt");
|
||
},
|
||
|
||
[ ]( leaf::catch_<file_open_error, file_read_error>, e_file_name const & fn, e_errno const & errn )
|
||
{
|
||
std::cerr <<
|
||
"I/O error!" << std::endl <<
|
||
"File name: " << fn.value << ", errno=" << errn << std::endl;
|
||
} );
|
||
----
|
||
|
||
[.text-right]
|
||
<<try_>> | <<catch_>>
|
||
|
||
NOTE: This technique works exactly the same way when errors are reported using `leaf::<<result,result>>` rather than by throwing exceptions.
|
||
|
||
'''
|
||
|
||
[[technique_defer]]
|
||
=== Capturing `errno` with `defer`
|
||
|
||
Consider the following function:
|
||
|
||
[source,c++]
|
||
----
|
||
void read_file(FILE * f) {
|
||
....
|
||
size_t nr=fread(buf,1,count,f);
|
||
if( ferror(f) )
|
||
throw leaf::exception( file_read_error(), e_errno{errno} );
|
||
....
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
`<<exception,exception>>`
|
||
|
||
It is pretty straight-forward, reporting `e_errno` as it detects a `ferror`. But what if it calls `fread` multiple times?
|
||
|
||
[source,c++]
|
||
----
|
||
void read_file(FILE * f) {
|
||
....
|
||
size_t nr1=fread(buf1,1,count1,f);
|
||
if( ferror(f) )
|
||
throw leaf::exception( file_read_error(), e_errno{errno} );
|
||
|
||
size_t nr2=fread(buf2,1,count2,f);
|
||
if( ferror(f) )
|
||
throw leaf::exception( file_read_error(), e_errno{errno} );
|
||
|
||
size_t nr3=fread(buf3,1,count3,f);
|
||
if( ferror(f) )
|
||
throw leaf::exception( file_read_error(), e_errno{errno} );
|
||
....
|
||
}
|
||
----
|
||
|
||
Ideally, associating `e_errno` with each exception should be automated. One way to achieve this is to not call `fread` directly, but wrap it in another function which checks for `ferror` and associates the `e_errno` with the exception it throws.
|
||
|
||
<<technique_preload,Preloading Errors>> describes how to solve a very similar problem without a wrapper function, but that technique does not work for `e_errno` because `<<preload,preload>>` would capture `errno` before a `fread` call was attempted, at which point `errno` is probably `0` -- or, worse, leftover from a previous I/O failure.
|
||
|
||
The solution is to use `<<defer,defer>>`, so we don't have to remember to include `e_errno` with each exception; `errno` will be associated automatically with any exception that escapes `read_file`:
|
||
|
||
[source,c++]
|
||
----
|
||
void read_file(FILE * f) {
|
||
|
||
auto propagate = leaf::defer([ ]{ return e_errno{errno}; });
|
||
|
||
....
|
||
size_t nr1=fread(buf1,1,count1,f);
|
||
if( ferror(f) )
|
||
throw leaf::exception(file_read_error());
|
||
|
||
size_t nr2=fread(buf2,1,count2,f);
|
||
if( ferror(f) )
|
||
throw leaf::exception(file_read_error());
|
||
|
||
size_t nr3=fread(buf3,1,count3,f);
|
||
if( ferror(f) )
|
||
throw leaf::exception(file_read_error());
|
||
....
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<defer>> | `<<exception,exception>>`
|
||
|
||
This works similarly to `preload`, except that the capturing of the `errno` is deferred until the destructor of the `propagate` object is called, which calls the passed lambda function to obtain the `errno`.
|
||
|
||
NOTE: This technique works exactly the same way when errors are reported using `leaf::<<result,result>>` rather than by throwing exceptions.
|
||
|
||
'''
|
||
|
||
[[technique_augment_in_catch]]
|
||
=== Augmenting Exceptions in a `catch`
|
||
|
||
What makes `<<preload,preload>>` and `<<defer,defer>>` useful (see <<technique_preload,Preloading Errors>> and <<technique_defer,Capturing `errno` with `defer`>>) is that they automatically include E-objects with any exception or error reported by a function.
|
||
|
||
But what if we need to include some E-object conditionally? When using exception handling, it would be nice to be able to do this in a `catch` statement which selectively augments passing exceptions.
|
||
|
||
LEAF supports the following approach, assuming all exceptions derive from `std::exception`:
|
||
|
||
[source,c++]
|
||
----
|
||
try
|
||
{
|
||
....
|
||
function_that_throws();
|
||
....
|
||
}
|
||
catch( std::exception const & e )
|
||
{
|
||
if( condition )
|
||
leaf::get_error_id(e).propagate( e_this{....}, e_that{....} );
|
||
throw;
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<get_error_id-exception>> | <<propagate>>
|
||
|
||
The reason we need to use <<get_error_id-exception>> is that not all exception types derive from `leaf::<<error_id,error_id>>`. If the caught exception has a `leaf::error_id` subobject, `get_error_id` will return that `leaf::error_id` slice.
|
||
|
||
But if the caught exception doesn't have a `leaf::error_id` subobject, `get_error_id` returns an unspecified `leaf::error_id`, which is temporarily associated with any and all current exceptions, until successfully handled by <<try_>>. While this association is imperfect (because it does not pertain to a specific exception object) it is the best that can be done in this case.
|
||
|
||
[NOTE]
|
||
--
|
||
The following approach is roughly equivalent:
|
||
[source,c++]
|
||
----
|
||
try
|
||
{
|
||
....
|
||
function_that_throws();
|
||
....
|
||
}
|
||
catch( leaf::error_id err )
|
||
{
|
||
if( condition )
|
||
err.propagate( e_this{....}, e_that{....} );
|
||
throw;
|
||
}
|
||
catch( std::exception const & e )
|
||
{
|
||
if( condition )
|
||
leaf::next_error().propagate( e_this{....}, e_that{....} );
|
||
throw;
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<propagate>> | <<next_error>>
|
||
--
|
||
|
||
'''
|
||
|
||
[[technique_disparate_error_types]]
|
||
=== Working with Disparate Error Types
|
||
|
||
Because most libraries define their own mechanism for reporting errors, programmers often need to use multiple incompatible error-reporting interfaces in the same program. This led to the introduction of `boost::system::error_code` which later became `std::error_code`. Each `std::error_code` object is assigned an `error_category`. Libraries that communicate errors in terms of `std::error_code` define their own `error_category`. For libraries that do not, the user can "easily" define a custom `error_category` and still translate domain-specific error codes to `std::error_code`.
|
||
|
||
But let's take a step back and consider _why_ did we want to express every error in terms of the same static type, `std::error_code` in the first place? We need this translation because the {CPP} static type-checking system makes it difficult to write functions that may return error objects of the disparate static types used by different libraries. Outside of this limitation, it would be preferable to be able to write functions that can communicate errors in terms of arbitrary {CPP} types, as needed.
|
||
|
||
To drive this point further, consider the real world problem of mixing `boost::system::error_code` and `std::error_code` in the same program. In theory, both systems are designed to be able to express one error code in terms of the other. In practice, describing a _generic_ system for error categorization in terms of another _generic_ system for error categorization is not trivial.
|
||
|
||
Ideally, functions should be able to communicate different error types without having to translate between them, and {CPP} does offer a mechanism that does just that, it's called exception handling. And it is not a coincidence that the attempt to bind {CPP} exception types with the interface of each function, A.K.A. exception specifications, was so problematic that it had to be abandoned (while I am an outspoken proponent of exception handling, I do acknowledge that in practice, often for good reasons, exception handling may not be available or permitted).
|
||
|
||
LEAF solves this problem without using exception handling: a scope that is able to handle either `std::error_code` or `boost::system::error_code` would look like this:
|
||
|
||
[source,c++]
|
||
----
|
||
return handle_some(
|
||
|
||
[ ]() -> leaf::result<T>
|
||
{
|
||
//Call operations which may report std::error_code and boost::system::error_code.
|
||
//Return errors via result<T>.
|
||
},
|
||
|
||
[ ]( std::error_code const & e )
|
||
{
|
||
//Handle std::error_code
|
||
},
|
||
|
||
[ ]( boost::system::error_code const & e )
|
||
{
|
||
//Handle boost::system::error_code
|
||
} );
|
||
----
|
||
|
||
[.text-right]
|
||
<<handle_some>> | <<result>>
|
||
|
||
And here is a function which, using LEAF, forwards either `std::error_code` or `boost::system::error_code` objects reported by lower level functions:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::result<T> f()
|
||
{
|
||
if( std::error_code ec = g1() )
|
||
{
|
||
//Success
|
||
}
|
||
else
|
||
return leaf::new_error(ec);
|
||
|
||
if( boost::system::error_code ec = g2() )
|
||
{
|
||
//Success
|
||
}
|
||
else
|
||
return leaf::new_error(ec);
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<result>> | <<new_error>>
|
||
|
||
'''
|
||
|
||
[[technique_exception_to_result]]
|
||
=== Converting Exceptions to `result<T>`
|
||
|
||
It is sometimes necessary to catch exceptions thrown by lower-level library function, and report the error through different means, to a higher-level library which may not use exception handling.
|
||
|
||
Suppose we have an exception type hierarchy and a function `compute_answer_throws`:
|
||
|
||
[source,c++]
|
||
----
|
||
class error_base: public virtual std::exception { };
|
||
class error_a: public virtual error_base { };
|
||
class error_b: public virtual error_base { };
|
||
class error_c: public virtual error_base { };
|
||
|
||
int compute_answer_throws()
|
||
{
|
||
switch( rand()%4 )
|
||
{
|
||
default: return 42;
|
||
case 1: throw error_a();
|
||
case 2: throw error_b();
|
||
case 3: throw error_c();
|
||
}
|
||
}
|
||
----
|
||
|
||
We can write a simple wrapper using <<exception_to_result>>, which calls `compute_answer_throws` and switches to `result<int>` for error handling:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::result<int> compute_answer() noexcept
|
||
{
|
||
return leaf::exception_to_result<error_a, error_b>(
|
||
[ ]
|
||
{
|
||
return compute_answer_throws();
|
||
} );
|
||
}
|
||
----
|
||
|
||
(As a demonstration, `compute_answer` specifically converts exceptions of type `error_a` or `error_b`, while it leaves `error_c` to be captured by `std::exception_ptr`).
|
||
|
||
Here is a simple function which prints successfully computed answers, forwarding any error (originally reported by throwing an exception) to its caller:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::result<void> print_answer() noexcept
|
||
{
|
||
LEAF_AUTO(answer, compute_answer());
|
||
std::cout << "Answer: " << answer << std::endl;
|
||
return { };
|
||
}
|
||
----
|
||
|
||
Finally, here is a scope that handles the errors (which used to be exception objects):
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::handle_all(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
LEAF_CHECK(print_answer());
|
||
return { };
|
||
},
|
||
|
||
[ ]( error_a const & e )
|
||
{
|
||
std::cerr << "Error A!" << std::endl;
|
||
},
|
||
|
||
[ ]( error_b const & e )
|
||
{
|
||
std::cerr << "Error B!" << std::endl;
|
||
},
|
||
|
||
[ ]
|
||
{
|
||
std::cerr << "Unknown error!" << std::endl;
|
||
} );
|
||
----
|
||
|
||
NOTE: The complete program illustrating this technique is available https://github.com/zajo/leaf/blob/master/example/exception_to_result.cpp?ts=3[here].
|
||
|
||
'''
|
||
|
||
[[technique_preload_in_c_callbacks]]
|
||
=== Using `next_error` in (Lua) C-callbacks
|
||
|
||
Communicating information pertaining to a failure detected in a C callback is tricky, because C callbacks are limited to a specific static signature, which may not use {CPP} types.
|
||
|
||
LEAF makes this easy. As an example, we'll write a program that uses Lua and reports a failure from a {CPP} function registered as a C callback, called from a Lua program. The failure will be propagated from {CPP}, through the Lua interpreter (written in C), back to the {CPP} function which called it.
|
||
|
||
C/{CPP} functions designed to be invoked from a Lua program must use the following signature:
|
||
|
||
[source,c]
|
||
----
|
||
int do_work( lua_State * L );
|
||
----
|
||
|
||
Arguments are passed on the Lua stack (which is accessible through `L`). Results too are pushed onto the Lua stack.
|
||
|
||
First, let's initialize the Lua interpreter and register `do_work` as a C callback, available for Lua programs to call:
|
||
|
||
[source,c++]
|
||
----
|
||
std::shared_ptr<lua_State> init_lua_state() noexcept
|
||
{
|
||
std::shared_ptr<lua_State> L(lua_open(),&lua_close); //<1>
|
||
|
||
lua_register( &*L, "do_work", &do_work ); //<2>
|
||
|
||
luaL_dostring( &*L, "\ //<3>
|
||
\n function call_do_work()\
|
||
\n return do_work()\
|
||
\n end" );
|
||
|
||
return L;
|
||
}
|
||
----
|
||
<1> Create a new `lua_State`. We'll use `std::shared_ptr` for automatic cleanup.
|
||
<2> Register the `do_work` {CPP} function as a C callback, under the global name `do_work`. With this, calls from Lua programs to `do_work` will land in the `do_work` {CPP} function.
|
||
<3> Pass some Lua code as a `C` string literal to Lua. This creates a global Lua function called `call_do_work`, which we will later ask Lua to execute.
|
||
|
||
Next, let's define our `enum` used to communicate `do_work` failures:
|
||
|
||
[source,c++]
|
||
----
|
||
enum do_work_error_code
|
||
{
|
||
ec1=1,
|
||
ec2
|
||
};
|
||
|
||
namespace boost { namespace leaf {
|
||
|
||
template<> struct is_error_type<do_work_error_code>: std::true_type { };
|
||
|
||
} }
|
||
----
|
||
[.text-right]
|
||
<<is_error_type>>
|
||
|
||
We're now ready to define the `do_work` callback function:
|
||
|
||
[source,c++]
|
||
----
|
||
int do_work( lua_State * L ) noexcept
|
||
{
|
||
bool success=rand()%2; //<1>
|
||
if( success )
|
||
{
|
||
lua_pushnumber(L,42); //<2>
|
||
return 1;
|
||
}
|
||
else
|
||
{
|
||
leaf::next_error().propagate(ec1); //<3>
|
||
return luaL_error(L,"do_work_error"); //<4>
|
||
}
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<next_error>> | <<propagate>>
|
||
|
||
<1> "Sometimes" `do_work` fails.
|
||
<2> In case of success, push the result on the Lua stack, return back to Lua.
|
||
<3> Associate an `do_work_error_code` object with the *next* `leaf::error_id` object we will definitely return from the `call_lua` function...
|
||
<4> ...once control reaches it, after we tell the Lua interpreter to abort the program.
|
||
|
||
Now we'll write the function that calls the Lua interpreter to execute the Lua function `call_do_work`, which in turn calls `do_work`. We'll return `<<result,result>><int>`, so that our caller can get the answer in case of success, or an error:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::result<int> call_lua( lua_State * L )
|
||
{
|
||
lua_getfield( L, LUA_GLOBALSINDEX, "call_do_work" );
|
||
if( int err=lua_pcall(L,0,1,0) ) //<1>
|
||
{
|
||
auto propagate = leaf::preload( e_lua_error_message{lua_tostring(L,1)} ); //<2>
|
||
lua_pop(L,1);
|
||
return leaf::new_error( e_lua_pcall_error{err} );
|
||
}
|
||
else
|
||
{
|
||
int answer=lua_tonumber(L,-1); //<3>
|
||
lua_pop(L,1);
|
||
return answer;
|
||
}
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<result>> | <<preload>> | <<new_error>>
|
||
|
||
<1> Ask the Lua interpreter to call the global Lua function `call_do_work`.
|
||
<2> Something went wrong with the call, so we'll return a <<new_error>>. If this is a `do_work` failure, the `do_work_error_code` object prepared in `do_work` will become associated with this `leaf::error_id`. If not, we will still need to communicate that the `lua_pcall` failed with an error code and an error message.
|
||
<3> Success! Just return the int answer.
|
||
|
||
Finally, here is the `main` function which exercises `call_lua`, each time handling any failure:
|
||
|
||
[source,c++]
|
||
----
|
||
int main() noexcept
|
||
{
|
||
std::shared_ptr<lua_State> L=init_lua_state();
|
||
|
||
for( int i=0; i!=10; ++i )
|
||
{
|
||
leaf::handle_all(
|
||
|
||
[&]() -> leaf::result<void>
|
||
{
|
||
LEAF_AUTO(answer, call_lua(&*L));
|
||
std::cout << "do_work succeeded, answer=" << answer << '\n'; <1>
|
||
return { };
|
||
},
|
||
|
||
[ ]( do_work_error_code e ) <2>
|
||
{
|
||
std::cout << "Got do_work_error_code = " << e << "!\n";
|
||
},
|
||
|
||
[ ]( e_lua_pcall_error const & err, e_lua_error_message const & msg ) <3>
|
||
{
|
||
std::cout << "Got e_lua_pcall_error, Lua error code = " << err.value << ", " << msg.value << "\n";
|
||
},
|
||
|
||
[ ]( leaf::error_info const & unmatched )
|
||
{
|
||
std::cerr <<
|
||
"Unknown failure detected" << std::endl <<
|
||
"Cryptic diagnostic information follows" << std::endl <<
|
||
unmatched;
|
||
} );
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<handle_all>> | <<result>>
|
||
|
||
<1> If the call to `call_lua` succeeded, just print the answer.
|
||
<2> Handle `do_work` failures.
|
||
<3> Handle all other `lua_pcall` failures.
|
||
|
||
[NOTE]
|
||
--
|
||
Follow this link to see the complete program: https://github.com/zajo/leaf/blob/master/example/lua_callback_result.cpp?ts=3[lua_callback_result.cpp].
|
||
|
||
Remarkably, the Lua interpreter is {CPP} exception-safe, even though it is written in C. Here is the same program, this time using a {CPP} exception to report failures from `do_work`: https://github.com/zajo/leaf/blob/master/example/lua_callback_eh.cpp?ts=3[lua_callback_eh.cpp].
|
||
--
|
||
|
||
'''
|
||
|
||
[[technique_transport]]
|
||
=== Transporting Error Objects Between Threads
|
||
|
||
With LEAF, <<e_objects>> use automatic storage duration, stored inside the scope of <<handle_some>>, <<handle_all>> or <<try_>> functions. When using concurrency, we need a mechanism to detach E-objects from a worker thread and transport them to another thread where errors are handled.
|
||
|
||
LEAF offers two interfaces for this purpose, one using `result<T>`, and another designed for programs that use exception handling.
|
||
|
||
[[technique_transport-result]]
|
||
==== Using `result<T>`
|
||
|
||
Let's assume we have a `task` which produces a result but could also fail:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::result<task_result> task();
|
||
----
|
||
|
||
To prepare the returned `result` to be sent across the thread boundary, when we launch the asynchronous task, we wrap it in а call to <<capture_result>>, specifying which E-types we want captured:
|
||
|
||
[source,c++]
|
||
----
|
||
std::future<leaf::result<task_result>> launch_task()
|
||
{
|
||
return std::async(
|
||
std::launch::async,
|
||
leaf::capture_result<E1, E2, E3>(&task));
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<capture_result>> | <<result>>
|
||
|
||
That's it! Later when we `get` the `std::future`, we can process the returned `result<task_result>` as if it was generated locally:
|
||
|
||
[source,c++]
|
||
----
|
||
//std::future<leaf::result<task_result>> fut;
|
||
fut.wait();
|
||
|
||
return leaf::handle_some(
|
||
|
||
[&]() -> leaf::result<void>
|
||
{
|
||
LEAF_AUTO(r, fut.get());
|
||
//Success!
|
||
return { }
|
||
},
|
||
|
||
[ ]( E1 e1, E2 e2 )
|
||
{
|
||
//Deal with E1, E2
|
||
....
|
||
return { };
|
||
},
|
||
|
||
[ ]( E3 e3 )
|
||
{
|
||
//Deal with E3
|
||
....
|
||
return { };
|
||
} );
|
||
----
|
||
|
||
[.text-right]
|
||
<<handle_some>> | <<result>>
|
||
|
||
NOTE: Follow this link to see a complete example program: https://github.com/zajo/leaf/blob/master/example/capture_result.cpp?ts=3[capture_result.cpp].
|
||
|
||
'''
|
||
|
||
[[technique_transport-exceptions]]
|
||
==== Using Exception Handling
|
||
|
||
Let's assume we have a `task` which produces a result but could also throw:
|
||
|
||
[source,c++]
|
||
----
|
||
task_result task();
|
||
----
|
||
|
||
To prepare exceptions thrown by the `task` function to be sent across the thread boundary, when we launch the asynchronous task, we wrap it in а call to <<capture_exception>>, specifying which E-types we want captured:
|
||
|
||
[source,c++]
|
||
----
|
||
std::future<task_result> launch_task()
|
||
{
|
||
return std::async(
|
||
std::launch::async,
|
||
leaf::capture_exception<E1, E2, E3>(&task));
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<result>>
|
||
|
||
That's it! Later when we `get` the `std::future`, we can catch exceptions as if they're thrown locally:
|
||
|
||
[source,c++]
|
||
----
|
||
//std::future<task_result> fut;
|
||
fut.wait();
|
||
|
||
try_(
|
||
|
||
[&]
|
||
{
|
||
auto r = fut.get();
|
||
//Success!
|
||
},
|
||
|
||
[ ]( E1 e1, E2 e2 )
|
||
{
|
||
//Deal with E1, E2
|
||
....
|
||
},
|
||
|
||
[ ]( E3 e3 )
|
||
{
|
||
//Deal with E3
|
||
....
|
||
} );
|
||
----
|
||
|
||
[.text-right]
|
||
<<try_>> | <<result>>
|
||
|
||
NOTE: Follow this link to see a complete example program: https://github.com/zajo/leaf/blob/master/example/capture_eh.cpp?ts=3[capture_eh.cpp].
|
||
|
||
[[rationale]]
|
||
== Design
|
||
|
||
=== Rationale
|
||
|
||
Definition: :: Objects that carry information about error conditions are called error objects. For example, objects of type `std::error_code` are error objects.
|
||
|
||
NOTE: The following reasoning is independent of what mechanics are used to transport error objects, whether it is exception handling or anything else.
|
||
|
||
Definition: :: Depending on their interaction with error objects, functions can be classified as follows:
|
||
* *Error-initiating*: functions that initiate error conditions by creating new error objects.
|
||
* *Error-neutral*: functions that forward to the caller error objects returned by functions they call.
|
||
* *Error-handling*: functions that dispose of error objects forwarded to them, recovering normal program operation.
|
||
|
||
A crucial observation is that _error-initiating_ functions are typically low level functions that lack any context and can not determine, much less dictate, the correct program behavior in response to errors they initiate. Error conditions which (correctly) lead to termination in some programs may (correctly) be ignored in others; yet other programs may recover from them and resume normal operation.
|
||
|
||
Stronger: authors of _error-initiating_ functions may not even reason about what information about the error is required to deal with it, by a specific _error-handling_ function in a specific program domain, except to pass any relevant information which is naturally available to them.
|
||
|
||
The same reasoning applies to _error-neutral_ functions, but in this case there is the additional problem that the errors they need to communicate, in general, are initiated by functions multiple levels removed from them in the call chain, functions which usually are -- and should be treated as -- implementation details. The _error-neutral_ function should not be coupled with any error object type used by _error-initiating_ functions, for the same reason it should not be coupled with any other aspect of their interface.
|
||
|
||
Finally, _error-handling_ functions, by definition, have the full context they need to deal with at least some, if not all, failures. In this scope it is an absolute necessity that the author knows exactly what information must be communicated by lower level functions in order to recover from each error condition. Specifically, none of this necessary information can be treated as implementation details; here, the coupling which is to be avoided by _error-neutral_ functions is unavoidable, in fact desirable.
|
||
|
||
We're now ready to define our
|
||
|
||
Design goals: ::
|
||
* *Error-initiating* functions should be able to communicate all information available to them that is relevant to the failure being reported.
|
||
* *Error-neutral* functions should not interfere or be coupled with error-related information that passes through them. They should be able to augment it with any additional information available to them that may be relevant to any error they forward to the caller.
|
||
* *Error-handling* functions should be able to access all the information communicated by _error-initiating_ or _error-neutral_ functions that is needed in their domain in order to deal with failures.
|
||
|
||
The difficulty in reaching these design goals is in that they seem to require that all error objects be allocated dynamically (the Boost Exception library meets these design goals at the cost of dynamic memory allocation).
|
||
|
||
As it turns out, dynamic memory allocation is not necessary due to the following
|
||
|
||
Fact: ::
|
||
* *Error-handling* functions already "know" which of the information _error-initiating_ and _error-neutral_ functions are [.underline]#able# to communicate is [.underline]#actually needed# in order to deal with failures in a particular program domain. Ideally, no resources should be [.line-through]#used# wasted storing or communicating information which is not currently needed to handle errors, even if it is relevant to the failure.
|
||
|
||
The LEAF functions <<handle_some>>, <<handle_all>> and <<try_>> implement this idea. Users provide error-handling lambda functions which, naturally, take the arguments types they need. LEAF simply provides space needed to store these types, using automatic storage duration, before they are passed to a handler.
|
||
|
||
When this space is created, `thread_local` pointers of the required error types are set to point to the corresponding storage within the scope of <<handle_some>>, <<handle_all>> or <<try_>>. Later on, _error-initiating_ or _error-neutral_ functions wanting to communicate an error object of type `E` access the corresponding `thread_local` pointer to see if there is currently storage available for this type:
|
||
|
||
* If the pointer is not null, storage is available and the object is moved into that storage, exactly once -- regardless of how many levels of function calls must unwind before an appropriate _error-handling_ function is reached.
|
||
* If the pointer is null, storage is not available and the error object is discarded, since no error-handling function makes any use of it -- saving resources.
|
||
|
||
This almost works, except we need to make sure that _error-handling_ functions are protected from being passed stale error objects stored in an error-handling scope, left there from previous failures, which would be a serious logic error. To this end, each failure is assigned a unique <<error_id>>, which is transported inside `leaf::<<result,result>>` objects that communicate failures. Each of the `E...` objects stored in error-handling scopes is assigned the same unique identifier, permanently associating it with a particular error.
|
||
|
||
Lastly, in _error-handling_ functions it makes sense to be able to not only recognize individual error conditions, but match specific error-handling code with the complete set of error objects that is required in each case.
|
||
|
||
In terms of {CPP} exception handling, it would be nice to be able to say something like:
|
||
|
||
[source,c++]
|
||
----
|
||
try
|
||
{
|
||
auto r = process_file();
|
||
|
||
//Success, use r:
|
||
....
|
||
}
|
||
|
||
catch( file_read_error const &, e_file_name const & fn, e_errno const & err )
|
||
{
|
||
std::cerr <<
|
||
"Could not read " << fn << ", errno=" << err << std::endl;
|
||
}
|
||
|
||
catch( file_read_error const &, e_errno const & err )
|
||
{
|
||
std::cerr <<
|
||
"File read error, errno=" << err << std::endl;
|
||
}
|
||
|
||
catch( file_read_error const & )
|
||
{
|
||
std::cerr << "File read error!" << std::endl;
|
||
}
|
||
----
|
||
|
||
That is to say, it is desirable to be able to dispatch error handling based not only on the kind of failure being handled, but also based on the kind of E-objects available. Unfortunately this syntax is not possible and, even if it were, not all programs use exceptions to handle errors.
|
||
|
||
LEAF achieves the same effect using a slightly different syntax:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::try_(
|
||
|
||
[ ]
|
||
{
|
||
auto r = process_file(); //Throws in case of failure, E-objects stored inside the try_ scope
|
||
|
||
//Success, use r:
|
||
....
|
||
}
|
||
|
||
[ ]( leaf::catch_<file_read_error>, e_file_name const & fn, e_errno const & err )
|
||
{
|
||
std::cerr <<
|
||
"Could not read " << fn << ", errno=" << err << std::endl;
|
||
},
|
||
|
||
[ ]( leaf::catch_<file_read_error>, e_errno const & err )
|
||
{
|
||
std::cerr <<
|
||
"File read error, errno=" << err << std::endl;
|
||
},
|
||
|
||
[ ]( leaf::catch_<file_read_error> )
|
||
{
|
||
std::cerr << "File read error!" << std::endl;
|
||
} );
|
||
----
|
||
|
||
[.text-right]
|
||
<<try_>> | <<catch_>>
|
||
|
||
Of course LEAF works without exception handling as well. Below is the same snippet, written using `<<result,result>><T>`:
|
||
|
||
[source,c++]
|
||
----
|
||
return leaf::handle_some(
|
||
|
||
[ ]() -> leaf::result<void>
|
||
{
|
||
LEAF_AUTO(r, process_file()); //In case of errors, E-objects are stored inside the handle_some scope
|
||
|
||
//Success, use r:
|
||
....
|
||
|
||
return { };
|
||
}
|
||
|
||
[ ]( leaf::match<error_enum, file_read_error>, e_file_name const & fn, e_errno const & err )
|
||
{
|
||
std::cerr <<
|
||
"Could not read " << fn << ", errno=" << err << std::endl;
|
||
},
|
||
|
||
[ ]( leaf::match<error_enum, file_read_error>, e_errno const & err )
|
||
{
|
||
std::cerr <<
|
||
"File read error, errno=" << err << std::endl;
|
||
},
|
||
|
||
[ ]( leaf::match<error_enum, file_read_error> )
|
||
{
|
||
std::cerr << "File read error!" << std::endl;
|
||
} );
|
||
----
|
||
|
||
[.text-right]
|
||
<<result>> | <<handle_some>> | <<match>>
|
||
|
||
NOTE: Please post questions and feedback on the Boost Developers Mailing List (LEAF is not part of Boost).
|
||
|
||
'''
|
||
|
||
[[exception_specifications]]
|
||
=== Critique 1: LEAF Does Not Enforce Correct Error Handling at Compile Time
|
||
|
||
A common critique of the LEAF design is that it does not statically enforce correct error handling. One specific idea I've heard from multiple sources is to add `E...` parameter pack to `result<T>`.
|
||
|
||
The idea is to be able to write something along these lines:
|
||
|
||
[source,c++]
|
||
----
|
||
result<T, E1, E2, E3> f() noexcept; <1>
|
||
|
||
result<T, E1, E3> g() noexcept <2>
|
||
{
|
||
if( result<T, E1, E2, E3> r = f() )
|
||
{
|
||
return r; //Success, return the T
|
||
}
|
||
else
|
||
{
|
||
return r.handle_error<E2>( [ ] ( .... ) <3>
|
||
{
|
||
....
|
||
} );
|
||
}
|
||
}
|
||
----
|
||
<1> `f` may only return errors of type `E1`, `E2`, `E3`.
|
||
<2> `g` narrows that to only `E1` and `E3`.
|
||
<3> Because `g` may only return errors of type `E1` and `E3`, it uses `handle_error` to deal with `E2`. In case `r` contains `E1` or `E3`, `handle_error` simply returns `r`, narrowing the error type parameter pack from `E1, E2, E3` down to `E1, E3`. If `r` contains an `E2`, `handle_error` calls the supplied lambda, which is required to return one of `E1`, `E3` (or a valid `T`).
|
||
|
||
The motivation here is to help avoid bugs in functions that handle errors that pop out of `g`: as long as the programmer deals with `E1` and `E3`, he can rest assured that no error is left unhandled.
|
||
|
||
Congratulations, we've just discovered exception specifications. The difference is that exception specifications, before being removed from {CPP}, were enforced dynamically, while this idea is equivalent to statically-enforced exception specifications, like they are in Java.
|
||
|
||
Why not statically enforce exception specifications?
|
||
|
||
"The short answer is that nobody knows how to fix exception specifications in any language, because the dynamic enforcement C++ chose has only different (not greater or fewer) problems than the static enforcement Java chose. ... When you go down the Java path, people love exception specifications until they find themselves all too often encouraged, or even forced, to add `throws Exception`, which immediately renders the exception specification entirely meaningless. (Example: Imagine writing a Java generic that manipulates an arbitrary type `T`).footnote:[https://herbsutter.com/2007/01/24/questions-about-exception-specifications/]"
|
||
-- Herb Sutter
|
||
|
||
While it is possible to turn every error *type* into, for example, a `std::error_code` *object*, this would eliminate the static type checking benefits we sought. Worse, the excessive coupling between the signatures of <<rationale,error-neutral>> functions (read: most functions in a program) and the error types they need to forward up the call stack interferes with the lossless propagation of errors throughout the program.
|
||
|
||
Conclusion: :: Attempts to harness the {CPP} static type checking system to help enforce correct error handling based on the types of errors a function may return require dynamic error types in generic contexts.
|
||
|
||
'''
|
||
|
||
[[translation]]
|
||
=== Critique 2: LEAF Does Not Facilitate Mapping Between Disparate Error Codes
|
||
|
||
Most {CPP} programs use multiple C and {CPP} libraries, and each library may provide its own system of error codes. But because it is difficult to define static interfaces that can communicate arbitrary error code types, a popular idea is to map each library-specific error code to a common program-wide enum.
|
||
|
||
For example, if we have --
|
||
|
||
[cols="1a,1a",stripes=none,frame="none",grid="none",width=1%]
|
||
|====
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
namespace lib_a
|
||
{
|
||
enum error
|
||
{
|
||
ok,
|
||
ec1,
|
||
ec2,
|
||
....
|
||
};
|
||
}
|
||
----
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
namespace lib_b
|
||
{
|
||
enum error
|
||
{
|
||
ok,
|
||
ec1,
|
||
ec2,
|
||
....
|
||
};
|
||
}
|
||
----
|
||
|====
|
||
|
||
-- we could define:
|
||
|
||
[source,c++]
|
||
----
|
||
namespace program
|
||
{
|
||
enum error
|
||
{
|
||
ok,
|
||
lib_a_ec1,
|
||
lib_a_ec2,
|
||
....
|
||
lib_b_ec1,
|
||
lib_b_ec2,
|
||
....
|
||
};
|
||
}
|
||
----
|
||
|
||
An error-handling library could provide a conversion API that uses the {CPP} static type system to automate the mapping between the different error enums. For example, it may define a class template `result<T,E>` with value-or-error variant semantics, so that:
|
||
|
||
* `lib_a` errors are transported in `result<T,lib_a::error>`,
|
||
* `lib_b` errors are transported in `result<T,lib_b::error>`,
|
||
* then both are automatically mapped to `result<T,program::error>` once control reaches the appropriate scope.
|
||
|
||
There are several problems with this idea:
|
||
|
||
* It is prone to errors, both during the initial implementation as well as under maintenance.
|
||
|
||
* It does not compose well. For example, if `lib_a` and `lib_b` use `lib_c`, errors that originate in `lib_c` would be obfuscated by the different APIs exposed by each of the higher level libraries.
|
||
|
||
* It presumes that all errors in the program can be specified by exactly one error code, which is false.
|
||
|
||
Consider a program that attempts to read a configuration file from three different locations: in case all of the attempts fail, it should communicate each of the failures. In theory `result<T,E>` handles this case well:
|
||
|
||
[source,c++]
|
||
----
|
||
struct attempted_location
|
||
{
|
||
std::string path;
|
||
error ec;
|
||
};
|
||
|
||
struct config_error
|
||
{
|
||
attempted_location current_dir, user_dir, app_dir;
|
||
};
|
||
|
||
result<config,config_error> read_config();
|
||
----
|
||
|
||
This looks nice, until we realize what the `config_error` type means for the automatic mapping API we wanted to define: an `enum` can not represent a `struct`. It is a fact that we can not assume that all error conditions can be fully specified by an `enum`; an error handling library must be able to transport arbitrary static types efficiently.
|
||
|
||
Conclusion: :: Transporting error objects in return values works great if the failure is handled by the immediate caller of the function that reports it, but most error objects must be communicated across multiple layers of function calls and APIs, which leads to excessive physical coupling between these interfaces.
|
||
|
||
NOTE: While the `leaf::<<result,result>><T>` class template does have value-or-error semantics, it does not carry the actual error objects. Instead, they are forwarded directly to the appropriate error-handling scope and their types do not participate in function signatures.
|
||
|
||
== Alternatives to LEAF
|
||
|
||
* https://www.boost.org/doc/libs/release/libs/exception/doc/boost-exception.html[Boost Exception]
|
||
* https://ned14.github.io/outcome[Boost Outcome]
|
||
* https://github.com/pdimov/variant2[variant2 / `expected<T,E...>`]
|
||
* https://zajo.github.io/boost-noexcept[Noexcept]
|
||
|
||
Below we offer a comparison of LEAF to Boost Exception and to Boost Outcome.
|
||
|
||
[[boost_exception]]
|
||
=== Comparison to Boost Exception
|
||
|
||
While LEAF can be used without exception handling, in the use case when errors are communicated by throwing exceptions, it can be viewed as a better, more efficient alternative to Boost Exception. LEAF has the following advantages over Boost Exception:
|
||
|
||
* LEAF does not allocate memory dynamically;
|
||
* LEAF does not waste system resources communicating error objects not used by specific error handling functions;
|
||
* LEAF does not store the error objects in the exception object, and therefore it is able to augment exceptions thrown by external libraries (Boost Exception can only augment exceptions of types that derive from `boost::exception`).
|
||
|
||
The following tables outline the differences between the two libraries which should be considered when code that uses Boost Exception is refactored to use LEAF instead:
|
||
|
||
.Defining a custom type for transporting values of type T
|
||
[cols="1a,1a",options="header",stripes=none]
|
||
|====
|
||
| Boost Exception | LEAF
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
typedef error_info<struct my_info_,T> my_info;
|
||
----
|
||
[.text-right]
|
||
https://www.boost.org/doc/libs/release/libs/exception/doc/error_info.html[`boost::error_info`]
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
struct my_info { T value; };
|
||
----
|
||
|====
|
||
|
||
.Passing arbitrary info at the point of the throw
|
||
[cols="1a,1a",options="header",stripes=none]
|
||
|====
|
||
| Boost Exception | LEAF
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
throw my_exception() <<
|
||
my_info(x) <<
|
||
my_info(y);
|
||
----
|
||
[.text-right]
|
||
https://www.boost.org/doc/libs/release/libs/exception/doc/exception_operator_shl.html[`operator<<`]
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
throw leaf::exception( my_exception(),
|
||
my_info{x},
|
||
my_info{y} );
|
||
----
|
||
[.text-right]
|
||
<<exception>>
|
||
|====
|
||
|
||
.Augmenting exceptions in exception-neutral contexts
|
||
[cols="1a,1a",options="header",stripes=none]
|
||
|====
|
||
| Boost Exception | LEAF
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
try
|
||
{
|
||
f();
|
||
}
|
||
catch( boost::exception & e )
|
||
{
|
||
e << my_info(x);
|
||
throw;
|
||
}
|
||
----
|
||
[.text-right]
|
||
https://www.boost.org/doc/libs/release/libs/exception/doc/exception.html[`boost::exception`] \| https://www.boost.org/doc/libs/release/libs/exception/doc/exception_operator_shl.html[`operator<<`]
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
auto propagate = leaf::preload( my_info{x} );
|
||
|
||
f();
|
||
----
|
||
[.text-right]
|
||
<<preload>>
|
||
|====
|
||
|
||
.Obtaining arbitrary info at the point of the catch
|
||
[cols="1a,1a",options="header",stripes=none]
|
||
|====
|
||
| Boost Exception | LEAF
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
try
|
||
{
|
||
f();
|
||
}
|
||
catch( my_exception & e )
|
||
{
|
||
if( T * v = get_error_info<my_info>(e) )
|
||
{
|
||
//my_info is available in e.
|
||
}
|
||
}
|
||
----
|
||
[.text-right]
|
||
https://www.boost.org/doc/libs/release/libs/exception/doc/get_error_info.html[`boost::get_error_info`]
|
||
|
|
||
[source,c++,options="nowrap"]
|
||
----
|
||
leaf::try_(
|
||
[ ]
|
||
{
|
||
f();
|
||
}
|
||
[ ]( leaf::catch_<my_exception>, my_info const & x )
|
||
{
|
||
//my_info is available with e.
|
||
} );
|
||
----
|
||
[.text-right]
|
||
<<try_>> | <<catch_>>
|
||
|====
|
||
|
||
.Propagation of <<e_objects>>
|
||
[cols="1a,1a",options="header",stripes=none]
|
||
|====
|
||
| Boost Exception | LEAF
|
||
| All supplied https://www.boost.org/doc/libs/release/libs/exception/doc/error_info.html[`boost::error_info`] objects are allocated dynamically, stored in the https://www.boost.org/doc/libs/release/libs/exception/doc/exception.html[`boost::exception`] object, and propagated.
|
||
| User-defined error objects are stored statically in the scope of <<try_>>, but only if their types are used to handle errors; otherwise they are discarded.
|
||
|====
|
||
|
||
.Transporting of <<e_objects>> across thread boundaries
|
||
[cols="1a,1a",options="header",stripes=none]
|
||
|====
|
||
| Boost Exception | LEAF
|
||
| https://www.boost.org/doc/libs/release/libs/exception/doc/exception_ptr.html[`boost::exception_ptr`] automatically captures https://www.boost.org/doc/libs/release/libs/exception/doc/error_info.html[`boost::error_info`] objects stored in a `boost::exception` and can transport them across thread boundaries.
|
||
| Transporting error objects across thread boundaries requires the use of <<capture_exception>>.
|
||
|====
|
||
|
||
.Printing of error objects in automatically-generated diagnostic information messages
|
||
[cols="1a,1a",options="header",stripes=none]
|
||
|====
|
||
| Boost Exception | LEAF
|
||
| `boost::error_info` types may define conversion to `std::string` by providing `to_string` overloads *or* by overloading `operator<<` for `std::ostream`.
|
||
| LEAF does not use `to_string`. Error types may define `operator<<` overloads for `std::ostream`.
|
||
|====
|
||
|
||
[WARNING]
|
||
====
|
||
The fact that Boost Exception stores all supplied `boost::error_info` objects -- while LEAF discards them if they aren't needed -- affects the completeness of the message we get when we print `leaf::<<diagnostic_info,diagnostic_info>>` objects, compared to the string returned by https://www.boost.org/doc/libs/release/libs/exception/doc/diagnostic_information.html[`boost::diagnostic_information`].
|
||
|
||
If the user requires a complete diagnostic message, the solution is to use `leaf::<<verbose_diagnostic_info,verbose_diagnostic_info>>`. In this case, before unused error objects are discarded by LEAF, they are converted to string and captured. Note that this allocates memory dynamically.
|
||
====
|
||
|
||
'''
|
||
|
||
[[boost_outcome]]
|
||
=== Comparison to Boost Outcome
|
||
|
||
==== Design Differences
|
||
|
||
Like LEAF, the https://ned14.github.io/outcome[Boost Outcome] library is designed to work in low latency environments. It provides two class templates, `result<>` and `outcome<>`:
|
||
|
||
* `result<T,EC,NVP>` can be used as the return type in `noexcept` functions which may fail, where `T` specifies the type of the return value in case of success, while `EC` is an "error code" type. Semantically, `result<T,EC>` is similar to `std::variant<T,EC>`. Naturally, `EC` defaults to `std::error_code`.
|
||
* `outcome<T,EC,EP,NVP>` is similar to `result<>`, but in case of failure, in addition to the "error code" type `EC` it can hold a "pointer" object of type `EP`, which defaults to `std::exception_ptr`.
|
||
|
||
NOTE: `NVP` is a policy type used to customize the behavior of `.value()` when the `result<>` or the `outcome<>` object contains an error.
|
||
|
||
The idea is to use `result<>` to communicate failures which can be fully specified by an "error code", and `outcome<>` to communicate failures that require additional information.
|
||
|
||
Another way to describe this design is that `result<>` is used when it suffices to return an error object of some static type `EC`, while `outcome<>` can also transport a polymorphic error object, using the pointer type `EP`.
|
||
|
||
NOTE: In the default configuration of `outcome<T>` the additional information -- or the additional polymorphic object -- is an exception object held by `std::exception_ptr`. This targets the use case when an exception thrown by a lower-level library function needs to be transported through some intermediate contexts that are not exception-safe, to a higher-level context able to handle it. LEAF supports this case more efficiently, see <<exception_to_result>>.
|
||
|
||
Similar reasoning drives the design of LEAF as well. The difference is that while both libraries recognize the need to transport "something else" in addition to an "error code", LEAF provides an efficient solution to this problem, while Outcome shifts this burden to the user.
|
||
|
||
The `leaf::result<>` template deletes both `EC` and `EP`, which decouples it from the type of the error objects that are transported in case of a failure. This enables lower-level functions to freely communicate anything and everything they "know" about the failure: error code, even multiple error codes, file names, request IDs, etc. At the same time, the higher-level error-handling functions control which of this information is needed in a specific client program and which is not. This is ideal, because:
|
||
|
||
* Authors of lower-level library functions lack context to determine which of the information that is both relevant to the error _and_ naturally available to them needs to be communicated in order for a particular client program to recover from that error;
|
||
* Authors of higher-level error-handling functions easily and confidently make this determination, which they communicate naturally to LEAF, by simply writing the necessary error handlers. LEAF automatically and efficiently transports the needed E-objects while discarding the ones handlers don't use, saving resources.
|
||
|
||
TIP: The LEAF examples include an adaptation of the program from the https://ned14.github.io/outcome/tutorial/result/[Boost Outcome `result<>` tutorial]. You can https://github.com/zajo/leaf/blob/master/example/print_half.cpp?ts=3[view it on GitHub].
|
||
|
||
==== The Interoperability Problem
|
||
|
||
The Boost Outcome documentation discusses the important problem of bringing together multiple libraries -- each using its own error reporting mechanism -- and incorporating them in a robust error handling infrastructure in a client program.
|
||
|
||
Users are advised that whenever possible they should use a common error handling system throughout their entire codebase, but because this is not practical, both the `result<>` and the `outcome<>` templates can carry user-defined "payloads".
|
||
|
||
The following analysis is from the Boost Outcome documentation:
|
||
====
|
||
If library A uses `result<T, libraryA::failure_info>`, and library B uses `result<T, libraryB::error_info>` and so on, there becomes a problem for the application writer who is bringing in these third party dependencies and tying them together into an application. As a general rule, each third party library author will not have built in explicit interoperation support for unknown other third party libraries. The problem therefore lands with the application writer.
|
||
|
||
The application writer has one of three choices:
|
||
|
||
. In the application, the form of result used is `result<T, std::variant<E1, E2, ...>>` where `E1, E2 …` are the failure types for every third party library in use in the application. This has the advantage of preserving the original information exactly, but comes with a certain amount of use inconvenience and maybe excessive coupling between high level layers and implementation detail.
|
||
|
||
. One can translate/map the third party’s failure type into the application’s failure type at the point of the failure exiting the third party library and entering the application. One might do this, say, with a C preprocessor macro wrapping every invocation of the third party API from the application. This approach may lose the original failure detail, or mis-map under certain circumstances if the mapping between the two systems is not one-one.
|
||
|
||
. One can type erase the third party’s failure type into some application failure type, which can later be reconstituted if necessary. This is the cleanest solution with the least coupling issues and no problems with mis-mapping, but it almost certainly requires the use of `malloc` which the previous two did not.
|
||
====
|
||
|
||
The analysis above is clear and precise, but LEAF and Boost Outcome tackle the interoperability problem differently:
|
||
|
||
* The Boost Outcome design asserts that the "cleanest" solution based on type-erasure is suboptimal ("almost certainly requires the use of `malloc`pass:[]"), and instead provides a system for injecting custom converters into the `outcome::convert` namespace, used to translate between library-specific and program-wide error types.
|
||
|
||
* The LEAF design asserts that coupling the signatures of <<rationale,error-neutral>> functions with the static types of the error objects they need to forward to the caller <<translation,does not scale>>, and instead transports error objects directly to error-handling scopes where they are stored statically (without the use of `malloc`).
|
||
|
||
[[distribution]]
|
||
== Distribution
|
||
|
||
The source code is https://github.com/zajo/leaf[available] on GitHub.
|
||
|
||
Copyright (c) 2018-2019 Emil Dotchevski. Distributed under the http://www.boost.org/LICENSE_1_0.txt[Boost Software License, Version 1.0].
|
||
|
||
Please post questions and feedback on the Boost Developers Mailing List (LEAF is not part of Boost).
|
||
|
||
[[portability]]
|
||
== Portability
|
||
|
||
LEAF requires a {CPP}11 compiler.
|
||
|
||
See unit test matrix at https://travis-ci.org/zajo/leaf[Travis-CI]. It has also been tested with Microsoft Visual Studio 2015 and 2017 (please install the latest patches from Microsoft).
|
||
|
||
[[building]]
|
||
== Building
|
||
|
||
LEAF is a header-only library and it requires no building. It does not depend on Boost or on any other library.
|
||
|
||
The unit tests can be run with Boost Build or with https://mesonbuild.com[Meson Build]. To run the unit tests:
|
||
|
||
. If using Boost Build:
|
||
.. Clone LEAF under your `boost/libs` directory.
|
||
.. Execute:
|
||
+
|
||
[source,sh]
|
||
----
|
||
cd leaf/test
|
||
../../../b2
|
||
----
|
||
. If using Meson Build:
|
||
.. Clone LEAF into any local directory.
|
||
.. Execute:
|
||
+
|
||
[source,sh]
|
||
----
|
||
cd leaf
|
||
meson bld/debug
|
||
cd bld/debug
|
||
meson test
|
||
----
|
||
|
||
== Acknowledgements
|
||
|
||
Special thanks to Peter Dimov.
|
||
|
||
Ivo Belchev, Sean Palmer, Jason King, Vinnie Falco, Glen Fernandes, Nir Friedman -- thanks for the valuable feedback. |