mirror of
https://github.com/boostorg/leaf.git
synced 2026-02-21 03:02:14 +00:00
2866 lines
104 KiB
Plaintext
2866 lines
104 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: right
|
||
: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-expect,`capture`>>.]
|
||
|
||
* Any error-related object of any type is efficiently delivered to the correct error handler.
|
||
|
||
* Compatible with `std::error_code`, `errno` and any other `noexcept`-moveable type.
|
||
|
||
* Use with or without exception handling.
|
||
|
||
* Support for multi-thread programming.
|
||
|
||
[.text-right]
|
||
https://github.com/zajo/leaf[GitHub] | <<introduction,Introduction>> <<techniques>> | <<synopsis>> | <<reference>> | <<rationale,Design Rationale>>
|
||
====
|
||
|
||
[[introduction]]
|
||
== Five Minute Introduction
|
||
|
||
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. For demonstration, we'll implement two versions: one using error codes, and one using exception handling.
|
||
|
||
[[introduction-result]]
|
||
=== Using Error Codes
|
||
|
||
First, we need an `enum` to define our error codes:
|
||
|
||
[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
|
||
};
|
||
----
|
||
|
||
Very nice, but how does LEAF know that this `enum` represents error codes and not, say, types of cold cuts sold at http://order.bcdeli.com/[Bay Cities Italian Deli]? It doesn't, unless we tell it:
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template<> struct is_e_type<error_code>: std::true_type { };
|
||
|
||
} }
|
||
----
|
||
[.text-right]
|
||
<<is_e_type>>
|
||
|
||
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 `leaf::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 );
|
||
----
|
||
[.text-right]
|
||
<<result>>
|
||
|
||
What's this `result<T>` template, you ask? It's a simple variant: it holds a value of type `T` or else it represents an error condition. This way, functions can communicate failures -- and we don't even need an error code to represent success. Which makes sense if you think about it.
|
||
|
||
Now on to the `main` function: it brings everything together and handles all the errors that may occur. Did I say *all* the errors? I did, so don't be surprised when I tell you that this is done using `leaf::handle_all`. It has the following signature:
|
||
|
||
[source,c++]
|
||
----
|
||
template <class TryBlock, class... Handler>
|
||
<<deduced_return_type>> handle_all( TryBlock && try_block, Handler && ... handler );
|
||
----
|
||
|
||
`TryBlock` is a function type, almost always a lambda, which is required to return an instance of the `result` template. The first thing `handle_all` does is invoke the `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.
|
||
|
||
Now, let's see just what kind of a `TryBlock` does `main` pass to `handle_all`:
|
||
|
||
[source,c++]
|
||
----
|
||
int main( int argc, char const * argv[ ] )
|
||
{
|
||
return leaf::handle_all(
|
||
|
||
[&]() -> leaf::result<int> <1>
|
||
{
|
||
leaf::result<char const *> file_name = parse_command_line(argc,argv);
|
||
if( !file_name )
|
||
return file_name.error(); <2>
|
||
|
||
auto propagate = leaf::preload( leaf::e_file_name{*file_name} ); <3>
|
||
|
||
leaf::result<std::shared_ptr<FILE>> f = file_open(*file_name);
|
||
if( !f )
|
||
return f.error(); <4>
|
||
|
||
leaf::result<int> s = file_size(**f);
|
||
if( !s )
|
||
return s.error(); <4>
|
||
|
||
std::string buffer( 1 + *s, '\0' );
|
||
leaf::result<void> fr = file_read(**f, &buffer[0], buffer.size()-1);
|
||
if( !fr )
|
||
return fr.error(); <4>
|
||
|
||
std::cout << buffer;
|
||
std::cout.flush();
|
||
if( std::cout.fail() )
|
||
return leaf::new_error( cout_error, leaf::e_errno{errno} ); <5>
|
||
|
||
return 0;
|
||
},
|
||
|
||
.... <6>
|
||
|
||
); <7>
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<handle_all>> | <<new_error>> | <<preload>> | <<result>> | <<common>>
|
||
|
||
<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 return that error to `handle_all` (which invoked us). Otherwise, the returned `file_name` stores a value of type `char const *`, which we can access by "dereferencing" it: `*file_name`.
|
||
<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 any failures 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> List of error handler goes here. We'll see that later.
|
||
<7> 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.
|
||
|
||
Now let's look at the juicy second part of the call to `handle_all`, which lists our error handler:
|
||
|
||
[source,c++]
|
||
----
|
||
int main( int argc, char const * argv[ ] )
|
||
{
|
||
return leaf::handle_all(
|
||
|
||
[&]() -> leaf::result<int>
|
||
{
|
||
.... <1>
|
||
},
|
||
|
||
[ ](leaf::match<error_code, input_file_open_error>, <2>
|
||
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>, <3>
|
||
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>, <4>
|
||
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>, <5>
|
||
leaf::e_errno const & errn)
|
||
{
|
||
std::cerr << "Output error, errno=" << errn << std::endl;
|
||
return 4;
|
||
},
|
||
|
||
[ ](leaf::match<error_code, bad_command_line>) <6>
|
||
{
|
||
std::cout << "Bad command line argument" << std::endl;
|
||
return 5;
|
||
},
|
||
|
||
[ ](leaf::error_info const & unmatched) <7>
|
||
{
|
||
std::cerr <<
|
||
"Unknown failure detected" << std::endl <<
|
||
"Cryptic diagnostic information follows" << std::endl <<
|
||
unmatched;
|
||
return 6;
|
||
}
|
||
);
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<handle_all>> | <<match>> | <<common>>
|
||
|
||
<1> This is the `TryBlock` from the previous listing, it does all the work and bails out if it encounters an error. In that case, `handle_all` will consider the error handler that follow, in order, and it will call the first one that can deal with the error:
|
||
|
||
<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` that has `.value` equal to `ENOENT`, 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 `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`.
|
||
|
||
<4> 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`.
|
||
|
||
<5> 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`),
|
||
|
||
<6> This handler will be called if the error includes an object of type `error_code` equal to `bad_command_line`.
|
||
|
||
<7> 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. In this program this handler will never be called, but it is required by `handle_all` because, well, it must handle all errors (the alternative is to use `handle_some` instead, which doesn't require a "catch all" last-resort handler; instead, if it fails to find a suitable handler for an error, it returns the error to its caller.)
|
||
|
||
To conclude this introduction, let's look at one of the error-reporting functions that our `TryBlock` calls, for example `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} );
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<result>> | <<new_error>> | <<common>>
|
||
|
||
If `fopen` succeeds, it returns a `shared_ptr` which will automatically call `fclose` when needed. If it 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 error code (`input_file_open_error`), as well as the system `errno` (LEAF defines `struct e_errno { int value; }`).
|
||
|
||
NOTE: 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.
|
||
|
||
TIP: When used with exception handling, LEAF can be viewed as <<boost_exception,a better Boost Exception>>.
|
||
|
||
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 occurs when catching a base type, it is generally recommended to use virtual inheritance in exception type hierarchies.
|
||
|
||
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_return_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 that 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>
|
||
std::shared_ptr<FILE> f = file_open( file_name );
|
||
|
||
auto propagate1 = leaf::preload( leaf::e_file_name{file_name} ); <4>
|
||
|
||
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>
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<try_>> | <<preload>> | <<defer>> | <<common>>
|
||
|
||
<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 an 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 in order to include the relevant `e_errno` with the exception (LEAF defines `struct e_errno { int value; };` ).
|
||
<6> List of error handler 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 handler:
|
||
|
||
[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;
|
||
} );
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<try_>> | <<catch_>> | <<match>> | <<common>>
|
||
|
||
<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 handler below, 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 in the program.
|
||
|
||
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} );
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<exception>> | <<common>>
|
||
|
||
If `fopen` succeeds, it returns a `shared_ptr` which will automatically call `fclose` when needed. If it 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: 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
|
||
|
||
=== `error.hpp`
|
||
include::{sourcedir}/synopses/error.adoc[]
|
||
|
||
'''
|
||
|
||
=== `preload.hpp`
|
||
include::{sourcedir}/synopses/preload.adoc[]
|
||
|
||
'''
|
||
|
||
=== `result.hpp`
|
||
include::{sourcedir}/synopses/result.adoc[]
|
||
|
||
'''
|
||
|
||
=== `handle.hpp`
|
||
include::{sourcedir}/synopses/handle.adoc[]
|
||
|
||
'''
|
||
|
||
=== `throw.hpp`
|
||
include::{sourcedir}/synopses/throw.adoc[]
|
||
|
||
'''
|
||
|
||
=== `try.hpp`
|
||
include::{sourcedir}/synopses/try.adoc[]
|
||
|
||
'''
|
||
|
||
=== `common.hpp`
|
||
include::{sourcedir}/synopses/common.adoc[]
|
||
|
||
'''
|
||
|
||
=== `capture_result.hpp`
|
||
include::{sourcedir}/synopses/capture_result.adoc[]
|
||
|
||
[.text-right]
|
||
<<capture_result>>
|
||
|
||
'''
|
||
|
||
=== `capture_exception.hpp`
|
||
include::{sourcedir}/synopses/capture_exception.adoc[]
|
||
|
||
'''
|
||
|
||
=== `exception_to_result.hpp`
|
||
include::{sourcedir}/synopses/exception_to_result.adoc[]
|
||
|
||
[.text-right]
|
||
`<<diagnostic_output_current_exception,diagnostic_output_current_exception>>`
|
||
|
||
[[reference]]
|
||
|
||
[[e_objects]]
|
||
== E-Objects
|
||
|
||
[[is_e_type]]
|
||
=== `is_e_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_e_type,is_e_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_e_type` is `true` are called E-types. Objects of those types are called E-objects.
|
||
|
||
The main `is_e_type` template is defined so that `is_e_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>> value, 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>> value -- 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 `copy_file` function 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
|
||
|
||
LEAF is able to automatically generate diagnostic messages that include information about all E-objects available in an error-handling scope. For this purpose, it needs to be able to print objects of user-defined E-types.
|
||
|
||
First, 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: This 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.
|
||
|
||
'''
|
||
|
||
[[reporting]]
|
||
== Reporting Errors
|
||
|
||
|
||
'''
|
||
|
||
[[error]]
|
||
=== `error`
|
||
|
||
include::{sourcedir}/synopses/error.adoc[]
|
||
|
||
Objects of class `error` are values that identify an error across the entire program. They can be copied, moved, assigned to, and compared to other `error` objects. They occupy as much memory, and are as efficient as `unsigned int`.
|
||
|
||
'''
|
||
|
||
[[new_error]]
|
||
==== `new_error`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... E>
|
||
error new_error( E && ... e ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: :: `<<is_e_type,is_e_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` value, which is unique across the entire program.
|
||
|
||
'''
|
||
|
||
[[next_error]]
|
||
==== `next_error`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
error next_error() noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: The `error` 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` 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` object returned by the earlier call to `next_error`).
|
||
|
||
TIP: `error` values are unique across the entire program.
|
||
|
||
'''
|
||
|
||
[[last_error]]
|
||
==== `last_error`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
error last_error() noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: The `error` value returned the last time <<new_error>> was invoked from the calling thread.
|
||
|
||
TIP: `error` values are unique across the entire program.
|
||
|
||
'''
|
||
|
||
[[propagate]]
|
||
==== `propagate`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... E>
|
||
error error::propagate( E && ... e ) const noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Effects: :: Each of the `e...` objects is <<propagation,propagated>> and uniquely associated with `*this`.
|
||
|
||
Returns: :: `*this`.
|
||
|
||
'''
|
||
|
||
[[operator_eq-error]]
|
||
==== `operator==`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
bool operator==( error const & e1, error const & e2 ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: `true` if the two values `e1` and `e2` are equal, `false` otherwise.
|
||
|
||
'''
|
||
|
||
[[operator_neq-error]]
|
||
==== `operator!=`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
bool operator!=( error const & e1, error const & e2 ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Returns: :: `!(e1==e2)`.
|
||
|
||
'''
|
||
|
||
[[operator_shl-error]]
|
||
==== `operator<<`
|
||
|
||
.#include <boost/leaf/error.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
std::ostream & operator<<( std::ostream & os, error const & e )
|
||
|
||
} }
|
||
----
|
||
|
||
Effects: :: Prints an `unsigned int` value that uniquely identifies the value `e`.
|
||
|
||
'''
|
||
|
||
[[result]]
|
||
=== `result`
|
||
|
||
include::{sourcedir}/synopses/result.adoc[]
|
||
|
||
'''
|
||
|
||
[[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 const & v );
|
||
|
||
template <class T>
|
||
result<T>::result( T && v ) noexcept;
|
||
|
||
template <class T>
|
||
result<T>::result( leaf::error const & e ) noexcept;
|
||
|
||
template <class T>
|
||
result<T>::result( leaf::error_capture const & ec ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
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,error>>`, 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>> object, it contains captured E-objects.
|
||
|
||
To get a `result<T>` in value state, initialize it with an object of type `T` or use the default constructor.
|
||
|
||
To get a `result<T>` in error state, initialize it with an <<error>> object.
|
||
|
||
To get a `result<T>` in error-capture state, call <<capture_result>>.
|
||
|
||
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_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` was initialized in value state, returns `true`, otherwise returns `false`. See `<<result::result,Constructors>>`.
|
||
|
||
'''
|
||
|
||
[[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` was initialized in value state, returns a reference to the stored value, otherwise throws `leaf::<<bad_result,bad_result>>`. See `<<result::result,Constructors>>`.
|
||
|
||
'''
|
||
|
||
[[result::error]]
|
||
==== `error`
|
||
|
||
.#include <boost/leaf/result.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class T>
|
||
template <class... E>
|
||
leaf::error result<T>::error( E && ... e ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
This member function is designed to be used in `return` statements in functions that return `result<T>` (or `leaf::<<error,error>>`) to return an error to the caller.
|
||
|
||
Returns: ::
|
||
* If `*this` is in value state, returns `<<new_error,new_error>>(std::forward<E>(e...))`, which begins propagating a new `error` value (as opposed to forwarding an existing `error` value);
|
||
* If `*this` is in error-capture state, all captured E-objects are moved to available storage in active <<handle_some>>, <<handle_all>> or <<try_>> scopes, `*this` is converted to error state, and then
|
||
* If `*this` is in error state, returns `err.<<propagate,propagate>>(std::forward<E>(e...))`, where `err` is the <<error>> value 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 value state.
|
||
|
||
'''
|
||
|
||
[[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 all of E, `<<is_e_type,is_e_type>><E~i~>::value` is `true`.
|
||
|
||
Returns: :: An object of unspecified type which derives publicly from `Ex` *and* from class `<<error,error>>` such that:
|
||
* its `Ex` subobject is initialized by `std::forward<Ex>(ex)`;
|
||
* its `error` 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,error>>`.
|
||
|
||
NOTE: To automatically capture `pass:[__FILE__]`, `pass:[__LINE__]` and `pass:[__FUNCTION__]` with the returned object, use <<LEAF_EXCEPTION>> instead of `leaf::exception`.
|
||
|
||
'''
|
||
|
||
[[preload]]
|
||
=== `preload`
|
||
|
||
.#include <boost/leaf/preload.hpp>
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class... E>
|
||
<<unspecified-type>> preload( E && ... e ) noexcept;
|
||
|
||
} }
|
||
----
|
||
|
||
Requirements: :: `<<is_e_type,is_e_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 a new <<error>> value was created (in the calling thread) since the object returned by `preload` was created, the stored `e...` objects are <<propagation,propagated>> and become associated with the _last_ such `leaf::error` value;
|
||
* Otherwise, if `std::unhandled_exception()` returns `true`, the stored `e...` objects are propagated and become associated with the _first_ `leaf::error` value 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 the `f~i~` objects 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_e_type,is_e_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 a new <<error>> value was created (in 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 associated with the _last_ such `leaf::error` value;
|
||
* Otherwise, if `std::unhandled_exception()` returns `true`, each of the stored `f...` is called, and the returned objects are propagated and become associated with the _first_ `leaf::error` value created later on;
|
||
* Otherwise, the stored `f...` objects are discarded.
|
||
|
||
'''
|
||
|
||
[[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 addition to all E-objects `e...`, in a `<<common,e_source_location>>` object.
|
||
|
||
'''
|
||
|
||
[[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();
|
||
}
|
||
----
|
||
|
||
Alternatively:
|
||
|
||
.Try to send a message, then compute a value, report errors without LEAF_CHECK:
|
||
----
|
||
leaf::result<float> add_values()
|
||
{
|
||
if( auto r = send_message("Hello!") )
|
||
return compute_value();
|
||
else
|
||
return r.error();
|
||
}
|
||
----
|
||
|
||
'''
|
||
|
||
[[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>>.
|
||
|
||
== Error Handling
|
||
|
||
[[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 );
|
||
|
||
} }
|
||
----
|
||
|
||
When errors are communicated by `<<result,result>><T>`, 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 `<<result,result>><T>`;
|
||
* All `handler...` arguments must be functions that return the same type as the `try_block`, or types that convert implicitly to it (note: a `result<T>` object can be initialized by an <<error>> object, regardless of `T`);
|
||
|
||
* 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 an <<diagnostic_info>> argument by `const &`;
|
||
** may take a <<verbose_diagnostic_info>> argument by `const &`;
|
||
** 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>>, and <<verbose_diagnostic_info>> types 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 a `<<result,result>><T>` 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, and is uniquely associated with the specific <<error>> value returned by `new_error`. Objects of E-types for which none of these scopes have available storage are discarded.
|
||
|
||
Return Value: ::
|
||
If the `<<result,result>><T>` object `r` returned by the `try_block` function indicates success, `handle_some` returns `r`.
|
||
+
|
||
Otherwise, `handle_some` considers each of the `handler...` functions, in order, until it finds one that matches the reported error. The first matching handler is invoked and the `result<T>` it returns is forwarded to the caller of `handle_some`; if no match is found, `handle_some` returns `r`.
|
||
+
|
||
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` 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.
|
||
+
|
||
If `e` is the specific <<error>> value stored in the `result<T>` returned by the `try_block`, each argument value `a`~i~ to be passed to the `handler` currently under consideration is produced as follows:
|
||
+
|
||
--
|
||
* If `a`~i~ is of type `error_info const &` or `diagnostic_info const &` or `verbose_diagnostic_info const &`, `handle_some` is always able to produce it.
|
||
* If `a`~i~ is of type `A`~i~ `const &`:
|
||
** If an E-object of type `A`~i~, associated with `e`, is currently stored in the `handle_some` scope, `a`~i~ is initialized with a reference to the stored object; otherwise the handler is dropped.
|
||
** If `A`~i~ is of the predicate type `<<match,match>><E>`, if an object of type `E`, associated with `e`, 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`.
|
||
* 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 `e`, 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`.
|
||
* 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 &` and `verbose_diagnostic_info const &`, if a handler only takes arguments of these types, it 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:
|
||
+
|
||
--
|
||
* All `handler...` arguments must be functions that return `T` (rather than `result<T>`), or types that convert implicitly to `T`.
|
||
--
|
||
And the following addition:
|
||
* At least one `handler` function (usually the last) must match any error.
|
||
|
||
Effects: ::
|
||
|
||
The `handle_all` function works the same as <<handle_some>>, but because it is guaranteed to be able to match any error communicated by the `try_block` with a `handler`, it doesn't need to return a `result<T>`.
|
||
|
||
.Example
|
||
[source,c++]
|
||
----
|
||
int main()
|
||
{
|
||
return leaf::handle_all(
|
||
[ ]() -> leaf::result<int>
|
||
{
|
||
//do work, bail out returning leaf::error for any failure
|
||
....
|
||
|
||
std::cout << "Success!";
|
||
return 0;
|
||
},
|
||
[ ]
|
||
{
|
||
std::cerr << "Error!";
|
||
return 1;
|
||
} );
|
||
}
|
||
----
|
||
|
||
'''
|
||
|
||
[[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 with a `catch(std::exception const &)` or, if that fails, `catch(...)`.
|
||
|
||
Return Value: ::
|
||
|
||
* If the `try_block` succeeds, `try_` forwards the return value to the caller;
|
||
* Otherwise, it attempts match the <<error>> 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.
|
||
|
||
Handler Matching Procedure: ::
|
||
|
||
Because each E-object stored in the `try_` scope (see <<handle_some>>) is uniquely associated with a specific <<error>> value, `try_` needs to extract an `error` 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>> value 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` 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` value returned by <<next_error>>, which is a preview of sorts, of the `error` 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` value, E-objects are looked up using the `error` value returned by <<next_error>> just before `try_` goes through its `handler`-matching search. If imperfect, this approach provides the needed association.
|
||
+
|
||
With the `error` 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 of type `A`~i~ `const &`:
|
||
** 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`.
|
||
|
||
'''
|
||
|
||
[[match]]
|
||
=== `match`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
template <class E, typename deduce-type<E>::type... V>
|
||
struct match
|
||
{
|
||
using type = typename deduce-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 of `V...` values.
|
||
|
||
.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());
|
||
----
|
||
|
||
'''
|
||
|
||
[[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 `value` with `ex`.
|
||
|
||
The `catch_` template is a predicate function type: `operator()` returns `true` iff the expression `dynamic_cast<Ex~i~ const *>(&value) != 0` is `true` for at least one of the `Ex...` types.
|
||
|
||
.Example:
|
||
[source,c++]
|
||
----
|
||
struct exception1: std::exception { };
|
||
struct exception2: std::exception { };
|
||
struct exception3: std::exception { };
|
||
|
||
exception2 x;
|
||
|
||
catch_<exception2> c1(x);
|
||
assert(!c1());
|
||
|
||
catch_<exception2> c2(x);
|
||
assert(c2());
|
||
|
||
catch_<exception1,exception2> c3(x);
|
||
assert(c2());
|
||
|
||
catch_<exception1,exception3> c4(x);
|
||
assert(!c4());
|
||
----
|
||
|
||
'''
|
||
|
||
[[error_info]]
|
||
=== `error_info`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
class error_info
|
||
{
|
||
//Constructors unspecified
|
||
|
||
public:
|
||
|
||
leaf::error const & get_error() const noexcept;
|
||
|
||
bool exception_caught() const noexcept;
|
||
|
||
std::exception const * get_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 `get_error` member function returns the program-wide unique identifier of the <<error>>.
|
||
|
||
The `exception_caught` member function returns `true` if the handler that received `*this` is being invoked by <<try_>>, `false` otherwise.
|
||
|
||
The `get_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 `get_exception` unless `exception_caught()` is `true`.
|
||
|
||
The `operator<<` overload prints diagnostic information about all E-objects, associated with the <<error>> value returned by `get_error()`, currently stored in any active <<handle_some>>, <<handle_all>> or <<try_>> scope.
|
||
|
||
'''
|
||
|
||
[[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 information about E-objects that were communicated to LEAF for which there was no storage available in any active <<handle_some>>, <<handle_all>> or <<try_>> scope.
|
||
|
||
The additional information includes the total count, as well as the type of the first such E-object.
|
||
|
||
'''
|
||
|
||
[[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 for which there was no storage available in any active <<handle_some>>, <<handle_all>> or <<try_>> scope.
|
||
|
||
The additional information includes the types and the values of all such E-objects.
|
||
|
||
WARNING: Using `verbose_diagnostic_info` may allocate memory dynamically.
|
||
|
||
== Multi-Thread Programming
|
||
|
||
[[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.
|
||
|
||
'''
|
||
|
||
[[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.
|
||
|
||
[[common]]
|
||
== Common E-Types
|
||
include::{sourcedir}/synopses/common.adoc[]
|
||
|
||
This header defines some common error types which can be used directly.
|
||
|
||
'''
|
||
|
||
[[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 which returned an error. 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.
|
||
|
||
WARNING: It is a logic error to use `e_errno` with <<preload>>; it should be passed to <<new_error>> directly, or used with <<defer>>.
|
||
|
||
'''
|
||
|
||
[[e_LastError]]
|
||
=== `e_LastError`
|
||
|
||
[source,c++]
|
||
----
|
||
namespace boost { namespace leaf {
|
||
|
||
namespace windows
|
||
{
|
||
struct e_LastError
|
||
{
|
||
unsigned value;
|
||
};
|
||
}
|
||
|
||
} }
|
||
----
|
||
|
||
`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 communicating 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.
|
||
|
||
[[techniques]]
|
||
== Programming Techniques
|
||
|
||
[[technique_preload]]
|
||
=== Preloading Errors
|
||
|
||
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 tha 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 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`.
|
||
|
||
'''
|
||
|
||
[[technique_disparate_error_types]]
|
||
=== Working with Disparate Error Codes
|
||
|
||
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_ do we have to express every error in terms of the same static type, `std::error_code`? 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_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 error objects with any exception or error reported by a function.
|
||
|
||
But what if we need to include some error 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(e).propagate( e_this{....}, e_that{....} );
|
||
throw;
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<get_error>> | <<propagate>>
|
||
|
||
The reason we need to use <<get_error>> is that not all exception types derive from `leaf::<<error,error>>`. If the caught exception has a `leaf::error` subobject, `get_error` will return that `leaf::error` value. Also, such exceptions can be intercepted by `catch( error e )` if needed.
|
||
|
||
But if the caught exception doesn't have a `leaf::error` subobject, `get_error` returns an unspecified `leaf::error` value, 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.
|
||
|
||
'''
|
||
|
||
[[technique_preload_in_c_callbacks]]
|
||
=== Using `next_error` in 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 called 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_e_type<do_work_error_code>: std::true_type { };
|
||
|
||
} }
|
||
----
|
||
[.text-right]
|
||
<<is_e_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` object we will definitely return from the `call_lua` function (below)...
|
||
<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 `leaf::<<error,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` value. 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 handles all failures:
|
||
|
||
[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";
|
||
} );
|
||
|
||
return 0;
|
||
}
|
||
----
|
||
[.text-right]
|
||
<<handle_all>> | <<result>>
|
||
|
||
<1> If the call to `call_lua` succeeded, just print the answer.
|
||
<2> Handle `e_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 E-Objects Between Threads
|
||
|
||
With LEAF, error objects use automatic storage duration, stored inside `<<expect,expect>>` instances. When using concurrency, we need a mechanism to detach error 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>`
|
||
|
||
Without exceptions, transporting error objects between threads is as easy as calling `<<capture-result,capture>>`, passing the `<<expect,expect>>` object whose contents needs to be transported, and a `<<result,result>><T>` which may be in either value state or error state. This gets us a new `<<result,result>><T>` object which can be safely sent across thread boundaries.
|
||
|
||
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 a lambda function that captures its result:
|
||
|
||
[source,c++]
|
||
----
|
||
std::future<leaf::result<task_result>> launch_task()
|
||
{
|
||
return std::async( std::launch::async, [ ]
|
||
{
|
||
leaf::expect<E1,E2,E3> exp;
|
||
return capture(exp,task());
|
||
} );
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<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++]
|
||
----
|
||
....
|
||
leaf::expect<E1,E2,E3> exp;
|
||
|
||
if( leaf::result<task_result> r = fut.get() )
|
||
{
|
||
//Success! Use *r to access task_result.
|
||
}
|
||
else
|
||
{
|
||
handle_error( exp, r,
|
||
|
||
[ ] ( E1 e1, E2 e2 )
|
||
{
|
||
//Deal with E1, E2
|
||
},
|
||
|
||
[ ] ( E3 e3 )
|
||
{
|
||
//Deal with E3
|
||
}
|
||
|
||
);
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<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
|
||
|
||
When using exception handling, we need to capture the exception using `std::exception_ptr`, then capture the current error objects in an `<<error_capture,error_capture>>` and wrap both into another exception. In the main thread we unwrap and throw the original exception.
|
||
|
||
This, of course, is done automatically by LEAF. Let's assume we have a `task` which produces a `task_result` and throws on errors:
|
||
|
||
[source,c++]
|
||
----
|
||
task_result task();
|
||
----
|
||
|
||
When we launch the asynchronous task, we wrap it in a simple lambda function which calls `<<capture_exception,capture_exception>>`, specifying which error types we need transported:
|
||
|
||
[source,c++]
|
||
----
|
||
std::future<task_result> launch_task()
|
||
{
|
||
return std::async( std::launch::async,
|
||
leaf::capture_exception<E1,E2,E3>( [ ]
|
||
{
|
||
return task();
|
||
} ) );
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<capture_exception>>
|
||
|
||
Later, instead of using `std::future::get`, we use `leaf::<<get,get>>`, then catch exceptions as if the function was called locally:
|
||
|
||
[source,c++]
|
||
----
|
||
....
|
||
leaf::expect<E1,E2,E3> exp;
|
||
|
||
try
|
||
{
|
||
task_result r = leaf::get(fut);
|
||
//Success!
|
||
}
|
||
catch( my_exception & e )
|
||
{
|
||
handle_exception( exp, e,
|
||
|
||
[ ] ( E1 e1, E2 e2 )
|
||
{
|
||
//Deal with E1, E2
|
||
},
|
||
|
||
[ ] ( E3 e3 )
|
||
{
|
||
//Deal with E3
|
||
}
|
||
|
||
);
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
`<<expect,expect>>` | <<get>> | <<handle_exception>>
|
||
|
||
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; the coupling which is to be avoided by _error-neutral_ functions is unavoidable and even desirable here.
|
||
|
||
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, which 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 with the following
|
||
|
||
Adjustment: ::
|
||
* *Error-handling* functions should specify 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, even if it is relevant to the failure.
|
||
|
||
The `leaf::<<expect,expect>><E...>` class template implements this idea: it provides local storage for error objects of the `E...` types. Users instantiate this template in _error-handling_ functions, knowing which types of error objects are needed. When an `expect` object is created, `thread_local` pointers of the `E...` types are set to point to the corresponding storage within it. 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 (that's the function where the `expect` object resides).
|
||
* If the pointer is null, storage is not available and the error object is discarded, since there is no use for it -- saving resources.
|
||
|
||
This almost works, except we need to make sure that _error-handling_ functions are protected from accessing stale error objects stored in `expect` objects, left there from previous failures, which would be a serious logic error. To this end, each failure is assigned a unique serial number, which is transported inside `leaf::<<result,result>>` objects that communicate failures. Each of the `E...` objects stored in an `expect<E...>` object is assigned the same unique identifier, permanently associating it with a particular failure.
|
||
|
||
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
|
||
{
|
||
T r = process_file();
|
||
|
||
//Success, use r:
|
||
....
|
||
}
|
||
catch( file_read_error<e_file_name,e_errno> & e )
|
||
{
|
||
std::cerr <<
|
||
"Could not read " << e.get<e_file_name>() <<
|
||
", errno=" << e.get<e_errno>() << std::endl;
|
||
}
|
||
catch( file_read_error<e_errno> & e )
|
||
{
|
||
std::cerr <<
|
||
"File read error, errno=" << e.get<e_errno>() << std::endl;
|
||
}
|
||
catch( file_read_error<> & e )
|
||
{
|
||
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 error objects available. Unfortunately this syntax is not possible and, even if it were, not all programs use exceptions to handle errors.
|
||
|
||
Below is the same snippet, expressed in terms of LEAF:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::expect<e_file_name, e_errno> exp;
|
||
|
||
try
|
||
{
|
||
T r = process_file(); //Throws in case of failure, error objects stored inside exp.
|
||
|
||
//Success, use r:
|
||
....
|
||
}
|
||
catch( file_read_error & e )
|
||
{
|
||
//Match available error objects stored in exp,
|
||
//associated with the specific failure communicated by e.
|
||
leaf::handle_exception( exp, e,
|
||
|
||
[ ]( e_file_name const & file_name, e_errno const & errno_ )
|
||
{
|
||
std::cerr <<
|
||
"Could not read " << file_name.value <<
|
||
", errno=" << errno_ << std::endl;
|
||
},
|
||
|
||
[ ]( e_errno const & errno_ )
|
||
{
|
||
std::cerr <<
|
||
"File read error, errno=" << errno_ << std::endl;
|
||
},
|
||
|
||
[ ]
|
||
{
|
||
std::cerr << "File read error!" << std::endl;
|
||
}
|
||
|
||
);
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
`<<expect,expect>>` | <<handle_exception>>
|
||
|
||
Of course LEAF works without exception handling as well. Below is the same snippet, written using `<<result,result>><T>`:
|
||
|
||
[source,c++]
|
||
----
|
||
leaf::expect<e_what, e_file_name, e_errno> exp;
|
||
|
||
if( leaf::result<T> r = process_file() ) //In case of failure error objects are stored inside exp.
|
||
{
|
||
//Success, use r.value():
|
||
....
|
||
}
|
||
else
|
||
{
|
||
//e_what is used to dispatch between error conditions:
|
||
switch( *leaf::peek<e_what>(exp,r) )
|
||
{
|
||
case file_read_error:
|
||
{
|
||
//Match available error objects stored in exp,
|
||
//associated with the specific failure communicated by r.
|
||
bool handled = leaf::handle_error( exp, r,
|
||
|
||
[ ]( e_file_name const & file_name, e_errno const & errno_ )
|
||
{
|
||
std::cerr <<
|
||
"Could not read " << file_name.value <<
|
||
", errno=" << errno_ << std::endl;
|
||
},
|
||
|
||
[ ]( e_errno const & errno_ )
|
||
{
|
||
std::cerr <<
|
||
"File read error, errno=" << errno_ << std::endl;
|
||
},
|
||
|
||
[ ]
|
||
{
|
||
std::cerr << "File read error!" << std::endl;
|
||
}
|
||
|
||
);
|
||
if( handled )
|
||
break;
|
||
}
|
||
//fallthrough:
|
||
default:
|
||
return r; //Error not handled, forward to the caller.
|
||
}
|
||
----
|
||
|
||
[.text-right]
|
||
<<result>>
|
||
|
||
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`)."
|
||
-- Herb Sutterfootnote:[https://herbsutter.com/2007/01/24/questions-about-exception-specifications/]
|
||
|
||
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 exaclty 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, and therefore we can not assume that all error conditions can be fully specified by an enum; it must support arbitrary static types.
|
||
|
||
Conclusion: :: Transporting error objects statically in return values works great if the failure is handled in 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::expect<my_info> exp;
|
||
try
|
||
{
|
||
f();
|
||
}
|
||
catch( my_exception & e )
|
||
{
|
||
leaf::handle_exception( exp, e,
|
||
[ ]( my_info const & x )
|
||
{
|
||
//my_info is available in e.
|
||
} );
|
||
}
|
||
----
|
||
[.text-right]
|
||
`<<expect,expect>>` \| <<handle_exception>> \| <<peek-expect>>
|
||
|====
|
||
|
||
.Error object propagation
|
||
[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 `leaf::<<expect,expect>>` objects but only if their types are used in the instantiation the `expect` class template; otherwise they are discarded.
|
||
|====
|
||
|
||
.Transporting of error 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 printed by `leaf::<<diagnostic_output-expect,diagnostic_output>>` compared to the string returned by https://www.boost.org/doc/libs/release/libs/exception/doc/diagnostic_information.html[`boost::diagnostic_information`].
|
||
|
||
By default, the `leaf::<<diagnostic_output-expect,diagnostic_output>>` includes only error objects of types used in the instantiation of the `<<expect,expect>>` class template. If the user requires a complete `diagnostic_output` message, the solution is to use <<e_unexpected_diagnostic_output>> with `expect`. In this case, before unused error objects are discarded by LEAF, they are converted to string and stored in the `e_unexpected_diagnostic_output` object. 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.
|
||
|
||
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 can easily and confidently make this determination, and instantiate the `<<expect,leaf::expect>>` class template with the error types they do in fact need; LEAF automatically and efficiently discards error objects of all other types that error-reporting functions attempt to communicate, 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 is not practical, and instead transports error objects directly to error-handling contexts 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 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
|
||
|
||
Thanks to Peter Dimov for the countless library design discussions. Ivo Belchev, Sean Palmer, Jason King, Vinnie Falco, Glen Fernandes, Nir Friedman -- thanks for your feedback. |