I. Design
There is a need in C++ for dealing with failures without using exceptions.
This is not so much driven by actual inefficiencies (as opposed to
perceived inefficiencies) of exception handling, but by the fact that not
all C++ code in existence is exception-safe, and the fact that some C++
programmers work in environments where exception handling is disabled, as
ill-advised such a decision may be.
In my opinion the library should drop option<T> and result<T> altogether
and should be reduced to outcome<T> only, which can be empty, or hold a T,
or hold an exception (but not error code, see below.) This is critical for
a library that aims to provide a _common_ general mechanism for dealing
with failures in interfaces; providing a rich set of alternatives works
against that goal.
(It seems desirable to get rid of the empty state too but the discussions
during the review period show that this might cause more problems than it
solves.)
Secondly, it is not a good idea to use error codes or any other value as
means to dispatch on different kinds of failures because semantically such
dispatch should be static: there is some _code_ which detects the error,
and there is some other _code_ that needs to bind it and handle it. This is
one reason why in C++ catch dispatches by the static type of the exception
objects, not by some exception.what() value.
Further, using static types to communicate different kinds of failures
allows users to recognize and handle an entire class of errors by means of
implicit type conversions, by organizing error types in a hierarchy. For
example:
struct io_error: virtual std::runtime_error { };
struct read_error: virtual io_error { };
struct write_error: virtual io_error { };
struct file_error: virtual io_error { };
struct file_read_error: virtual file_error, virtual read_error { };
struct parse_error: virtual std::runtime_error { };
struct syntax_error: virtual parse_error { };
With this hierarchy, we can use read_error to handle any kind of read
errors (not just file-related), file_error to handle any kind of file
failures (read or write), etc.
That said, outcome<T> should also be able to transport values, however
their purpose should not be to tell _what_ went wrong, but to provide
additional information.
It must be stressed that such additional information should be decoupled
from the classification of the error. For example:
outcome
In my opinion the library should drop option<T> and result<T> altogether and should be reduced to outcome<T> only, which can be empty, or hold a T, or hold an exception (but not error code, see below.) This is critical for a library that aims to provide a _common_ general mechanism for dealing with failures in interfaces; providing a rich set of alternatives works against that goal.
As was covered in the FAQ, outcome<T> may store an exception_ptr, which is implemented using atomics. This causes the compiler to emit a lot more code than a result<T>, which is why we have a result<T> with implicit conversion on demand to an outcome<T>. If one uses the least representative type possible, one gets minimum code bloat and maximum performance.
Secondly, it is not a good idea to use error codes or any other value as means to dispatch on different kinds of failures because semantically such dispatch should be static: there is some _code_ which detects the error, and there is some other _code_ that needs to bind it and handle it. This is one reason why in C++ catch dispatches by the static type of the exception objects, not by some exception.what() value.
Further, using static types to communicate different kinds of failures allows users to recognize and handle an entire class of errors by means of implicit type conversions, by organizing error types in a hierarchy. For example:
struct io_error: virtual std::runtime_error { }; struct read_error: virtual io_error { }; struct write_error: virtual io_error { }; struct file_error: virtual io_error { }; struct file_read_error: virtual file_error, virtual read_error { }; struct parse_error: virtual std::runtime_error { }; struct syntax_error: virtual parse_error { };
With this hierarchy, we can use read_error to handle any kind of read errors (not just file-related), file_error to handle any kind of file failures (read or write), etc.
If you favour using the type system to statically enforce error codes,
then expected
III. Documentation
The documentation makes broad claims about the inefficiency of exception handling which seem to be motivated by a desire to appeal to programmers who hold such beliefs. There is no need to make such generally untrue claims in order to justify the need for Outcome.
As I told you when you raised this originally, no such claims were ever
made. I made a specific claim only - with benchmarks - about the
relative cost of an exception throw and catch to returning an integer
error code or an expected
Should Outcome be accepted into Boost? NO
This obviously applies only to the current state of the library.
Would you be able to list changes you would consider minimum to change your vote to a YES? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Le 29/05/2017 à 01:46, Niall Douglas via Boost a écrit :
In my opinion the library should drop option<T> and result<T> altogether and should be reduced to outcome<T> only, which can be empty, or hold a T, or hold an exception (but not error code, see below.) This is critical for a library that aims to provide a _common_ general mechanism for dealing with failures in interfaces; providing a rich set of alternatives works against that goal. As was covered in the FAQ, outcome<T> may store an exception_ptr, which is implemented using atomics. This causes the compiler to emit a lot more code than a result<T>, which is why we have a result<T> with implicit conversion on demand to an outcome<T>. If one uses the least representative type possible, one gets minimum code bloat and maximum performance.
Why do you need atomics?
Secondly, it is not a good idea to use error codes or any other value as means to dispatch on different kinds of failures because semantically such dispatch should be static: there is some _code_ which detects the error, and there is some other _code_ that needs to bind it and handle it. This is one reason why in C++ catch dispatches by the static type of the exception objects, not by some exception.what() value.
Further, using static types to communicate different kinds of failures allows users to recognize and handle an entire class of errors by means of implicit type conversions, by organizing error types in a hierarchy. For example:
struct io_error: virtual std::runtime_error { }; struct read_error: virtual io_error { }; struct write_error: virtual io_error { }; struct file_error: virtual io_error { }; struct file_read_error: virtual file_error, virtual read_error { }; struct parse_error: virtual std::runtime_error { }; struct syntax_error: virtual parse_error { };
With this hierarchy, we can use read_error to handle any kind of read errors (not just file-related), file_error to handle any kind of file failures (read or write), etc. If you favour using the type system to statically enforce error codes, then expected
is exactly the right object for you.
It seams Emil has a use case for empty or T or exception_ptr :( Vicente
As was covered in the FAQ, outcome<T> may store an exception_ptr, which is implemented using atomics. This causes the compiler to emit a lot more code than a result<T>, which is why we have a result<T> with implicit conversion on demand to an outcome<T>. If one uses the least representative type possible, one gets minimum code bloat and maximum performance.
Why do you need atomics?
It's not my code. It's std::exception_ptr. It uses atomics.
If you favour using the type system to statically enforce error codes, then expected
is exactly the right object for you. It seams Emil has a use case for empty or T or exception_ptr :(
That's an interesting combo. I'll support it after replacing the preprocessor stamped out varieties with template stamped out varieties, the only reason I didn't have it already was due to saving on preprocessor work. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Le 29/05/2017 à 17:43, Niall Douglas via Boost a écrit :
As was covered in the FAQ, outcome<T> may store an exception_ptr, which is implemented using atomics. This causes the compiler to emit a lot more code than a result<T>, which is why we have a result<T> with implicit conversion on demand to an outcome<T>. If one uses the least representative type possible, one gets minimum code bloat and maximum performance. Why do you need atomics? It's not my code. It's std::exception_ptr. It uses atomics. Sorry I misunderstood what you said.
If you favour using the type system to statically enforce error codes, then expected
is exactly the right object for you. It seams Emil has a use case for empty or T or exception_ptr :( That's an interesting combo. I'll support it after replacing the preprocessor stamped out varieties with template stamped out varieties, the only reason I didn't have it already was due to saving on preprocessor work.
Emil, I don't remember, would empty be a success case or a failure case? Is this important? Vicente
participants (3)
-
Emil Dotchevski
-
Niall Douglas
-
Vicente J. Botet Escriba