[review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018)
Hi, Everyone,
The formal review of Niall Douglas' Outcome (v2) library begins Fri-19-Jan
and continues until Sun-28-Jan, 2018.
Your participation is encouraged, as the proposed library is uncoupled and
focused, and reviewers do not need to be domain experts to appreciate the
potential usefulness of the library and to propose improvements. Everyone
needs (and has suffered) error handling, and can compose an opinion on that
topic.
Outcome is a header-only C++14 library providing expressive and very
lightweight 'outcome<T>' and 'result<T>' error handling, suitable for
low-latency code bases. The library further provides mechanisms for
wrapping '
The formal review of Niall Douglas' Outcome (v2) library begins Fri-19-Jan and continues until Sun-28-Jan, 2018.
I'd just like to take this opportunity to thank all the people who responded to my call two weeks ago to help comb through the reference API docs looking for problems and reporting them. I know that we did not fix all of the problems in time, but what you see before you now is enormously better than they were just two weeks ago. That's thanks to you, so thank you. I'd like to thank Jens Weller for specially expediting the mastering of the Meeting C++ video so it could appear in the Outcome docs in time for this review. And definitely the star of the past two weeks has been Jonathan Müller who has expended many, many hours adding features and fixing bugs in Standardese to create the reference API docs you see, despite a heavy class workload and often really terrible low quality bug reports by me submitted usually when he was just about to go to bed. Thank you Jonathan! Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
AMDG On 01/18/2018 03:07 PM, charleyb123 . via Boost wrote:
Tarball: *- https://github.com/ned14/boost-outcome/releases/tag/v2. 0-boost-peer-review
If you're going to provide this, it should contain documentation that I can read locally. In Christ, Steven Watanabe
Tarball: *- https://github.com/ned14/boost-outcome/releases/tag/v2. 0-boost-peer-review
If you're going to provide this, it should contain documentation that I can read locally.
The particular theme I chose requires a real web server, it doesn't work from a file:// address. Hence no point in bundling documentation that you can read locally in the tarball. If you would like to read the documentation locally, here's how: 1. Download a prebuilt Hugo binary for your system from https://github.com/gohugoio/hugo/releases 2. Fetch the standalone tarball from https://dedi4.nedprod.com/static/files/outcome-v2.0-source-latest.tar.xz and unpack it. 3. Unpack the Hugo binary into outcome/doc/src 4. In a terminal, cd to outcome/doc/src 5. Run 'hugo serve' and follow the instructions it prints with regard to a http site on localhost. If you run into any problems, let me know. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
AMDG On 01/23/2018 02:58 PM, Niall Douglas via Boost wrote:
Tarball: *- https://github.com/ned14/boost-outcome/releases/tag/v2. 0-boost-peer-review
If you're going to provide this, it should contain documentation that I can read locally.
The particular theme I chose requires a real web server, it doesn't work from a file:// address. Hence no point in bundling documentation that you can read locally in the tarball.
Frankly, I find this completely ridiculous. It's also goes against Boost's documentation guidelines: http://www.boost.org/development/requirements.html#Documentation "The format for documentation should be HTML, and should not require an advanced browser or server-side extensions"
If you would like to read the documentation locally, here's how:
1. Download a prebuilt Hugo binary for your system from https://github.com/gohugoio/hugo/releases
2. Fetch the standalone tarball from https://dedi4.nedprod.com/static/files/outcome-v2.0-source-latest.tar.xz and unpack it.
3. Unpack the Hugo binary into outcome/doc/src
4. In a terminal, cd to outcome/doc/src
5. Run 'hugo serve' and follow the instructions it prints with regard to a http site on localhost.
If you run into any problems, let me know.
In Christ, Steven Watanabe
The particular theme I chose requires a real web server, it doesn't work from a file:// address. Hence no point in bundling documentation that you can read locally in the tarball.
Frankly, I find this completely ridiculous. It's also goes against Boost's documentation guidelines:
http://www.boost.org/development/requirements.html#Documentation
"The format for documentation should be HTML, and should not require an advanced browser or server-side extensions"
1. No server side extensions are needed. It's a static website. 2. Standard HTML5 is generated throughout. It (very slightly) fails validation currently due to using <center>, but I'll be upgrading to the latest release of the theme after the review which should fix that. It ticks all the mandatory boxes in the requirements you link to. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
AMDG On 01/23/2018 03:22 PM, Niall Douglas via Boost wrote:
The particular theme I chose requires a real web server, it doesn't work from a file:// address. Hence no point in bundling documentation that you can read locally in the tarball.
Frankly, I find this completely ridiculous. It's also goes against Boost's documentation guidelines:
http://www.boost.org/development/requirements.html#Documentation
"The format for documentation should be HTML, and should not require an advanced browser or server-side extensions"
1. No server side extensions are needed. It's a static website.
So, why exactly can't you generate static html?
2. Standard HTML5 is generated throughout. It (very slightly) fails validation currently due to using <center>, but I'll be upgrading to the latest release of the theme after the review which should fix that.
It ticks all the mandatory boxes in the requirements you link to.
In Christ, Steven Watanabe
1. No server side extensions are needed. It's a static website.
So, why exactly can't you generate static html?
Most web browsers don't render modern static websites with AJAX, accessibility, mobile rendering, semantic search etc from file:// for security reasons. The problem is in the web browser, not in the site. If you're super adverse to running Hugo, the Python 2 program below will serve a static website onto localhost for you. Taken from https://docs.python.org/2/library/simplehttpserver.html: ``` import SimpleHTTPServer import SocketServer PORT = 8000 Handler = SimpleHTTPServer.SimpleHTTPRequestHandler httpd = SocketServer.TCPServer(("", PORT), Handler) print "serving at port", PORT httpd.serve_forever() ``` Start using: python -m SimpleHTTPServer 8000 Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
Most web browsers don't render modern static websites with AJAX, accessibility, mobile rendering, semantic search etc from file:// for security reasons. The problem is in the web browser, not in the site.
You could generate the static html using an appropriate offline theme. If accepted you'll in any event need either static html in doc/html, or a doc/Jamfile that builds said doc/html. Might as well do that now to give the reviewers an offline doc.
On 23 January 2018 at 23:10, Peter Dimov via Boost
Niall Douglas wrote:
Most web browsers don't render modern static websites with AJAX, accessibility, mobile rendering, semantic search etc from file:// for security reasons. The problem is in the web browser, not in the site.
You could generate the static html using an appropriate offline theme. If accepted you'll in any event need either static html in doc/html, or a doc/Jamfile that builds said doc/html. Might as well do that now to give the reviewers an offline doc.
Using ugly urls and rewriting relative urls would be a good start: https://gohugo.io/content-management/urls/
Most web browsers don't render modern static websites with AJAX, accessibility, mobile rendering, semantic search etc from file:// for security reasons. The problem is in the web browser, not in the site.
You could generate the static html using an appropriate offline theme. If accepted you'll in any event need either static html in doc/html, or a doc/Jamfile that builds said doc/html. Might as well do that now to give the reviewers an offline doc.
Here is an offline archive of the docs website as pulled and converted by HTTrack: https://github.com/ned14/outcome/releases/download/v2.0-boost-peer-review/ou... I had a quick flick through and it seems mostly the same. Search doesn't work obviously. I would emphasise that the review submission is the public website at https://ned14.github.io/outcome/, and not this zip archive which hasn't received anything like the same amount of validation and checking. Regarding what else you said, yes one could simply run Hugo and tell it to make an offline copy. After all the true source content is written in Markdown, and actually any Markdown to HTML/PDF/DocBook processor will technically do. However the documentation took as long to do as everything else put together (writing, testing, everything else). Some four months of effort - and not just by me either - went into the docs alone. The public website https://ned14.github.io/outcome/ has many problems and issues, but at least I know what those are, and I am prepared for any review feedback on those. Quickly firing out an offline website, getting it properly validated and throwing it into a ZIP file is not doable in the time available to me before the review ends. I only get, at most, two free hours per day and those go on writing emails like this one. Regarding how these docs would end up in Boost if the library is accepted, historically Boost has preferred to generate the docs from their original sources rather than take a copy of the HTML dumped out. I would assume this would continue to be the case, and so we'd need some theme which outputs something with the Boost look and feel, and fix up the Boost servers with Hugo et al and get them into a Jamfile as you say. But those involve decisions by those who maintain the Boost website, and many more weeks of tweaking things in whatever direction is chosen ultimately. As I do not know what that decision would be until after this review, I have kicked that can down the road for now. For all I know, maybe they would prefer a static HTML dump, but I suspect not. You're right Peter that all this is easy, but it is also time consuming and detail orientated. And that means weeks of time must elapse as I am currently on contract. Also, nobody seems yet to have noticed the many problems in the Standardese generated output. There is a fair bit of rope to pull in on that yet too, Jonathan I am sure awaits feedback from this review with anticipation. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Tue, Jan 23, 2018 at 11:55 PM, Niall Douglas via Boost
1. No server side extensions are needed. It's a static website.
So, why exactly can't you generate static html?
Most web browsers don't render modern static websites with AJAX, accessibility, mobile rendering, semantic search etc from file:// for security reasons. The problem is in the web browser, not in the site.
If you're super adverse to running Hugo, the Python 2 program below will serve a static website onto localhost for you. Taken from https://docs.python.org/2/library/simplehttpserver.html:
``` import SimpleHTTPServer import SocketServer
PORT = 8000
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)
print "serving at port", PORT httpd.serve_forever() ```
Start using:
python -m SimpleHTTPServer 8000
The file above is not needed (that is an example of usage of the module) -- just the command line you wrote is enough: python -m SimpleHTTPServer 8000 Or for Python 3, even better, binding only to localhost: python -m http.server 8000 --bind 127.0.0.1 Not that I agree with something like the Boost docs requiring a server, but it is even easier to set up one :) My 2 cents, Miguel
Niall
-- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
On Thu, Jan 18, 2018 at 2:07 PM, charleyb123 . via Boost < boost@lists.boost.org> wrote:
Hi, Everyone,
The formal review of Niall Douglas' Outcome (v2) library begins Fri-19-Jan and continues until Sun-28-Jan, 2018.
From the Outcome documentation:
auto read_int_from_file(string_view path) noexcept -> outcome::result<int> { OUTCOME_TRY(handle, open_file(path)); // decltype(handle) == Handle OUTCOME_TRY(buffer, read_data(handle)); // decltype(buffer) == Buffer OUTCOME_TRY(val, parse(buffer)); // decltype(val) == int return val; } Question: if using the OUTCOME_TRY macro is equivalent to calling the function, checking for error and then returning an error if there is an error, how is this different from using exceptions? Semantically, exception handling does nothing more than check for errors and returning errors if there were errors, with much more readable syntax: return parse(read_data(open_file(path))); This kind of macro use seems rather inelegant and more appropriate for C programs, though it may be justified in C since it lacks exception handling. Emil
Question: if using the OUTCOME_TRY macro is equivalent to calling the function, checking for error and then returning an error if there is an error, how is this different from using exceptions? Semantically, exception handling does nothing more than check for errors and returning errors if there were errors, with much more readable syntax:
Semantically they are similar, and if the compiler implements EH using SJLJ or any of the non-table approaches, they are also pretty much identical in terms of implementation. But on table-based EH, the cost of handling failure is many orders of magnitude more expensive than handling success. OUTCOME_TRY emulates the SJLJ balance of success/failure, you pay a constant fixed overhead during successful codepaths in exchange for predictable overhead during unsuccessful codepaths. So OUTCOME_TRY opts back into non-table EH characteristics on table-based EH implementations. Hence the "Decision Matrix" at https://ned14.github.io/outcome/use-matrix/. There is no point in using Outcome if failure almost never occurs. Use exceptions instead. But if failure occurs in say 0.1% of the time, Outcome likely will win, possibly even in 0.01% of the time depending. See https://ned14.github.io/outcome/faq/#what-kind-of-performance-benefits-will-... Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Fri, Jan 26, 2018 at 7:57 PM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
Question: if using the OUTCOME_TRY macro is equivalent to calling the function, checking for error and then returning an error if there is an error, how is this different from using exceptions? Semantically, exception handling does nothing more than check for errors and returning errors if there were errors, with much more readable syntax:
Semantically they are similar, and if the compiler implements EH using SJLJ or any of the non-table approaches, they are also pretty much identical in terms of implementation.
I was only talking about semantics. Are you saying that, except for performance considerations, there is no reason to use OUTCOME_TRY(handle, open_file(path)); OUTCOME_TRY(buffer, read_data(handle)); OUTCOME_TRY(val, parse(buffer)); return val; instead of return parse(read_data(open_file(path))); Emil
I was only talking about semantics. Are you saying that, except for performance considerations, there is no reason to use
OUTCOME_TRY(handle, open_file(path)); OUTCOME_TRY(buffer, read_data(handle)); OUTCOME_TRY(val, parse(buffer)); return val;
instead of
return parse(read_data(open_file(path)));
It is rare that functions would consume an Outcome as a parameter. After
all Outcome is not Expected, and Default Actions get a throw site away
from the cause of the error.
But in the end, there is nothing stopping you using Outcome as if
Expected, and there is a `checked
On Sat, Jan 27, 2018 at 4:13 PM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
I was only talking about semantics. Are you saying that, except for performance considerations, there is no reason to use
OUTCOME_TRY(handle, open_file(path)); OUTCOME_TRY(buffer, read_data(handle)); OUTCOME_TRY(val, parse(buffer)); return val;
instead of
return parse(read_data(open_file(path)));
It is rare that functions would consume an Outcome as a parameter. After all Outcome is not Expected, and Default Actions get a throw site away from the cause of the error.
My question is really what is the use case for OUTCOME_TRY. I'm asking because the above use of OUTCOME_TRY is literally the first example given in the Outcome documentation, yet the code would be simpler, more readable, more robust and, it seems to me, semantically very similar if it used exceptions instead. So the question is what's the upside of using the cumbersome macro rather than relying on exception handling? Is it just the perceived performance improvement? Emil
So the question is what's the upside of using the cumbersome macro rather than relying on exception handling? Is it just the perceived performance improvement?
Outcome's default configuration is to do nothing when E is a UDT. If E matches the various documented traits, observing T when an E is present will throw various exceptions according to the rules you tell it. The TRY operation is therefore an alternative way of unwinding the stack than throwing exceptions, one which does not invoke EH table scans. Sure, it's more manual and cumbersome, but it also means that the failure handling path has the same latency as the success handling path. And yes, if your EH implementation in the compiler is not table based, then there is not much performance gain. Some on WG21 have mused about an attribute by which one could mark up a function, class or namespace to say "please use balanced EH here". It would not be an ABI break, and code with the different EH mechanisms could intermix freely. If Outcome is successful in terms of low latency userbase, I don't doubt that SG14 will propose just such an EH attribute in the future. Until then, this is a library based solution. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 28 January 2018 at 07:37, Niall Douglas via Boost
Sure, it's more manual and cumbersome, but it also means that the failure handling path has the same latency as the success handling path. And yes, if your EH implementation in the compiler is not table based, then there is not much performance gain.
I take away the following from this: 1. Use of outcome is more manual and cumbersome, i.e. more error-prone. 2. *In certain setups* the failure path will have the same latency as the success path. Failure (when exceptions would be thrown) will in general be the result of lack of resources (network-connection(s), memory, disk-space). The handling of lack of resources (as I see it) would normally just try to stop the app from doing more damage and to preserve a state (of data etc...), that is hopefully valid. Logic errors just need fixing in my view, not exception handled. I, the client, am now in the doldrums (message on screen: "Oops, something went wrong, call the IT dept. please!"). I don't see why this would need to be fast, it will be slow in any case (Sorry, Niall is on his lunch-break! Back in an hour.) and I (although I hate using them) agree with Emil D. that exceptions is the C++ builtin mechanism to do just that (to save the app from totally wrecking itself (its data)). Exchanging Outcome for exceptions adds another lib I need to master (docs took 4 months to write), adds complexity, adds ugly C-like macros (now we finally have constexpr and noexcept) and ICE-es compilers. The objective of the exercise is to avoid the "slow path". The cost of using exceptions (in the success path) is a contentious issue, to which there doesn't seem to be a consensus view. If the cost of the failure path is an issue, you're doing something wrong, I think. The fact that the current implementation ICE-es even the most current compilers, means that it's usefulness will take years to materialize. First for compilers to catch up and then the users, as people (and companies) generally are conservative and seem very reluctant to move forward. I don't understand why people prefer old bugs over new bugs, as newer bugs have a better chance of getting fixed, once reported. degski
2018-01-28 12:12 GMT-03:00 degski via Boost
1. Use of outcome is more manual and cumbersome, i.e. more error-prone.
What is complex about the following? outcome::result<int> convert(const std::string& str) noexcept { if (str.empty()) return ConversionErrc::EmptyString; if (!std::all_of(str.begin(), str.end(), ::isdigit)) return ConversionErrc::IllegalChar; if (str.length() > 9) return ConversionErrc::TooLong; return atoi(str.c_str()); } Or maybe about the following? outcome::result<void> print_half(const std::string& text) { if (outcome::result<int> r = convert(text)) // #1 { std::cout << (r.value() / 2) << std::endl; // #2 } else { if (r.error() == ConversionErrc::TooLong) // #3 { OUTCOME_TRY (i, BigInt::fromString(text)); // #4 std::cout << i.half() << std::endl; } else { return r.as_failure(); // #5 } } return outcome::success(); // #6 } Let's convert the first example to exceptions and see how much more readable your exceptions are: int convert(const std::string& str) { if (str.empty()) throw ConversionErrc::EmptyString; if (!std::all_of(str.begin(), str.end(), ::isdigit)) throw ConversionErrc::IllegalChar; if (str.length() > 9) throw ConversionErrc::TooLong; return atoi(str.c_str()); } It looks the same to me. Let's try to convert the second example: void print_half(const std::string& text) { try { int r = convert(text); std::cout << (r / 2) << std::endl; } catch (const ConversionErrc &e) { if (e == ConversionErrc::TooLong) { BigInt i = BigInt::fromString(text); std::cout << i.half() << std::endl; } else { throw; } } } Also looks the same. So... you give us an example where exceptions are specially less verbose (and I'm not challenging that your case is uncommon because it looks very common to me), but then you generalize stating that it'll be like this everywhere. What is that old ditto? You can write shitty code in any language? It sounds to apply here. Just because you can write shitty code with Boost.Outcome it doesn't mean that Boost.Outcome is shit. Failure (when exceptions would be thrown) will in general be the result of
lack of resources (network-connection(s), memory, disk-space). The handling of lack of resources (as I see it) would normally just try to stop the app from doing more damage and to preserve a state (of data etc...), that is hopefully valid. Logic errors just need fixing in my view, not exception handled.
Server side code is expected to handle lot of failures and none of them are "stop the world". Also, if you add significant cost to the failure case, you can risk easy DoS on your app. If the error is exceptional or not, it depends on context, not on the algorithm (e.g. connection failure on game client code and game server code and both of them being backed up by the same functions). Boost.Outcome let's you do just that by converting an error into an exception. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
On Sun, Jan 28, 2018 at 12:10 PM, Vinícius dos Santos Oliveira via Boost < boost@lists.boost.org> wrote:
2018-01-28 12:12 GMT-03:00 degski via Boost
: 1. Use of outcome is more manual and cumbersome, i.e. more error-prone.
What is complex about the following?
outcome::result<int> convert(const std::string& str) noexcept { if (str.empty()) return ConversionErrc::EmptyString;
if (!std::all_of(str.begin(), str.end(), ::isdigit)) return ConversionErrc::IllegalChar;
if (str.length() > 9) return ConversionErrc::TooLong;
return atoi(str.c_str()); }
Or maybe about the following?
outcome::result<void> print_half(const std::string& text) { if (outcome::result<int> r = convert(text)) // #1 { std::cout << (r.value() / 2) << std::endl; // #2 } else { if (r.error() == ConversionErrc::TooLong) // #3 { OUTCOME_TRY (i, BigInt::fromString(text)); // #4 std::cout << i.half() << std::endl; } else { return r.as_failure(); // #5 } } return outcome::success(); // #6 }
Let's convert the first example to exceptions and see how much more readable your exceptions are:
int convert(const std::string& str) { if (str.empty()) throw ConversionErrc::EmptyString;
if (!std::all_of(str.begin(), str.end(), ::isdigit)) throw ConversionErrc::IllegalChar;
if (str.length() > 9) throw ConversionErrc::TooLong;
return atoi(str.c_str()); }
It looks the same to me. Let's try to convert the second example:
void print_half(const std::string& text) { try { int r = convert(text); std::cout << (r / 2) << std::endl; } catch (const ConversionErrc &e) { if (e == ConversionErrc::TooLong) { BigInt i = BigInt::fromString(text); std::cout << i.half() << std::endl; } else { throw; } } }
Also looks the same. So... you give us an example where exceptions are specially less verbose
Exception-neutral contexts, that is, when you can't handle the error. Instead of: f1(); if( error ) return error; f2(); if( error ) return error; f3(); if( error ) return error; You can just say: f1(); f2(); f3(); With exceptions, the compiler writes the ifs for you. (and I'm not challenging that your case is uncommon
because it looks very common to me), but then you generalize stating that it'll be like this everywhere.
Yes, there are very few try...catch contexts in most programs. Note that this is not the case in garbage-collected languages. For example, in Java you have to use try...finally to close files, handles, free mutexes, etc but in C++ this stuff happens automatically. Emil
On Sun, Jan 28, 2018 at 5:37 AM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
So the question is what's the upside of using the cumbersome macro rather than relying on exception handling? Is it just the perceived performance improvement?
Outcome's default configuration is to do nothing when E is a UDT. If E matches the various documented traits, observing T when an E is present will throw various exceptions according to the rules you tell it. The TRY operation is therefore an alternative way of unwinding the stack than throwing exceptions, one which does not invoke EH table scans.
Sure, it's more manual and cumbersome, but it also means that the failure handling path has the same latency as the success handling path. And yes, if your EH implementation in the compiler is not table based, then there is not much performance gain.
Some on WG21 have mused about an attribute by which one could mark up a function, class or namespace to say "please use balanced EH here". It would not be an ABI break, and code with the different EH mechanisms could intermix freely. If Outcome is successful in terms of low latency userbase, I don't doubt that SG14 will propose just such an EH attribute in the future.
Until then, this is a library based solution.
The reason why I wanted to keep the discussion on semantics rather than implementation details is that fundamentally exception handling ABI matters only when crossing function call boundaries. When a function throws an exception through 10 levels of function calls which got inlined, the compiler has much more freedom to optimize exception handling, and in the case when we throw an exception and catch it without crossing function call boundaries (that is, all relevant functions are inlined), there is no need to "really" throw an exception. Also note that when performance matters, usually function calls are inlined, which means that the case when throwing exceptions may, in theory, cause performance problems, is also the case when the compilers, in theory, have the most freedom to optimize. Now, I'm not aware of any compiler doing this kind of optimizations today (someone correct me if I'm wrong!) but I'm also old enough to remember when inlining function calls was problematic. I've had discussions with C programmers who argued that it was a mistake to introduce the unreliable inlining in C++ because C already had perfectly reliable inlining mechanism: preprocessor macros. If we talk about real world performance benefits or lack thereof, I've had this conversation with other game programmers many times before. The claim is that there is no way they can afford the exception handling overhead, so they don't use exceptions. But there is zero evidence to support this claim. This is an axiomatic belief, just like the axiomatic belief that they can't afford to use shared_ptr. In reality, in both cases it is a matter of preference of one coding style over another. Which is fine, but then (circling back) OUTCOME_TRY is puzzling me, because semantically it is as if using exceptions (e.g. if(error) return error) except a lot less elegant. Emil
2018-01-27 21:49 GMT+01:00 Emil Dotchevski via Boost
On Fri, Jan 26, 2018 at 7:57 PM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
Question: if using the OUTCOME_TRY macro is equivalent to calling the function, checking for error and then returning an error if there is an error, how is this different from using exceptions? Semantically, exception handling does nothing more than check for errors and returning errors if there were errors, with much more readable syntax:
Semantically they are similar, and if the compiler implements EH using SJLJ or any of the non-table approaches, they are also pretty much identical in terms of implementation.
I was only talking about semantics. Are you saying that, except for performance considerations, there is no reason to use
OUTCOME_TRY(handle, open_file(path)); OUTCOME_TRY(buffer, read_data(handle)); OUTCOME_TRY(val, parse(buffer)); return val;
instead of
return parse(read_data(open_file(path)));
It is my understanding that one of the situations that would prompt you to use Outcome is when you want to make all failure execution paths explicit and you want the compiler to help you with verifying that. In that case the variant: ``` return parse(read_data(open_file(path))); ``` is inferior because failure execution paths are not explicit. On the other hand, while you want failure execution paths to be explicit, you often still want the code to be concise and not distract the reader too much from following the positive execution paths. Operator TRY gives you a reasonable compromise. Also, it is not everywhere that you would use operator TRY. In more complicated control flows you will prefer manual if-statements. Exceptions and operator TRY are good for the cases where one operation always requires the previous operation to succeed as a precondition. But sometimes the dependency between operations is more complicated than this. Regards, &rzej;
On Mon, Jan 29, 2018 at 2:55 AM, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
2018-01-27 21:49 GMT+01:00 Emil Dotchevski via Boost < boost@lists.boost.org> :
On Fri, Jan 26, 2018 at 7:57 PM, Niall Douglas via Boost < OUTCOME_TRY(handle, open_file(path)); OUTCOME_TRY(buffer, read_data(handle)); OUTCOME_TRY(val, parse(buffer)); return val;
instead of
return parse(read_data(open_file(path)));
It is my understanding that one of the situations that would prompt you to use Outcome is when you want to make all failure execution paths explicit and you want the compiler to help you with verifying that. In that case the variant:
``` return parse(read_data(open_file(path))); ```
is inferior because failure execution paths are not explicit.
They are about as implicit as when using OUTCOME_TRY, because it too removes the ifs from plain view. But that's a good thing, removing the ifs is very desirable because leaving them in makes the code longer, more difficult to read, more prone to errors. "Say, what happens if there is an error?" "We just return the error, sir!" "Oh okay, carry on then!" Perhaps you mean that you could easily forget that e.g. open_file may throw. That's true, it takes a while to switch into the mindset that anything may throw.
Also, it is not everywhere that you would use operator TRY. In more complicated control flows you will prefer manual if-statements.
Yes! This is one of the practical (as opposed to simply a matter of stylistic preference) use cases in my mind: when it would be annoying to deal with an exception from a particular function. Formally, this is the case when you need to be able to detect a failure, but there is no useful postcondition to be enforced. Another use case is when we are only storing, but not consuming the result immediately so we need any exception throwing to be postponed until that happens. In some APIs this can be very common. Emil
2018-01-27 0:31 GMT-03:00 Emil Dotchevski via Boost
Question: if using the OUTCOME_TRY macro is equivalent to calling the function, checking for error and then returning an error if there is an error, how is this different from using exceptions? Semantically, exception handling does nothing more than check for errors and returning errors if there were errors
There is a single control flow to analyse: the return of the function. You don't need a "parallel" control flow construct to check for error case. This simplification just adds up: - You can't forget to check the error case (it's part of the type system). - It's self-documenting. - There are no strange interactions between Outcome and the rest of the language (e.g. throwing destructors, transporting exception between threads, and so on...). - Add templates to the mix and you'll remember who is responsible for non-insignificant amount of rules and added code boilerplate. - ... OUTCOME_TRY is just convenience. It mirrors the Rust's try macro: https://doc.rust-lang.org/1.9.0/std/macro.try!.html Rust has sum types and pattern matching at language level from day 0 (day 0 is Rust 1.0). The rest of the features were thought (or revised) with pattern matching in mind before 1.0 release. It has conveniences that C++ will never have (Rust /lacks/ constructors that can throw, moves that can throw and moved objects that will call a destructor). Rust doesn't have exceptions and nobody misses them. On the level of what C++ could have, Rust has monadic operations. with much more readable syntax:
return parse(read_data(open_file(path)));
Check OUTCOME_TRYX Of course it'd be more verbose: return parse(OUTCOME_TRYX(read_data(OUTCOME_TRYX(open_file(path))))); To the point of being ridiculous. Here it'd be better to split it in one line per call. In Rust, try! macro is only 4 letters long. Also, it was "promoted" to an operator: https://m4rw3r.github.io/rust-questionmark-operator With monadic operations, we could turn the above code into something like: return open_file(path).and_then(read_data).and_then(parse); But this assumes all operations return the same error type (e.g. std::error_code). This should be true at least to the I/O functions (open_file and read_data). Maybe we could use something like std::common_type or another magic detection to relax the requirements (i.e. it must be the same error type) a bit (it'd diverge from other implementations, but we have this freedom). -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
On Sat, Jan 27, 2018 at 5:33 AM, Vinícius dos Santos Oliveira < vini.ipsmaker@gmail.com> wrote:
2018-01-27 0:31 GMT-03:00 Emil Dotchevski via Boost
:
Question: if using the OUTCOME_TRY macro is equivalent to calling the function, checking for error and then returning an error if there is an error, how is this different from using exceptions? Semantically, exception handling does nothing more than check for errors and returning errors if there were errors
There is a single control flow to analyse: the return of the function. You don't need a "parallel" control flow construct to check for error case.
Where is the parallel control flow in return parse(read_data(open_file()))?
- You can't forget to check the error case (it's part of the type system).
You can't forget to check for errors if you use exceptions, either.
Literally, if you use exceptions it is as if the compiler writes the ifs for you.
- It's self-documenting.
Only to the extent that you can see that a function may return an error.
With exceptions, except for noexcept functions, functions may "return" an error. Some would count the fact that with e.g. Outcome you can specify what kind of errors can be returned as an advantage, but that is similar to statically enforced exception specifications. Sutter explained why that is a bad idea back in 2007: https://herbsutter.com/2007/01/24/questions-about- exception-specifications/.
- There are no strange interactions between Outcome and the rest of the language (e.g. throwing destructors, transporting exception between threads, and so on...).
So, don't throw in destructors. Also, you can't use Outcome in
destructors, but that is fine -- it is a logic error to not be able to destroy an object. Though this reminds me: in C++, exceptions are the only way constructors may report an error, and this is very deliberate, integral part of RAII. This guarantees that you can't use an object that failed to initialize, which is the reason why member functions are free to assume, rather than check, that all invariants of the class have been established. Thus, exception handling is an integral part of the C++ object encapsulation model. Choose to not use exceptions and the result is that like in C, each function must check whether the object was initialized, and return some error code to indicate that condition. You're replacing a bullet-proof automatically enforced error checking system with a manual one, prone to errors; worse, we're talking about error handling code, which by its very nature is difficult to test. OUTCOME_TRY is just convenience. It mirrors the Rust's try macro:
There are many languages which lack C++ exception handling, it doesn't mean that their approach is better. It is common for programmers coming from a different background, forced by reality to have to use C++, to complain what a horrible language it is for lacking this or that feature. :)
With monadic operations, we could turn the above code into something like:
return open_file(path).and_then(read_data).and_then(parse);
But this assumes all operations return the same error type (e.g. std::error_code).
I'd much rather use return parse(read_data(open_file())) without having to assume anything. Emil
2018-01-28 1:02 GMT-03:00 Emil Dotchevski via Boost
Where is the parallel control flow in return parse(read_data(open_file()))?
The hidden throw. int a = 4; int x() { a = y(); return z(a); } Any C programmer will think this function is as clear as it can be. Add a C++ programmer in the game and he'll start to think about possible exception interactions and maintain a useless mind state in his head which solves no problems. Lots of languages do fine without exceptions. And Rust, which is very similar to C++ (only pay for what you use, systems programming language, RAII...) also does fine without exceptions. You do have less complication. Go write a container and then you don't know whether functions will throw. Now you have to maintain two lines of control flow in your head. It won't be fun at all. Some would count the fact that with e.g. Outcome you can specify what kind
of errors can be returned as an advantage, but that is similar to statically enforced exception specifications. Sutter explained why that is a bad idea back in 2007: https://herbsutter.com/2007/01 /24/questions-about- exception-specifications/ https://herbsutter.com/2007/01/24/questions-about-exception-specifications/ .
This post will teach what is the proper way to "catch everything" in response to "how can I be sure I'm catching all exceptions?". However, one of the points is to not introduce new error types ever. Why would a user who is calling parse_int handle errors other than "invalid data" and "overflow error"? What should the developer put in the catch-all statement? Sometimes, a catch-all statement won't make sense. The problem here — really — is a brittle architecture in large code bases to handle error cases (which should happen only occasionally and most likely won't be tested as much as the success case). Add a new type failure that isn't expected by the callers and you break everything. If the user doesn't have access to the sources of an updated library, he won't even be able to test if his calls need to be updated. Now, add this to the fact that exception specification is a bad-practice/not-really-used in C++ and you may agree that this architecture is brittle. If I refactor large code bases in Rust projects, I'll never face similar problems. Everything will be compile time errors without any special setup that only advanced users could pay. This post you link here will also explain why exception specifications isn't a good idea. The argument is twofold: - “no one really knows how to design them”. - They are broken. What does this have to do with Outcome? This is not the topic being discussed here. The conclusion to this question is irrelevant here (a.k.a. ignoratio elenchi). Though this reminds me: in C++, exceptions are the only way constructors
may report an error, and this is very deliberate, integral part of RAII. This guarantees that you can't use an object that failed to initialize, which is the reason why member functions are free to assume, rather than check, that all invariants of the class have been established.
C++ didn't have Expected or Outcome's result back then. This is why exceptions are the only way to report an error from destructors. But a constructor is just a function which will instantiate an object of the given class. The only "special" requirement for such function is ACL to access all class' members. You have this idiom in C++ (private constructors). Whether you choose to design your objects using exceptions or Boost.Outcome, your functions /can/ rely on invariants of the classes. So, nothing special about exceptions here except for legacy code. Conclusion: exceptions are only an integral part of RAII in C++. Rust does have RAII and it doesn't need exceptions. If the objects you are instantiating use Boost.Outcome instead exceptions, then there is no reason why you need exceptions to orchestrate the "object fully constructed" scenario. Moves will never throw. You can just construct the object parts (potentially returning early in case of failure) and them moving everything into a structure of the proper class. Maybe I'm simplifying too much. Tell me if that is the case. Thus, exception handling is an integral part of the C++ object
encapsulation model. Choose to not use exceptions and the result is that like in C, each function must check whether the object was initialized, and return some error code to indicate that condition. You're replacing a bullet-proof automatically enforced error checking system with a manual one, prone to errors; worse, we're talking about error handling code, which by its very nature is difficult to test.
Given your previous argument was attacked, you can no longer follow/derive this argument. Therefore, it is not this much manual system as you imply. There are many languages which lack C++ exception handling, it doesn't mean
that their approach is better. It is common for programmers coming from a different background, forced by reality to have to use C++, to complain what a horrible language it is for lacking this or that feature. :)
Okay. I'd much rather use return parse(read_data(open_file())) without having to
assume anything.
Write `return open_file(path).and_then(read_data).and_then(parse);` then. It'll compile and work. Much simpler than to design a container which needs to take consideration the hidden control flow of exceptions everywhere. How much effort did we lose into this already? There was one of the review comments here about Niall getting the swap exception specification wrong, for instance. Is it every day and every week? And errors popping without we noticing? What is the argument to have such brittle error system (exactly the thing which should be the most robust)? -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
2018-01-28 5:02 GMT+01:00 Emil Dotchevski via Boost
On Sat, Jan 27, 2018 at 5:33 AM, Vinícius dos Santos Oliveira < vini.ipsmaker@gmail.com> wrote:
2018-01-27 0:31 GMT-03:00 Emil Dotchevski via Boost < boost@lists.boost.org
:
Question: if using the OUTCOME_TRY macro is equivalent to calling the function, checking for error and then returning an error if there is an error, how is this different from using exceptions? Semantically, exception handling does nothing more than check for errors and returning errors if there were errors
There is a single control flow to analyse: the return of the function. You don't need a "parallel" control flow construct to check for error case.
Where is the parallel control flow in return parse(read_data(open_file()))?
- You can't forget to check the error case (it's part of the type system).
You can't forget to check for errors if you use exceptions, either.
Literally, if you use exceptions it is as if the compiler writes the ifs for you.
- It's self-documenting.
Only to the extent that you can see that a function may return an error.
With exceptions, except for noexcept functions, functions may "return" an error.
Some would count the fact that with e.g. Outcome you can specify what kind of errors can be returned as an advantage, but that is similar to statically enforced exception specifications. Sutter explained why that is a bad idea back in 2007: https://herbsutter.com/2007/ 01/24/questions-about- exception-specifications/.
- There are no strange interactions between Outcome and the rest of the language (e.g. throwing destructors, transporting exception
between
threads, and so on...).
So, don't throw in destructors. Also, you can't use Outcome in destructors, but that is fine -- it is a logic error to not be able to destroy an object.
Though this reminds me: in C++, exceptions are the only way constructors may report an error, and this is very deliberate, integral part of RAII. This guarantees that you can't use an object that failed to initialize, which is the reason why member functions are free to assume, rather than check, that all invariants of the class have been established.
Thus, exception handling is an integral part of the C++ object encapsulation model. Choose to not use exceptions and the result is that like in C, each function must check whether the object was initialized, and return some error code to indicate that condition. You're replacing a bullet-proof automatically enforced error checking system with a manual one, prone to errors; worse, we're talking about error handling code, which by its very nature is difficult to test.
There is a usage model that allows you to both report failures via result and have your types retain strong invariants. Once you have decided to use "explicit failure execution paths" approach in a library, you provide factory functions rather than constructors as the interface for creating new objects. This solves another long-standing problem of C++ constructors: that they do not have names and sometimes you do not know what they do: vector<char> v1 (20); // size or capacity? vector<char> v2 {size_t(20), 'c'}; // 20 elements or just 2? vector<T> v3 {x.begin(), x.end()} // 2 elements or x.size() elements? point<double> p {1.141, 0.535}; // polar or Cartesian co-ords? Regards, &rzej;
Hi folks, this is my review of Boost.Outcome v2. tl;dr The library should be *accepted* Am 18.01.2018 um 23:07 schrieb charleyb123 . via Boost:
- What is your evaluation of the design?
It is clean and matches my expectations. In particular, I like the defaults and the ease-of-use without annoying ceremony to construct instances of result<> or outcome<>. As far as I am concerned this matches my typical use-cases. In this regard I prefer Outcome over the WG21-proposed Expected. The same is true for the chosen design of the observers.
- What is your evaluation of the implementation?
I didn't dig too deep into the current source code, but afaics the sources are approachable and not too convoluted. Even I can understand what's going on there.
- What is your evaluation of the documentation?
Congratulations, I love it. There are some minor inconsistencies between code snippets and their textual explanation. Therefore I recommend another round of proof-reading by someone who is not involved in the development process, therefore witnessing those warts much easier not being spoilt by a tainted mind.
- What is your evaluation of the potential usefulness of the library?
Our team has use-cases for such kind of library just as laid out in the documentation. Therefore I'll happily pick Boost.Outcome for that. My gut feeling is that we are not alone in this regard.
- Did you try to use the library? With what compiler? Did you have any problems?
I cloned the library locally and integrated it into Boost 1.66.0 as described in the documentation. Using this augmented Boost 1.66.0, I ran all of my tests and the evaluation with Visual Studio 2017 Update 5 (i.e. toolset 19.12). As already pointed out, the compiler from toolset 19.12 will ice in such a scenario. Therefore I upgraded the toolset to version 19.13.26122 (the latest daily toolset version as of today) and configured my VS test solutions and the commandline environment to pick-up this toolset version. With this setup in place, I first ran the test-suite which passed without failures or compiler warnings at /W4 /std:c++latest /permissive-. Great! Then I compiled and ran the code snippets from the documentation subdirectory while studying the tutorial. Those are fine as well. Beyond that I plugged result<> into our own sources where we use a similar but much less sophisticated class for the same purpose. This turned out to work well, too.
- How much effort did you put into your evaluation? A glance? A quick reading? In-depth study?
I spent a couple of hours over the course of the last week on the review process.
- Are you knowledgeable about the problem domain?
Somewhat. We already use a similar class in our customer projects where it fits the needs.
- Do you think the library should be accepted as a Boost library?
*yes* without conditions. Ciao Dani
I cloned the library locally and integrated it into Boost 1.66.0 as described in the documentation. Using this augmented Boost 1.66.0, I ran all of my tests and the evaluation with Visual Studio 2017 Update 5 (i.e. toolset 19.12). As already pointed out, the compiler from toolset 19.12 will ice in such a scenario. Therefore I upgraded the toolset to version 19.13.26122 (the latest daily toolset version as of today) and configured my VS test solutions and the commandline environment to pick-up this toolset version.
With this setup in place, I first ran the test-suite which passed without failures or compiler warnings at /W4 /std:c++latest /permissive-. Great! Then I compiled and ran the code snippets from the documentation subdirectory while studying the tutorial. Those are fine as well. Beyond that I plugged result<> into our own sources where we use a similar but much less sophisticated class for the same purpose. This turned out to work well, too.
Ok, so trunk MSVC is fixed. Very useful to know, thanks!
- Do you think the library should be accepted as a Boost library?
*yes* without conditions.
Thank you for the review! Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
AMDG
I started my review, but I don't think I'm going to
get it finished, so I'm throwing in a partial review
as is:
HOME:
- "View this code in Github"
This is fine for online viewing, but for a packaged release
it's better to link the local source.
- "contains an error information"
"information" shouldn't have an indefinite article.
PREREQUISITES:
- "It is worth turning on C++ 17 if you can, there are..."
comma splice.
BUILD AND INSTALL:
This section refers to all kinds of things that
do not exist in the review version. Such as:
- CMake
- Single header version
- outcome/include/outcome.hpp
DECISION MATRIX:
- The font size in the decision matrix scales with
the page size, which may make it unreadable.
(I had to zoom to 300%, before I could read the
third flow chart) It's also completely unreadable
without javascript.
TUTORIAL:
RESULT
- "View this code in Github" This is fine for online viewing, but for a packaged release it's better to link the local source.
- "contains an error information" "information" shouldn't have an indefinite article.
PREREQUISITES:
- "It is worth turning on C++ 17 if you can, there are..." comma splice.
All fixed in develop branch.
BUILD AND INSTALL:
This section refers to all kinds of things that do not exist in the review version. Such as: - CMake - Single header version - outcome/include/outcome.hpp
The documentation is for the standalone edition. If the library is accepted, it will be adjusted for the Boost edition.
DECISION MATRIX:
- The font size in the decision matrix scales with the page size, which may make it unreadable. (I had to zoom to 300%, before I could read the third flow chart) It's also completely unreadable without javascript.
The decision matrix will likely be removed in the Boost edition, along with all the other Javascript based stuff.
TUTORIAL:
RESULT
: - OUTCOME_V2_NAMSPACE. This seems singularly pointless. How does accessing the namespace via a macro help with backwards compatiblity? It will break source compatibility exactly as much as if you just used a normal namespace. If you're concerned with binary compatibility, then the only thing that matters is the symbol mangling and there's no need to expose this ugliness to the user. Also, why is this attached to result, and not in its own section.
The reason is that Outcome is expected to be used extensively in ABI boundaries. So if DLL A has public APIs returning Outcome, and DLL B has public APIs returning Outcome, but DLL A uses a different version of Outcome to DLL B, we specifically want to force the compiler to utilise the ValueOrError concept interoperation mechanism rather than having incompatible Outcome implementations corrupt one another.
- If some type `X` is both convertible to `T` and `EC` parallel structure: 'convertible' should either be placed before 'both' or be duplicated on both sides of the 'and.'
- You say that it's ambiguous if the constructor argument is convertible to both T and EC. Is it still ambiguous if it is exactly T or EC? If so, that seems wrong. If not, you need to explain the behavior more clearly.
All implicit conversions disable if there is any relationship between T and EC whatsoever. Yes, that's far too harsh. But that was what the first review concluded was safe, and there was universal consensus on that decision (eventually, it took a very long discussion). I have fixed the tutorial wording to make this clear.
- Why do you need to use tagged constructors instead of just casting? (I suppose the fact that you felt the need to have these indicates to me that an exact match /is/ ambiguous.)
The previous review wanted to avoid the surprises that one can experience with constructing std::variant, hence the much stricter restrictions.
tutorial/result/inspecting:
- "Suppose we will be writing function..." function needs an article.
- "integral number" This is just a verbose way of saying "integer."
- "Type result<void>" -> "The type result<void>"
- "Class template `result<>` is declared with attribute [[nodiscard]] which means compiler" Add "the" before "class," "attribute," and "compiler."
- "In the implementation we will use function `convert`" "*the* function"
- "#2. Function `.value()` extracts the successfully returned `BigInt`." "*The* function". Also it's an int, not a BigInt.
All fixed in develop.
- I noticed that the starts of the list items after the #N. are not vertically aligned, which looks a little odd for #1, #2, #3, and #4, which are short enough to make it obvious. This appears to be because each item is treated as a normal paragragh which happens to begin with a number instead of a proper list item.
I'm personally fine with it.
- "It implies that the function call in the second argument" I presume that it takes a generic expression, not just a function call.
- "It is defined as" The antecedent of "It" is ambiguous. It refers to the function in the preceding sentence, but when I first read it I assumed that it referred to "OUTCOME_TRY"
Fixed in develop.
- I think the name OUTCOME_TRY is misleading because it has the exact opposite meaning of "try." try/catch stops stack unwinding, but OUTCOME_TRY is used to implement manual stack unwinding.
I think the ship has sailed on that. Swift, Rust etc all use try. The proposal before WG21 is try. I think it better we conform to what other languages are doing unless there is a good reason not to.
- I'm not sure what I think of as_failure. I'd actually prefer to just write return r;
I think you mean `return r.error();` as the T types are not compatible.
- "this will be covered later" A link would be nice.
Improved in develop branch.
- I can't see the point of having success<void> default construct T. It seems like dangerous implicit behavior, unless you make it so that success is variadic and constructs the result in place.
Can you explain why it would be dangerous?
Also, there is no type named success. It's called success_type.
Good point. The reason is that on C++ 17, we were using a success<T> type directly constructible via template deduction guides. But the C++ 17 standard is subtly broken here, I believe it gets fixed in C++ 20. So I killed off the C++ 17 implementation, we now use the C++ 14 implementation everywhere. I've logged the issue to https://github.com/ned14/outcome/issues/123
tutorial/result/try
- OUTCOME_TRYV I think it would be nicer if you used a single macro that has different behavior depending on whether it is passed 1 or 2 arguments.
It'll need some preprocessor metaprogramming, but sure. Logged to https://github.com/ned14/outcome/issues/124.
It
would be even better if you could also merge OUTCOME_TRYX in. The behavior of OUTCOME_TRYV is logically the same as that of OUTCOME_TRYX for a result<void>.
Alas expression TRY is not portable across compilers, so I'll be keeping that as its own macro to prevent portability problems.
tutorial/outcome:
tutorial/outcome/inspecting:
- "outcome<> has also function" -> "outcome<> also has a function"
Fixed in develop.
- How does outcome::value interact with custom error_code type? It needs to know what exception type to throw, no?
Covered in https://ned14.github.io/outcome/tutorial/default-actions/
tutorial/payload: - "...symbolising that into human readable text at the moment of conversion into human readable text" This sentence does not parse.
Fixed.
- "#4 Upon a namespace-localised `result` from library A being copy/moved into a namespace-localised `result` from C bindings..." I can't understand this at all. What is a "namespace-localised result" How does this relate to a custom payload? A plain error_code has everything you need to set errno.
Namespace localised results are covered slightly later in the same section starting from https://ned14.github.io/outcome/tutorial/payload/copy_file2/. Basically we define a local EC type to cause ADL for the extension points into our local namespace, and then locally bind `result` into our local namespace. Most codebases using Outcome which are of any size will namespace localise their result implementation in this fashion.
general:
- In code examples, _'s can sometimes disappear mysteriously. I haven't figured out the exact conditions, but resizing the window changes the result. Seems to be a result of overlapping with the line underneath.
Ah, great spot. I had been stumped. Lines overlapping is a very likely cause of that problem. Logged to https://github.com/ned14/outcome/issues/125
- Also, the lines are too long. They still wrap even when I put the browser in full screen. (Admittedly, the VM is only 1280x768)
Logged to https://github.com/ned14/outcome/issues/126 Thanks for the feedback! Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Here is my review see the answers below. ________________________________________ From: Boost [boost-bounces@lists.boost.org] on behalf of charleyb123 . via Boost [boost@lists.boost.org] Sent: 18 January 2018 22:07 To: boost@lists.boost.org Cc: charleyb123 . Subject: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018) Hi, Everyone, The formal review of Niall Douglas' Outcome (v2) library begins Fri-19-Jan and continues until Sun-28-Jan, 2018. YOUR REVIEW --------------------- Please post your comments and review to the boost mailing list (preferably), or privately to the Review Manager (to me ;-). Here are some questions you might want to answer in your review: - What is your evaluation of the design? I have only looked at a limited part of the design, outcome::outcome as it is relevant to the use case which is of interest to me. The part I have looked at seems fine to me. - What is your evaluation of the implementation? The implementation of the ideas appears to be consistent. - What is your evaluation of the documentation? The documentation made available is not consistent with the Boost implementation. e.g. macro names (BOOST_OUTCOME_TRY vs OUTCOME_TRY). I beleive other people have raised this. - What is your evaluation of the potential usefulness of the library? The library is very useful for the use case I have in mind, a Boost library which makes a small use of exceptions to cope with a small number of difficult cases. - Did you try to use the library? With what compiler? Did you have any problems? I have run some of the examples in src/snippets and adapted them into my own examples. I have run these with Clang 4.0 and libc++ with C++14 and Boost 1.66.0. Once I had located the header and namespace this has not been a problem. I am running on Ubuntu Linux. - How much effort did you put into your evaluation? A glance? A quick reading? In-depth study? Not as much as I would have liked, hence the fact I have not looked at everything. - Are you knowledgeable about the problem domain? I have worked on functional codes for a number of years and contributed to several recent reviews. And most importantly: - Do you think the library should be accepted as a Boost library? YES subject to sorting out the documentation, which other people have already mentioned. Best wishes John Fletcher
- Do you think the library should be accepted as a Boost library?
YES subject to sorting out the documentation, which other people have already mentioned.
Thanks for the review! Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Thu, Jan 18, 2018 at 2:07 PM, charleyb123 . via Boost
The formal review of Niall Douglas' Outcome (v2) library begins Fri-19-Jan and continues until Sun-28-Jan, 2018.
My sincerest apologies, I am aware that the review period has ended. However, given the absence of dissenting voices I find it necessary to enter my comments into the public record. I will keep this short, so I will state unequivocally: Outcome should be REJECTED My reasons are as follows: 1. The library is not available to C++11 users. I understand that technically this is not against "the rules" but my rationale is as follows: a. Outcome provides fundamental vocabulary types. Boost has traditionally been the library collection to provide up and coming C++ features. For example boost::variant is usable in C++11 while std::variant is not b. Nothing in the implementation of Outcome should require C++14. As an example, Peter wrote a remarkably good version of expected and result using only C++11 and his "better variant" class (which also only requires C++11). Unfortunately he never published this work. I would prefer it over this Outcome. c. As Beast is C++11 and relies heavily on error_code, I can't use it in Beast. 2. The source code is generated from another project with a different license. 3. The Boost policy gives library authors practically unlimited discretion to make any changes they want after their library has been accepted. I have seen endless public examples where the author quarrels with other developers. I am concerned that after acceptance, Outcome will generate significant conflict over the odd choices which the author will unquestionably make and implement (as evidence by the odd choices the author has made in the past). 4. This library is too complicated for what it does: policies are unnecessary. I recognize that some of these reasons may be considered outside the scope of the review process (for example, considering the qualities of the author in addition to the library). I think they are relevant. This review is late for a number of reasons. Among which: I was expecting a significant number of people to reject it. At the very least, the people who rejected the last submission. And I realize that my review will cause a significant amount of drama on its own, which I hoped to avoid, by the frank and somewhat confrontational nature of it. Regards
On 1/30/18 9:56 AM, Vinnie Falco via Boost wrote:
On Thu, Jan 18, 2018 at 2:07 PM, charleyb123 . via Boost
wrote:
2. The source code is generated from another project with a different license.
Hmmmm - could you elaborate on this please? I'm aware I could track it down on my own, but since you've already done this, it seems much easier just to ask you. Robert Ramey
On Tue, Jan 30, 2018 at 10:34 AM, Robert Ramey via Boost
2. The source code is generated from another project with a different license.
Hmmmm - could you elaborate on this please? I'm aware I could track it down on my own, but since you've already done this, it seems much easier just to ask you.
https://github.com/ned14/outcome/blob/master/Licence.txt Thanks
On 01/30/18 21:40, Vinnie Falco via Boost wrote:
On Tue, Jan 30, 2018 at 10:34 AM, Robert Ramey via Boost
wrote: 2. The source code is generated from another project with a different license.
Hmmmm - could you elaborate on this please? I'm aware I could track it down on my own, but since you've already done this, it seems much easier just to ask you.
I believe, the author is allowed to distribute his work under multiple licenses. We (Boost) can only require that the version of the library that is proposed for Boost is licensed under the BSL. In the few files that I checked there is the BSL license quoted in the header comment (although a few files were missing any license), so it looks like the Boost.Outcome library is licensed under the BSL. Note to Niall. Please, make sure that every file contains a license header, including the source code, tests and documentation. You might also want to consider using the recommended header[1] for applying the license. I believe, the presence of this or reasonably similar header is verified by the inspection tool that generates this[2] report. [1]: http://www.boost.org/users/license.html [2]: http://boost.cowic.de/rc/docs-inspect-develop.html
On Tue, Jan 30, 2018 at 2:23 PM, Andrey Semashev via Boost wrote:
On 01/30/18 21:40, Vinnie Falco via Boost wrote:
On Tue, Jan 30, 2018 at 10:34 AM, Robert Ramey via Boost wrote:
2. The source code is generated from another project with a different license.
Hmmmm - could you elaborate on this please? I'm aware I could track it down on my own, but since you've already done this, it seems much easier just to ask you.
I believe, the author is allowed to distribute his work under multiple licenses. We (Boost) can only require that the version of the library that is proposed for Boost is licensed under the BSL. In the few files that I checked there is the BSL license quoted in the header comment (although a few files were missing any license), so it looks like the Boost.Outcome library is licensed under the BSL.
If the Boost.Outcome repository sources are going to be automatically "generated" from standalone-Outcome via scripts (currently a Travis job?) and standalone-Outcome has all code dual licensed under Apache and BSL, what would that mean for contributions to the Boost.Outcome library? i.e. Would they have to be contributed to standalone-Outcome first, under dual license? Glen
On 01/30/18 22:39, Glen Fernandes via Boost wrote:
On Tue, Jan 30, 2018 at 2:23 PM, Andrey Semashev via Boost wrote:
On 01/30/18 21:40, Vinnie Falco via Boost wrote:
On Tue, Jan 30, 2018 at 10:34 AM, Robert Ramey via Boost wrote:
2. The source code is generated from another project with a different license.
Hmmmm - could you elaborate on this please? I'm aware I could track it down on my own, but since you've already done this, it seems much easier just to ask you.
I believe, the author is allowed to distribute his work under multiple licenses. We (Boost) can only require that the version of the library that is proposed for Boost is licensed under the BSL. In the few files that I checked there is the BSL license quoted in the header comment (although a few files were missing any license), so it looks like the Boost.Outcome library is licensed under the BSL.
If the Boost.Outcome repository sources are going to be automatically "generated" from standalone-Outcome via scripts (currently a Travis job?) and standalone-Outcome has all code dual licensed under Apache and BSL, what would that mean for contributions to the Boost.Outcome library? i.e. Would they have to be contributed to standalone-Outcome first, under dual license?
That depends on what the contributor accepts and what the author requires of them. For example, if I contribute code to Boost.Outcome, I'm implicitly allowing distribution under the BSL but not Apache 2.0. Niall is in his rights to ask me if I'm ok to also distribute under Apache 2.0. Or reject my contribution. If I contribute to the standalone Outcome then I implicitly accept both licenses. I agree that these things need to be documented somewhere. My main point though is that I see no problem if another version of the library (not the one in Boost) is distributed under a different license.
Note to Niall. Please, make sure that every file contains a license header, including the source code, tests and documentation. You might also want to consider using the recommended header[1] for applying the license. I believe, the presence of this or reasonably similar header is verified by the inspection tool that generates this[2] report.
I believe all the source code, apart from the compile-fail tests, is licensed. I had an issue open on the tracker to remind me before the review began. The documentation does not. Until I know if there is acceptance, and what form the documentation ought to take on boost-website, I cannot know the best way to prepend licence boilerplate. Rest assured it would be fixed. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 01/31/18 03:15, Niall Douglas via Boost wrote:
Note to Niall. Please, make sure that every file contains a license header, including the source code, tests and documentation. You might also want to consider using the recommended header[1] for applying the license. I believe, the presence of this or reasonably similar header is verified by the inspection tool that generates this[2] report.
I believe all the source code, apart from the compile-fail tests, is licensed. I had an issue open on the tracker to remind me before the review began.
This one isn't licensed: https://github.com/ned14/boost-outcome/blob/master/include/boost/outcome.hpp The compile-fail tests should be licensed as well. I didn't look in every file, so please double check.
2018-01-30 14:56 GMT-03:00 Vinnie Falco via Boost
2. The source code is generated from another project with a different license.
Good point. Niall should do something about that. But then, why "rejected" instead "conditional acceptance"? It should be fairly easy to fix it. 3. The Boost policy gives library authors practically unlimited
discretion to make any changes they want after their library has been accepted. I have seen endless public examples where the author quarrels with other developers. I am concerned that after acceptance, Outcome will generate significant conflict over the odd choices which the author will unquestionably make and implement (as evidence by the odd choices the author has made in the past).
I thought the official purpose of Boost review was to find design flaws and improve the library design. I do understand there is this status symbol around it, but let's not make it the official purpose with statements like the one you're raising. Let's at least pretend the only reason for the review is to find design flaws. “As society lost its ability to act, social life and culture trod the road to decadence.” 4. This library is too complicated for what it does: policies are
unnecessary.
I'm pretty confident the SG "give me UB everywhere" 14 folks will deeply disagree with you, but I think Niall himself could elaborate more on this point as I believe he is in constant contact with them. Maybe macros could be used instead templatized policies, but this would hardly deliver the hard ABI requirements the library tries to provide. I recognize that some of these reasons may be considered outside the
scope of the review process (for example, considering the qualities of the author in addition to the library). I think they are relevant.
Noted. This review is late for a number of reasons. Among which: I was
expecting a significant number of people to reject it. At the very least, the people who rejected the last submission.
Maybe because these people voted against mainly for technical reasons (the null state insanity for instance!). All of which have been resolved in v2: https://github.com/ned14/outcome#changes-since-v1 -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
2018-01-30 18:56 GMT+01:00 Vinnie Falco via Boost
On Thu, Jan 18, 2018 at 2:07 PM, charleyb123 . via Boost
wrote: The formal review of Niall Douglas' Outcome (v2) library begins Fri-19-Jan and continues until Sun-28-Jan, 2018.
My sincerest apologies, I am aware that the review period has ended. However, given the absence of dissenting voices I find it necessary to enter my comments into the public record.
I will keep this short, so I will state unequivocally:
Outcome should be REJECTED
My reasons are as follows:
1. The library is not available to C++11 users. I understand that technically this is not against "the rules" but my rationale is as follows:
a. Outcome provides fundamental vocabulary types. Boost has traditionally been the library collection to provide up and coming C++ features. For example boost::variant is usable in C++11 while std::variant is not
b. Nothing in the implementation of Outcome should require C++14. As an example, Peter wrote a remarkably good version of expected and result using only C++11 and his "better variant" class (which also only requires C++11). Unfortunately he never published this work. I would prefer it over this Outcome.
A valid point. What is the reason for Boost.Outcome requiring C++14? If it is the `constexpr` guarantees, could the `constexpr` be just dropped for non-C++14 compilers? Regards, &rzej;
-----Original Message----- From: Boost [mailto:boost-bounces@lists.boost.org] On Behalf Of Andrzej Krzemienski via Boost Sent: 30 January 2018 19:34 To: boost@lists.boost.org Cc: Andrzej Krzemienski Subject: Re: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018)
<snip>
What is the reason for Boost.Outcome requiring C++14? If it is the `constexpr` guarantees, could the `constexpr` be just dropped for non-C++14 compilers?
Of course, Boost has some macros that might help with this BOOST_CONSTEXPR_OR_CONST ... But I can't see any objection to requiring a C++14 compiler. I suspect Outcome will mainly or only be used for new projects, a good time to start using the most recent (and fewer bugged) compiler. Paul PS I haven't felt qualified to make a worthwhile review, but I would vote ACCEPT from reading the views of other reviewers. Many of the objections seem insubstantial.
On Wed, Jan 31, 2018 at 4:53 AM, Paul A. Bristow wrote:
PS I haven't felt qualified to make a worthwhile review, but I would vote ...
If a Boost review can be of the form "+1 to what that guy said" or "-1 becaues of what the other guy said", that would probably diminish the value of the process substantially. :)
Many of the objections seem insubstantial.
On the other hand, I personally don't see the reviews as unsubstantial. I recall that you voted to accept Outcome v1 as an unfinished library purely on faith in Niall: https://lists.boost.org/Archives/boost/2017/05/235095.php Paul A. Bristow wrote:
[What is your evaluation of the design?] Terrifying
[What is your evaluation of the implementation? ] Way above my pay grade. Complex for reasons I don't pretend understand.
[How much effort did you put into your evaluation?] Re-reading documentation and another quick skirmish.
[Are you knowledgeable about the problem domain?] No
[Do you think the library should be accepted as a Boost library?] In the end, we now have to decide if this is all going to be 'OK in the end' and have faith in Niall's skill, judgement, and determination to see this functional and finished. I do. So that's a yes.
I felt your review was legitimate. And I think that a reviewer who has rejected Outcome v2 because they feel the opposite (to what you do, about the author) is equally so. Glen
On Wed, Jan 31, 2018 at 1:53 AM, Paul A. Bristow via Boost < boost@lists.boost.org> wrote:
-----Original Message----- From: Boost [mailto:boost-bounces@lists.boost.org] On Behalf Of Andrzej Krzemienski via Boost Sent: 30 January 2018 19:34 To: boost@lists.boost.org Cc: Andrzej Krzemienski Subject: Re: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018)
<snip>
What is the reason for Boost.Outcome requiring C++14? If it is the `constexpr` guarantees, could the `constexpr` be just dropped for non-C++14 compilers?
Of course, Boost has some macros that might help with this
BOOST_CONSTEXPR_OR_CONST ...
constexpr is C++11.
But I can't see any objection to requiring a C++14 compiler. I suspect Outcome will mainly or only be used for new projects, a good time to start using the most recent (and fewer bugged) compiler.
You'd be surprised how difficult it is for a large corporation to switch compiler versions. I am not sure what's the right compiler for Outcome to require, but certainly requiring C++11 is better than C++14. Emil
1. The library is not available to C++11 users. I understand that technically this is not against "the rules" but my rationale is as follows:
Firstly, Boost admission requirements are merely that it conforms to the latest published ISO C++ standard. That's currently C++ 17. It's certainly not grounds for a rejection, unless you don't care about the Boost admission requirements.
a. Outcome provides fundamental vocabulary types. Boost has traditionally been the library collection to provide up and coming C++ features. For example boost::variant is usable in C++11 while std::variant is not
If anything, this is an argument in favour of it being written in C++ 17, even C++ 20.
b. Nothing in the implementation of Outcome should require C++14. As an example, Peter wrote a remarkably good version of expected and result using only C++11 and his "better variant" class (which also only requires C++11). Unfortunately he never published this work. I would prefer it over this Outcome.
I think you'd be surprised at the number of internal compiler errors in older compilers when using anything Outcome-like of STL implementation quality in big codebases, maybe Peter will confirm. Outcome v1 went out of its way to support old compilers right back to clang 3.1 and GCC 4.9, it had many workarounds to suppress ICEs. It also used preprocessor metaprogramming to avoid pushing too hard on compile-time selected CRTP, because that ICEs older compilers and it's also hard on compile time. The last peer review was really clear that the preprocessor metaprogramming had to go. I did warn at the time that you'd get an equivalent structure in templates with all the consequences thereof, but people still wanted the preprocessor work gone. So it's gone. But the price is the compiler requirements, which are now high, because compilers just weren't reliably up to it until recently. Most of the complaints I've received so far from various users is about the high compiler version. Nobody is too bothered about the C++ 14, it's rather that the C++ 14 compiler that they will be using next few years is not as new as GCC 6 or VS2017. That's a far bigger problem for end users.
c. As Beast is C++11 and relies heavily on error_code, I can't use it in Beast.
That is however your choice Vinnie. C++ 14 is a bug fix release of C++
11. I don't know why you'd not want to move to a standard with so many
bug fixes. Most of your user base has moved, or will soon, for that very
reason.
But maybe I can do something for you anyway. After failing to persuade
boost-dev to substantially modify Boost.System, over at SG14 we are
discussing a
2. The source code is generated from another project with a different license.
I own almost all the copyright. I can license my work under any licence I like, including multiple licences under the Geneva Copyright Convention which is an international law. This is a non issue.
3. The Boost policy gives library authors practically unlimited discretion to make any changes they want after their library has been accepted. I have seen endless public examples where the author quarrels with other developers. I am concerned that after acceptance, Outcome will generate significant conflict over the odd choices which the author will unquestionably make and implement (as evidence by the odd choices the author has made in the past).
a.k.a. you don't like me, so no Boost for you. Ok.
4. This library is too complicated for what it does: policies are unnecessary.
Corporate users are very enthusiastic about the policies because it lets
the leadership programmers impose consistent and totally bespoke rules
across the entire codebase upon all the other programmers. Until now,
macros were the main tool for this. It's a big gain for them.
Now, me personally, I've never actually used a custom policy yet in any
of my own code. But it's going to come in real handy with the proposed
SG14
I recognize that some of these reasons may be considered outside the scope of the review process (for example, considering the qualities of the author in addition to the library). I think they are relevant.
The only requirement regarding qualities of the author is "The author must be willing to participate in discussions on the mailing list, and to refine the library accordingly." I've spent four years bringing this library to Boost, and did a complete rewrite because Boost asked for it last year, including taking a big risk on test casing new tooling for documentation because so many people dislike doxygen as we saw last peer review. I've been involved with Boost since 2002, and I've been here on boost-dev since 2012. I've served Boost in many capacities over the years, including spending quite a bit of their money. Not to blow one's own trumpet, but there are only a few library authors still here with a similar record of longevity and service to Boost. Up to you how you feel my qualities are, but I suspect your opinion is emotionally not factually based.
This review is late for a number of reasons. Among which: I was expecting a significant number of people to reject it. At the very least, the people who rejected the last submission. And I realize that my review will cause a significant amount of drama on its own, which I hoped to avoid, by the frank and somewhat confrontational nature of it.
You're grinding your axe. I suppose you feel entitled to do so after I publicly chided you on what an opportunity you missed on how you ran the Beast review. You just wanted into Boost no matter how. So fair enough. But perhaps you should look instead at how Outcome was done: I took the (very) long way round on getting this library into Boost, and now Expected/Outcome is considered so boring and "done" by the C++ community that none of the reviews questioned any of the major design decisions. There's widespread buy-in to this design, and I look forward to seeing how WG21 reacts (not well, I am sure). Reviews instead almost entirely focused on nitty gritty details like commas and spelling and what HTML theme to put on the documentation. I consider that a big success, we as a C++ community came together and collaboratively decided on an ideal form for a C++ `Result<T>`. Cool. That's what you can get if you run a Boost review right. Thanks for the review Vinnie. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Tue, Jan 30, 2018 at 4:09 PM, Niall Douglas via Boost
Firstly, Boost admission requirements are merely that it conforms to the latest published ISO C++ standard. That's currently C++ 17.
I am familiar with the letter of the law.
It's certainly not grounds for a rejection, unless you don't care about the Boost admission requirements.
"Don't care" is emotionally charged language. And also false. I care very much, especially for cases where the Boost admission requirements and review process show a weakness.
a. Outcome provides fundamental vocabulary types. Boost has traditionally been the library collection to provide up and coming C++ features. For example boost::variant is usable in C++11 while std::variant is not
If anything, this is an argument in favour of it being written in C++ 17, even C++ 20.
I disagree. As we have seen in the recent survey, C++11 is overwhelmingly popular. Your advice to consider C++17 is exactly the type of bad decision-making that I am concerned about if Outcome is accepted into Boost. To wit, requiring C++17 would exclude 88% of potential Boost users. Even requiring C++14 excludes two-thirds of all C++ programmers. See: https://www.jetbrains.com/research/devecosystem-2017/cpp/
...But the price is the compiler requirements, which are now high, because compilers just weren't reliably up to it until recently.
Part of the skill of a Boost C++ engineer is to figure out how to make it work on older compilers, without being too taxing on them. We suffer so the users don't have to.
But maybe I can do something for you anyway. After failing to persuade
I think it too small a library to go into Boost alone, so I may just implement it into Outcome as an optional, and usable standalone to C++ 11, extension.
This is again the questionable decision-making that I am concerned about if Outcome is accepted into Boost. I consider adding a substitute for error_code to be outside the scope of the library. Such an addition should be in its own library and get its own review. You are effectively stating that you do not believe your idea has enough merit to survive a review, so you will simply add it to your already-published library. Again, this is within the letter of the law. But is not Best Practices.
I own almost all the copyright. I can license my work under any licence I like, including multiple licences under the Geneva Copyright Convention which is an international law. This is a non issue.
The problem comes when someone else wants to make a contribution. Do they have to agree to both licenses? Or just the one? If someone contributes to the BSL licensed portion in Boost, how will you bring it over to the "real" Outcome?
a.k.a. you don't like me, so no Boost for you. Ok.
This has nothing to do with whether I like you or not. For better or for worse, a Boost library comes with the author attached. How they conduct themselves prior to inclusion is a good indicator of future performance. As you stated yourself in the previous message, you may just add out of scope items to your library once it is accepted. This is precisely the type of decision-making I object to (not the person making it).
You're grinding your axe. I suppose you feel entitled to do so after I publicly chided you on what an opportunity you missed on how you ran the Beast review.
No, that's not correct. I don't feel entitled to anything. My largest objection is by far the incompatibility with C++11 (currently in use by two-thirds of all programmers). Some libraries can get away with requiring higher versions of the language, but this isn't one of them. An error variant is sorely needed by ALL C++ programmers not just the one-third with the luxury of C++14 or higher. Making Outcome work flawlessly in C++11 and on the popular compilers including the one I use would be a significant accomplishment and reflect very positively on the skill and professionalism of the author. Of course, the converse is also true...
But perhaps you should look instead at how Outcome was done: I took the (very) long way round on getting this library into Boost
Yes, and I think this reflects positively on you. I see a lot of effort and work that went into this thing, and you have invested a lot of time and energy responding to everyone on the list and to the users. This is to your credit. I think if Outcome was usable in Beast (C++11 and a reasonably modern version of Visual Studio) I would be leaning towards ACCEPT rather than REJECT. I appreciate your thoughtful reply. Regards
It's certainly not grounds for a rejection, unless you don't care about the Boost admission requirements.
"Don't care" is emotionally charged language. And also false. I care very much, especially for cases where the Boost admission requirements and review process show a weakness.
And yet here you are posting a review mostly made up of emotional concerns, not technical ones, not engineering ones. And not even factual ones - my first open source landed on the internet over 25 years ago, some of which is still actively supported by me. Nobody can claim that I haven't been at this for a very long time now, and have more than demonstrated my long term commitment.
I disagree. As we have seen in the recent survey, C++11 is overwhelmingly popular. Your advice to consider C++17 is exactly the type of bad decision-making that I am concerned about if Outcome is accepted into Boost. To wit, requiring C++17 would exclude 88% of potential Boost users. Even requiring C++14 excludes two-thirds of all C++ programmers. See:
This is such not an issue. At best, Outcome might land in the Summer release, more likely the Winter release. That means it doesn't reach end users until 2019, most of whom only update to a recent Boost every two years or so. So now we're talking 2021, a time by which C++ 11 users will be a minority.
...But the price is the compiler requirements, which are now high, because compilers just weren't reliably up to it until recently.
Part of the skill of a Boost C++ engineer is to figure out how to make it work on older compilers, without being too taxing on them. We suffer so the users don't have to.
Or, in fact the skill of a Boost C++ engineer is to see through fashionable concerns for what are actual, rather than commonly believed, engineering exigencies and design a truly correct library rather than one which ticks all the popular boxes and rouses the rabble. Anyone wanting to stick to C++ 11 for the next five years is unlikely to be a risk taker, and thus unlikely to want something like Outcome anyway. There is also a raft of end users who will be jumping straight from C++ 03 to C++ 17 because they like to track whatever is the last point bugfix before a major release (I know I do exactly this myself in everything from new computers to new cars, it saves such a huge amount of hassle, and you usually get great end of line discounts. But I digress). You've got some axe to grind on C++ 11 being far more important for potential users of Outcome than is actually the case. I don't know why. Even if it were a problem in 2018 for some kinds of end users who want Outcome, it definitely is much less of a problem in 2019. I've already had big corporates reaching out saying they are thinking of folding Outcome in with their toolchain upgrades in 2019, so the timing for them is perfect. Fundamental libraries like Outcome are always treated very conservatively because of the severe lock in and the fact your entire codebase will be ruined if the fundamental library ends up having a broken design or its ABI changes, breaking all your prebuilt DLLs. None of these issues are anything like as pressing with niche libraries such as Beast. So perhaps you don't get the conservatism at work here. Few big users will be adopting Outcome quickly, they'll take at least a year, maybe two. We're talking 2019, 2020 for most big users.
But maybe I can do something for you anyway. After failing to persuade
I think it too small a library to go into Boost alone, so I may just implement it into Outcome as an optional, and usable standalone to C++ 11, extension.
This is again the questionable decision-making that I am concerned about if Outcome is accepted into Boost. I consider adding a substitute for error_code to be outside the scope of the library. Such an addition should be in its own library and get its own review. You are effectively stating that you do not believe your idea has enough merit to survive a review, so you will simply add it to your already-published library.
I love how you read in whatever emotional interpretation you wish to twist in irrespective of facts. Firstly, said micro-library - and it's two classes, so it's a micro-library - is purely an experimental and optional extension. It won't be used by default, which will remain std::error_code. Secondly said micro-library is hardly being invented out of nothing. It addresses the consensus issues raised in https://wg21.link/P0824. Thirdly, who said it would not be reviewed here? You did, without evidence. It'll definitely pass before here for feedback before I start work on a proper implementation. Firstly I need to merge the substantial feedback from SG14, and get sign off from there.
I own almost all the copyright. I can license my work under any licence I like, including multiple licences under the Geneva Copyright Convention which is an international law. This is a non issue.
The problem comes when someone else wants to make a contribution. Do they have to agree to both licenses? Or just the one? If someone contributes to the BSL licensed portion in Boost, how will you bring it over to the "real" Outcome?
It almost sounds like you have no experience of managing open source libraries, yet you do. Multi licence codebases abound in software, and in open source. Most contributors are happy for their work to be licensed under whatever. Even I, a long time staunch opponent of the GPL, suck it down when sending in a patchset to a GPL codebase. If a contributor were not happy with the Apache 2.0 licence, then I'd rewrite their patchset. This almost never happens in practice. Apache 2.0 licence is highly uncontroversial.
is by far the incompatibility with C++11 (currently in use by two-thirds of all programmers). Some libraries can get away with requiring higher versions of the language, but this isn't one of them. An error variant is sorely needed by ALL C++ programmers not just the one-third with the luxury of C++14 or higher.
However, Outcome is not an error variant. The first Boost peer review clearly landed on a design which is not Expected. Perhaps that's the problem here in truth. You want a C++ 11 Expected. This library is not that. Hence you recommend rejection of this library, irrespective of usefulness, irrespective of engineering, irrespective of any other factor because you're not getting what you want, and I'm not doing what you want. What you should be saying is this: "This library does not suit my needs or the needs of many others. So I'm going to submit for review a library which does." I'd be more than happy to see someone write a high quality Expected implementation and submit it to Boost. It would complement Outcome nicely. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
This is my review of Outcome v2.
- What is your evaluation of the design?
The design appears to not have been finalized, the documentation states as
much:
"As patches and modifications are applied to this library, namespaces get
permuted in order not to break any backward compatibility. At some point
namespace outcome::v2 will be defined, and this will be the prefered
namespace. Until then OUTCOME_V2_NAMESPACE denotes the most recently
updated version, getting closer to outcome::v2."
It is wise to take the author seriously on his intentions to evolve, rather
than refine, the design further. During the first review, changes were
happening in response to feedback received on the mailing list. The current
design is even less concrete with the addition of policies. To me, this is
a sign of uncertainty in the design. A hallmark of excellence in library
design is that at some point, if a user says "it would be nice to be able
to do X with this library" the answer is "no". I think Outcome's design is
driven by a desire to always say "yes".
The policy-based design is especially problematic because of the expressed
desire to provide interoperability between diverse APIs each using
different error reporting. This is a bit counter-intuitive, because the
natural inclination is to provide maximum flexibility, but in this case
other considerations are more important. There is a reason why in C
libraries the default error reporting mechanism is to return int, even
though the language does permit programmers to return structs, which would
be more flexible.
Speaking of interoperability, it is especially tricky to report errors from
C-style callbacks. This would be nice to support because C++ exceptions are
off-limits in this case, yet it is sometimes desirable to communicate
user-specific information across the third-party C callback mechanism (this
is sometimes supported by a void * user data pointer, but that is not
always the case).
The kind of objects that can survive crossing API boundaries would be of
basic types, or have one or two members of basic types. Think
std::shared_ptr<T>, which _always_ consists of two pointers, rather than
SmartPtr
I'll be short and add my comments to ED's review... In general, I would like to state that if there is so much to say about something relatively simple, red flags are up. On 30 January 2018 at 15:07, Emil Dotchevski via Boost < boost@lists.boost.org> wrote:
It is wise to take the author seriously on his intentions to evolve...
This, I feel, is related to what's raised below: " It seems like Outcome wants to stay away from Boost."
There is a reason why in C libraries the default error reporting mechanism is to return int, even though the language does permit programmers to return structs, which would be more flexible.
KISS... Speaking of interoperability, it is especially tricky to report errors from
C-style callbacks. This would be nice to support because C++ exceptions are off-limits in this case, yet it is sometimes desirable to communicate user-specific information across the third-party C callback mechanism (this is sometimes supported by a void * user data pointer, but that is not always the case).
C is a dirty word...
Further, I disagree with the motivation to avoid using exceptions to begin with. The supplied decision matrix, I think, does not reflect reality. In my experience, the decision matrix to use C++ exceptions vs. something else is much simpler: "Do you hate exceptions?" No => use exceptions, Yes => use something else, because exceptions suck.
+1 Yes, exception handling has overhead. No, you can't afford this overhead in
every last corner of a complex program, but yes, you can afford it in general.
And it's builtin... And this is trivially true: if exception handling causes problems in some
subsystem, the solution is to hide that subsystem behind a C-style API and compile only that subsystem with exception handling disabled.
Or write some defensive code to make sure a potential problem doesn't need to become an exception... This is important, as it
is typical for programmers coming from other languages to be shocked by various C++ language features, and this should not be confused with problems in the C++ language specification.
I'm shocked, coming from C, that simple things have to become so complex... - What is your evaluation of the implementation?
Lack of C++11 support could be problematic. The use of macros to disambiguate namespace is cumbersome. Overall the library relies heavily on macros, which is not a good thing for a C++ library.
This seems so weird (and contradictory), we're using advanced C++, but still rely on C concepts (PP) to make it work, ugly...
It seems like Outcome wants to stay away from Boost.
To me it seems Outcome wants to be able to break with Boost at any moment, and that safety-net is already builtin... - Are you knowledgeable about the problem domain?
Yes.
I'm not, I'm just a simple guy...
- Do you think the library should be accepted as a Boost library?
The library should be rejected.
+1, a lot of mental m. degski
- What is your evaluation of the design?
The design appears to not have been finalized, the documentation states as much:
Well of course it isn't. I knew reviews here would demand many breaking changes. So users are warned of this. Remember you were looking at the Standalone documentation. Users there needed to be made aware of upcoming instability.
The policy-based design is especially problematic because of the expressed desire to provide interoperability between diverse APIs each using different error reporting. This is a bit counter-intuitive, because the natural inclination is to provide maximum flexibility, but in this case other considerations are more important. There is a reason why in C libraries the default error reporting mechanism is to return int, even though the language does permit programmers to return structs, which would be more flexible.
As the last section of the tutorial covers, there is a non-source-intrusive mechanism for externally specifying interoperation rules to handle libraries using one policy interoperating with libraries with different policies. This lets Eve stitch together the Alice and Bob libraries without having to modify their source code, and without affecting any other libraries. I personally think this Outcome's coup de grace and why it's the only scalable choice for large programs considering using this sort of error handling.
Speaking of interoperability, it is especially tricky to report errors from C-style callbacks. This would be nice to support because C++ exceptions are off-limits in this case, yet it is sometimes desirable to communicate user-specific information across the third-party C callback mechanism (this is sometimes supported by a void * user data pointer, but that is not always the case).
The kind of objects that can survive crossing API boundaries would be of basic types, or have one or two members of basic types. Think std::shared_ptr<T>, which _always_ consists of two pointers, rather than SmartPtr
which consist of who knows what. Note that this is not the same problem as "using result<T> from C code", which Outcome allows. The important question is not how do I use result<T> from C, but how do I transport result<T> across a third party context which knows nothing about Outcome (as a side note, C++ exceptions also cause issues when crossing API boundaries, even if we set exception safety requirements aside.)
How do you transport any type across a third party context which knows nothing about that type? There is a long list of standard techniques. Outcome is just another C++/C type, and any of those techniques apply just the same to it for that problem solution. I don't see the issue you're making here.
[snip why Emil loves exceptions] So, the design of "an error handling library that does not use C++ exceptions" has to target the tricky bits where exceptions would be annoying or can't be used, not the general case where they can.
I get Emil that you love exceptions. So you won't like the whole premise behind Outcome, indeed you wrote an alternative to Outcome v1 last year to prove the pointlessness of the Outcome premise. But lots of people don't agree with you, and they want something like this in C++.
Finally, I'll point out that a lot of the positive feedback comes from people who think that it is a good idea to replicate the Rust error reporting mechanism in C++. This seems to be an axiomatic belief, since I've never seen anyone attempt to substantiate it. This is important, as it is typical for programmers coming from other languages to be shocked by various C++ language features, and this should not be confused with problems in the C++ language specification.
If this was being forced on all users across the board, I can see you might have a point. But this is an opt in library, and nobody is claiming nor pretending that anything but a minority will ever want to use it. Using Outcome in your code does come with costs in terms of maintenance and learning curve. Most C++ users will never want nor need it.
- What is your evaluation of the implementation?
Lack of C++11 support could be problematic. The use of macros to disambiguate namespace is cumbersome. Overall the library relies heavily on macros, which is not a good thing for a C++ library.
I have no idea where you get the idea that the library relies heavily on macros. The only obligatory macro is BOOST_OUTCOME_V2_NAMESPACE. All the rest are optional. And as I've explained several times now, the permuting namespace is to force DLLs built against version X of Outcome to interoperate with other DLLs built against Y of Outcome via the ValueOrError Concept interface. I'd have thought Emil given your experience in games that you'd understand the importance of stable ABI guarantees. This makes stable ABI guarantees possible for the standalone Outcome. Now, any Boost.Outcome would eventually decay its macro into boost::outcome::v2 once a final v2 has arrived. That's not a v2 design, that's a v2 *implementation*. I'll be running the ABI Compliance Checker per commit once v2 is decided upon to ensure it remains stable. Again, this is what makes stable ABI guarantees possible. I'm amazed that you don't get this.
It seems like Outcome wants to stay away from Boost. By that I mean that great care has been taken to decouple it from Boost, and I sense that this desire comes from the target audience: the so-called "low latency" crowd wouldn't touch Boost with a 9 foot pole. While it is generally a bad idea to speculate about such things, it seems to me the motivation for submitting Outcome for a Boost review is not to benefit the Boost community; for us the coupling with Boost is not a problem.
The decoupling is purely for ease of maintenance. I can script patchsets from a Boost.Outcome into Outcome, and vice versa. I am sorry that you have read ulterior motives into what is an engineering choice.
- What is your evaluation of the documentation?
The documentation seems complete.
- Are you knowledgeable about the problem domain?
Yes.
- Do you think the library should be accepted as a Boost library?
The library should be rejected.
Thank you for your review. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Tue, Jan 30, 2018 at 4:40 PM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
The policy-based design is especially problematic because of the expressed desire to provide interoperability between diverse APIs each using different error reporting. This is a bit counter-intuitive, because the natural inclination is to provide maximum flexibility, but in this case other considerations are more important. There is a reason why in C libraries the default error reporting mechanism is to return int, even though the language does permit programmers to return structs, which would be more flexible.
As the last section of the tutorial covers, there is a non-source-intrusive mechanism for externally specifying interoperation rules to handle libraries using one policy interoperating with libraries with different policies. This lets Eve stitch together the Alice and Bob libraries without having to modify their source code, and without affecting any other libraries. I personally think this Outcome's coup de grace and why it's the only scalable choice for large programs considering using this sort of error handling.
This is wishful thinking. Imagine someone wanted to create a library that enabled all the different string types used in various libraries to get seamlessly stitched together. I'm sure that's possible, but I don't think it's a good idea. Ultimately, you pass char const * if you want to be compatible. Not ideal, not always possible, but that's how things are sometimes. It is the same with error handling: if you want to facilitate inteoperability, you'd return an int. Not ideal, but it's still the best option for interoperability. In addition, semantically policies don't make sense to me. For example, to throw or not to throw can not be a matter of "policy" because that makes throwing completely pointless.
[snip why Emil loves exceptions] So, the design of "an error handling library that does not use C++ exceptions" has to target the tricky bits where exceptions would be annoying or can't be used, not the general case where they can.
I get Emil that you love exceptions. So you won't like the whole premise behind Outcome, indeed you wrote an alternative to Outcome v1 last year to prove the pointlessness of the Outcome premise.
I wanted to be fair and refrained from mentioning (Boost) Noexcept but the reason why I wrote it was not to prove Outcome is pointless, but because I saw the point in Outcome, disagreed with the design, yet I had nothing tangible to prove even to myself that another approach is preferable (and there are problems where the best solution is not ideal.) It's not that I love exceptions, but that there is no other way to enforce postconditions and, like I already pointed out, postconditions are an integral part of the object encapsulation model in C++. Besides, there is *nothing* good about having to write: f1(); if( error ) return error; f2(); if( error ) return error; More importantly, there are *no* technical reasons why this should be slow when done automatically by the compiler. Literally, the people who insist on using OUTCOME_TRY instead of throwing exceptions have no reason to want that, except for lack of better optimizers (and even then, they have no evidence that the current state of affairs is "too slow"). I would very much like to be proven wrong about this, that there is *nothing* inherently slow in exception handling. It would save me a lot of energy. :) It is this part of Outcome, where it is trying to solve a problem much better solved by exception handling, that I find pointless, except in cases where exception handling is off limits, for example when errors need to pass through third-party code that is not exception-safe, in particular when errors need to be transported across C code. But Outcome does nothng to solve this problem. In addition, I don't find it pointless to postpone or avoid exceptions being thrown. Writing: optional<int> value; try { value = foo(); } catch( error & ) { } if( value ) use_value(value); is cumbersome. It is better to be able to say if( result<int> value=foo() ) use_value(value.get()); But this is not the "stitch together all the error handling object types in the universe" problem.
C-style callbacks. This would be nice to support because C++ exceptions are off-limits in this case, yet it is sometimes desirable to communicate user-specific information across the third-party C callback mechanism (this is sometimes supported by a void * user data pointer, but that is not always the case).
The kind of objects that can survive crossing API boundaries would be of basic types, or have one or two members of basic types. Think std::shared_ptr<T>, which _always_ consists of two pointers, rather than SmartPtr
which consist of who knows what. Note that Speaking of interoperability, it is especially tricky to report errors from this
is not the same problem as "using result<T> from C code", which Outcome allows. The important question is not how do I use result<T> from C, but how do I transport result<T> across a third party context which knows nothing about Outcome (as a side note, C++ exceptions also cause issues when crossing API boundaries, even if we set exception safety requirements aside.)
How do you transport any type across a third party context which knows nothing about that type?
There is a long list of standard techniques. Outcome is just another C++/C type, and any of those techniques apply just the same to it for that problem solution.
I don't see the issue you're making here.
Are you saying that you recommend returning shared_ptr
this in C++.
If you mean that many people hold unsubstantiated beliefs and want something, that's just life. :)
Finally, I'll point out that a lot of the positive feedback comes from people who think that it is a good idea to replicate the Rust error reporting mechanism in C++. This seems to be an axiomatic belief, since I've never seen anyone attempt to substantiate it. This is important, as it is typical for programmers coming from other languages to be shocked by various C++ language features, and this should not be confused with problems in the C++ language specification.
If this was being forced on all users across the board, I can see you might have a point.
But this is an opt in library, and nobody is claiming nor pretending that anything but a minority will ever want to use it. Using Outcome in your code does come with costs in terms of maintenance and learning curve. Most C++ users will never want nor need it.
Obviously, I'm not against people using Outcome, not that anyone would care what I think.
- What is your evaluation of the implementation?
Lack of C++11 support could be problematic. The use of macros to disambiguate namespace is cumbersome. Overall the library relies heavily on macros, which is not a good thing for a C++ library.
I have no idea where you get the idea that the library relies heavily on macros.
The only obligatory macro is BOOST_OUTCOME_V2_NAMESPACE. All the rest are optional.
And as I've explained several times now, the permuting namespace is to force DLLs built against version X of Outcome to interoperate with other DLLs built against Y of Outcome via the ValueOrError Concept interface.
I'd have thought Emil given your experience in games that you'd understand the importance of stable ABI guarantees. This makes stable ABI guarantees possible for the standalone Outcome.
How does that benefit Boost users? But you have a point that most macros are dealing with using Outcome from C, my bad.
Now, any Boost.Outcome would eventually decay its macro into boost::outcome::v2 once a final v2 has arrived. That's not a v2 design, that's a v2 *implementation*. I'll be running the ABI Compliance Checker per commit once v2 is decided upon to ensure it remains stable.
v2 of Boost Outcome or standalone Outcome?
It seems like Outcome wants to stay away from Boost. By that I mean that great care has been taken to decouple it from Boost, and I sense that this desire comes from the target audience: the so-called "low latency" crowd wouldn't touch Boost with a 9 foot pole. While it is generally a bad idea to speculate about such things, it seems to me the motivation for submitting Outcome for a Boost review is not to benefit the Boost community; for us the coupling with Boost is not a problem.
The decoupling is purely for ease of maintenance. I can script patchsets from a Boost.Outcome into Outcome, and vice versa. I am sorry that you have read ulterior motives into what is an engineering choice.
That's why I said it is a speculation. But this still leaves me wondering which one is the real Outcome and why are there two? Is the intention to deprecate standalone Outcome? Again, this is what makes stable ABI guarantees possible. I'm amazed
that you don't get this.
You're looking at the problem of ABI compatibility as "what do we have to do to get this complex type passed across a DLL boundary correctly." I'm looking at it as "how should we design this type so that ABIs don't break as much". Policies (much less user-defined policies) don't fit into this view. Emil
2018-01-30 22:07 GMT+01:00 Emil Dotchevski via Boost
This is my review of Outcome v2.
Further, I disagree with the motivation to avoid using exceptions to begin with. The supplied decision matrix, I think, does not reflect reality. In my experience, the decision matrix to use C++ exceptions vs. something else is much simpler: "Do you hate exceptions?" No => use exceptions, Yes => use something else, because exceptions suck.
Yes, exception handling has overhead. No, you can't afford this overhead in every last corner of a complex program, but yes, you can afford it in general. I have had many discussions on this and other "but but but Overhead" topics, and I have never been given actual evidence that any given program that does not use exceptions to report errors could not be written using exceptions to report errors, without sacrificing performance. And this is trivially true: if exception handling causes problems in some subsystem, the solution is to hide that subsystem behind a C-style API and compile only that subsystem with exception handling disabled.
So, the design of "an error handling library that does not use C++ exceptions" has to target the tricky bits where exceptions would be annoying or can't be used, not the general case where they can.
Finally, I'll point out that a lot of the positive feedback comes from people who think that it is a good idea to replicate the Rust error reporting mechanism in C++. This seems to be an axiomatic belief, since I've never seen anyone attempt to substantiate it. This is important, as it is typical for programmers coming from other languages to be shocked by various C++ language features, and this should not be confused with problems in the C++ language specification.
I feel I need to respond to this. My remarks are only to this single argument about the superiority of exceptions -- not to the review and other arguments. Because you say this in the context of Boost.Outcome review it sounds a bit as if you were saying "I am against Boost.Outcome, because exceptions should be preferred to any other failure handling mechanisms". I do not know if you are really saying this, but this is the impression I get. Please correct me if I am wrong. While the introduction to docs mentions "The high relative cost of throwing and catching a C++ exception", it also lists other use cases that are not related to performance. Are you also questioning other motivations for using this library? You yourself admit that there are small isolated places in the code where it makes sense to use an alternative to exceptions for failure reporting. And would you not accept the library that may be the best alternative to exceptions in those isolated cases? Exceptions play well in the most common situation where failure to execute some instruction `x` should prevent the execution of the subsequent instruction. If this is not the case, exceptions just add mess compared to manual control flows. This is not limited to C callbacks. I had similar problems inside a task framework. Boost.Outcome is not to "replace exception handling in your programs": it is used to cover those isolated places where exception handling proves inferior to manual control flows. That is, there might be people who will chose to use it everywhere, but it is their choice. They might have used `boost::variant` as well. Boost.Outcome does not come with recommendation, "use it instead of exceptions everywhere". And you are actually saying the same thing. Except that somewhere you appear to arrive at the conclusion that this library should be rejected because of being alternative to exceptions. And I cannot understand this. And I note that I am not familiar with Rust. My motivation for using this library is that I have observed places in my programs where exceptions just make the flow more complicated, and I needed to use something else so that the code reflects my intentions more directly, and Boost.Outcome addresses those issues. Regards, &rzej;
On Wed, Jan 31, 2018 at 3:53 AM, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
2018-01-30 22:07 GMT+01:00 Emil Dotchevski via Boost < boost@lists.boost.org> :
This is my review of Outcome v2.
Further, I disagree with the motivation to avoid using exceptions to begin with. The supplied decision matrix, I think, does not reflect reality. In my experience, the decision matrix to use C++ exceptions vs. something else is much simpler: "Do you hate exceptions?" No => use exceptions, Yes => use something else, because exceptions suck.
Yes, exception handling has overhead. No, you can't afford this overhead in every last corner of a complex program, but yes, you can afford it in general. I have had many discussions on this and other "but but but Overhead" topics, and I have never been given actual evidence that any given program that does not use exceptions to report errors could not be written using exceptions to report errors, without sacrificing performance. And this is trivially true: if exception handling causes problems in some subsystem, the solution is to hide that subsystem behind a C-style API and compile only that subsystem with exception handling disabled.
So, the design of "an error handling library that does not use C++ exceptions" has to target the tricky bits where exceptions would be annoying or can't be used, not the general case where they can.
Finally, I'll point out that a lot of the positive feedback comes from people who think that it is a good idea to replicate the Rust error reporting mechanism in C++. This seems to be an axiomatic belief, since I've never seen anyone attempt to substantiate it. This is important, as it is typical for programmers coming from other languages to be shocked by various C++ language features, and this should not be confused with problems in the C++ language specification.
I feel I need to respond to this. My remarks are only to this single argument about the superiority of exceptions -- not to the review and other arguments.
Because you say this in the context of Boost.Outcome review it sounds a bit as if you were saying "I am against Boost.Outcome, because exceptions should be preferred to any other failure handling mechanisms". I do not know if you are really saying this, but this is the impression I get. Please correct me if I am wrong.
No, I am not against Outcome, I'm against Outcome being part of Boost.
While the introduction to docs mentions "The high relative cost of throwing and catching a C++ exception", it also lists other use cases that are not related to performance. Are you also questioning other motivations for using this library?
Let's address the rest of the enumerated use cases one at a time: - Making some or all control paths explicitly detailed to aid code correctness auditing, as opposed to having hidden control paths caused by exceptions potentially thrown from any place. This is one of these axiomatic beliefs that need to be substantiated instead. It is simply not true that code that uses exception handling ("hidden control paths") is more difficult to audit or more prone to errors. In my experience it's the other way around: people who don't use exceptions are not serious about error handling; they're the ones who also need advanced logging libraries to help them figure out what went wrong _this_ time around. - Company policy to compile with exceptions disabled. - Maintaining a code base that was never designed with exception-safety in mind. - Parts of the programs/frameworks that themselves implement exception handling and cannot afford to use exceptions, like propagating failure reports across threads, tasks, fibers… These are valid use cases, however this is not the main focus of Outcome. A library designed for maintetance of legacy code and interoperability can't assume that you can always change existing functions to return a result<T>, much less the complex policy-based types in Outcome. Such a library should be able to propagate errors through ucooperative layers of a complex code base. In my biased opinion, (Boost) Noexcept is a much better tool for this use case; see this example which propagates errors from C++ through the Lua interpreter (which obviously doesn't know about Noexcept) back into C++: https://zajo.github.io/boost-noexcept/#example_lua. Another important feature that facilitates interoperability is the ability to forward to the caller arbitrary errors that originate in lower level libraries, the way throw without argument does in case of exceptions. This is contrary to Outcome's insistance of specifying the error type for result<T>. This design decision leads to the following class of problems: what do you do if you've said you can only return errors of type E1, but a low level library or a callback function returns an error of type E2? The only solution is to translate. Incidentally, check out how well this solution works with exception specifications. :)
Exceptions play well in the most common situation where failure to execute some instruction `x` should prevent the execution of the subsequent instruction.
Yes, enforcing postconditions.
If this is not the case, exceptions just add mess compared to manual control flows. This is not limited to C callbacks. I had similar problems inside a task framework.
Agreed, but Outcome is not a good solution to this problem because it
doesn't erase the type of the stored error object. You couldn't store
outcome::result
And you are actually saying the same thing. Except that somewhere you appear to arrive at the conclusion that this library should be rejected because of being alternative to exceptions. And I cannot understand this.
My objection is that the main focus of Outcome is to solve a problem that does not exist. It's like using modern tech to create a great typewriter, with the added benefit of a free eraser pencil in the form of OUTCOME_TRY. :)
And I note that I am not familiar with Rust. My motivation for using this library is that I have observed places in my programs where exceptions just make the flow more complicated, and I needed to use something else so that the code reflects my intentions more directly, and Boost.Outcome addresses those issues.
I'm not against you using Outcome. Emil
2018-01-31 18:21 GMT-03:00 Emil Dotchevski via Boost
- Making some or all control paths explicitly detailed to aid code correctness auditing, as opposed to having hidden control paths caused by exceptions potentially thrown from any place.
This is one of these axiomatic beliefs that need to be substantiated instead. It is simply not true that code that uses exception handling ("hidden control paths") is more difficult to audit or more prone to errors. In my experience it's the other way around: people who don't use exceptions are not serious about error handling; they're the ones who also need advanced logging libraries to help them figure out what went wrong _this_ time around.
It's like arguing that RAII doesn't help with resource freeing and this is just axiomatic belief. How pushing a "I forgot to do proper error handling" to compile time errors becomes "this is just axiomatic belief and helps no-one"? During this same review, Andrzej Krzemienski noted an improper exceptions handling of exceptions in the swap function: Also, it is possible that while reswapping, another exception will be
thrown. In general, you cannot guarantee the roll-back, so maybe it would be cleaner for everyone if you just declared that upon throw from swap, one cannot rely on the state of `result`: it should be reset or destroyed.
Should we remember the variant saga maybe[1][2]? These are not isolated cases. They keep happening. How do you see that? The people who made these mistakes or took years to solve some of them are too dumb and can't use exceptions? What about every beginner C++ programmer? You'll push all these cases to compile-time errors. Even expert C++ programmers will make mistakes one time or another. Meanwhile, I haven't seen any error remotely similar to these ones in Rust. Is this information incorrect? If this information is true, how is this axiomatic belief? I see a few concrete cases, one of them happened right in these last days on the Boost mailing list review. All hidden paths and a mistake was made. No amount of testing in Boost.Outcome has detected them. But then it is you who is arguing: It is simply not true that code that uses exception handling ("hidden
control paths") is more difficult to audit
Can you show me _any_ code using Result-like error handling that had problems that made people as smart as the C++ committee take so much time just to design a variant? Because any Rust programmer will design a variant like this: enum Either { Left(A), Right(B) } What happened here? I designed a variant in less than 10 seconds as robust as the C++ variant which required effort by many people and wasn't designed nearly as fast as this one. Could you go into these concrete cases and tell me what I'm not seeing? The only mistake you can do is forget to check if a value is contained before accessing such value. I'll find this easier to audit anytime. And the operator TRY will make it even easier. In Rust, it is not even possible to make this mistake. Most of the possibly dangerous behaviour became compile time errors. The rest may be subjective, but we have plenty of clues that exception handling is not that great to say the least. My axiomatic belief here: compile time errors are easier to manage. [1] http://davidsankel.com/c/a-variant-for-the-everyday-joe/ [2] https://isocpp.org/blog/2015/11/the-variant-saga-a-happy-ending -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
AMDG On 01/31/2018 03:26 PM, Vinícius dos Santos Oliveira via Boost wrote:
<snip> Can you show me _any_ code using Result-like error handling that had problems that made people as smart as the C++ committee take so much time just to design a variant? Because any Rust programmer will design a variant like this:
enum Either { Left(A), Right(B) }
What happened here? I designed a variant in less than 10 seconds as robust as the C++ variant which required effort by many people and wasn't designed nearly as fast as this one.
You didn't design anything. It's built in to the language and has addition restrictions which outright forbid the cases that make variant hard in C++. In Christ, Steven Watanabe
2018-01-31 20:13 GMT-03:00 Steven Watanabe via Boost
You didn't design anything. It's built in to the language and has addition restrictions which outright forbid the cases that make variant hard in C++.
Agreed. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
On Wed, Jan 31, 2018 at 2:26 PM, Vinícius dos Santos Oliveira < vini.ipsmaker@gmail.com> wrote:
2018-01-31 18:21 GMT-03:00 Emil Dotchevski via Boost < boost@lists.boost.org>:
- Making some or all control paths explicitly detailed to aid code correctness auditing, as opposed to having hidden control paths caused by exceptions potentially thrown from any place.
This is one of these axiomatic beliefs that need to be substantiated instead. It is simply not true that code that uses exception handling ("hidden control paths") is more difficult to audit or more prone to errors. In my experience it's the other way around: people who don't use exceptions are not serious about error handling; they're the ones who also need advanced logging libraries to help them figure out what went wrong _this_ time around.
It's like arguing that RAII doesn't help with resource freeing and this is just axiomatic belief.
How pushing a "I forgot to do proper error handling" to compile time errors becomes "this is just axiomatic belief and helps no-one"?
Where is the compile time error? Example?
During this same review, Andrzej Krzemienski noted an improper exceptions handling of exceptions in the swap function:
Also, it is possible that while reswapping, another exception will be
thrown. In general, you cannot guarantee the roll-back, so maybe it would be cleaner for everyone if you just declared that upon throw from swap, one cannot rely on the state of `result`: it should be reset or destroyed.
Should we remember the variant saga maybe[1][2]? These are not isolated cases. They keep happening.
How do you see that? The people who made these mistakes or took years to solve some of them are too dumb and can't use exceptions? What about every beginner C++ programmer? You'll push all these cases to compile-time errors. Even expert C++ programmers will make mistakes one time or another. Meanwhile, I haven't seen any error remotely similar to these ones in Rust.
Well, go ahead and code in Rust then. :) You can't draw conclusions only by looking at anectodal evidence like this. If you want to demonstrate that Rust-style error handling is more robust, you have to have a control, the same real-world large scale project written/designed for using C++ exception handling. Neither of us has a control. So you have to be able to make your point in the abstract, which is very difficult. It is especially difficult because using either approach, a high quality development team can produce a robust program. But just because Google managed to build a phone using Java, it doesn't mean it was a good choice. Yes, using exception handling you can make mistakes that would be less likely to make otherwise. Also, using C++ you can make a whole lot of mistakes you can't make in Java, that's one reason why some people use Java and not C++, but this doesn't make them good C++ programmers -- nor it means that there is something wrong with C++.
But then it is you who is arguing:
It is simply not true that code that uses exception handling ("hidden
control paths") is more difficult to audit
Can you show me _any_ code using Result-like error handling that had problems that made people as smart as the C++ committee take so much time just to design a variant? Because any Rust programmer will design a variant like this:
enum Either { Left(A), Right(B) }
What happened here? I designed a variant in less than 10 seconds as robust as the C++ variant which required effort by many people and wasn't designed nearly as fast as this one. Could you go into these concrete cases and tell me what I'm not seeing?
There are very many examples of things being very difficult to express in C++ while being trivial in another language. There are usually good reasons for this, and there are many examples to the contrary. The ability to use RAII and exceptions to enforce postconditions is one such advantage.
The only mistake you can do is forget to check if a value is contained before accessing such value. I'll find this easier to audit anytime. And the operator TRY will make it even easier. In Rust, it is not even possible to make this mistake.
Most of the possibly dangerous behaviour became compile time errors. The rest may be subjective, but we have plenty of clues that exception handling is not that great to say the least.
This attitude is why I think Outcome does not belong to Boost or to C++. Its focus is not to help deal with exceptions in the few cases where they're annoying or inappropriate, or to help the poor souls who have to maintain large legacy code bases; it's part of an effort to change (in my opinion break) C++, by people who really don't seem to like C++ anyway. I'll point out again that in C++ it is impossible to report an error from a constructor except by throwing, and this is a good thing. I assume you disagree -- so what do you propose instead? It can't be OUTCOME_TRY, can it? Emil
2018-01-31 20:25 GMT-03:00 Emil Dotchevski
Where is the compile time error? Example?
Check the first Outcome tutorials. This won't compile: std::cout << (convert(text) / 2) << std::endl; This will: OUTCOME_TRY(foo, convert(text)); std::cout << (foo / 2) << std::endl; You can't forget to handle the error case. You can look at a function and you'll immediately know if this function can fail or not. You can't draw conclusions only by looking at anectodal evidence like this.
If you want to demonstrate that Rust-style error handling is more robust, you have to have a control, the same real-world large scale project written/designed for using C++ exception handling. Neither of us has a control.
At least you dropped the "axiomatic belief". But I haven't relied on anecdotal evidence. I specifically told you to show me /any/ code on Rust that is as complex as the examples I gave. C++ allows complexity to just snowball and it is not a opt-in feature. It's always there. Therefore, you always maintain useless state in your head when analysing C++ code. Go ask C programmers what they think about C++ exceptions. So you have to be able to make your point in the abstract, which is very
difficult. It is especially difficult because using either approach, a high quality development team can produce a robust program. But just because Google managed to build a phone using Java, it doesn't mean it was a good choice.
You still ignore the fact that I'm talking about a qualitative (not quantitative) property: more errors go to compile time errors. I have been focusing on such aspect since the beginning of the discussion. It's fact that you can't ignore the error because the code will fail to compile. Error cases are explicit. Yes, using exception handling you can make mistakes that would be less
likely to make otherwise. Also, using C++ you can make a whole lot of mistakes you can't make in Java, that's one reason why some people use Java and not C++, but this doesn't make them good C++ programmers -- nor it means that there is something wrong with C++.
Agreed. There are very many examples of things being very difficult to express in
C++ while being trivial in another language. There are usually good reasons for this, and there are many examples to the contrary. The ability to use RAII and exceptions to enforce postconditions is one such advantage.
Not sure what kind of postcondition you're talking about this time. If it is class invariants, you can have them without exceptions. And just one note about RAII: Rust does have RAII. No GC. A lot about the language was inspired by C++ knowledge. I'll point out again that in C++ it is impossible to report an error from a
constructor except by throwing, and this is a good thing. I assume you disagree -- so what do you propose instead? It can't be OUTCOME_TRY, can it?
I'm not proposing you to code differently. I'm discussing exceptions vs Result. Once I finish this discussion, you'll be surprised by how much opinion we (me and you) have in common. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
On Wed, Jan 31, 2018 at 4:52 PM, Vinícius dos Santos Oliveira < vini.ipsmaker@gmail.com> wrote:
2018-01-31 20:25 GMT-03:00 Emil Dotchevski
: Where is the compile time error? Example?
Check the first Outcome tutorials.
This won't compile:
std::cout << (convert(text) / 2) << std::endl;
This will:
OUTCOME_TRY(foo, convert(text)); std::cout << (foo / 2) << std::endl;
Does this guarantee that your program is dealing with the error correctly? Not at all, if the context within which you call convert is not exception-safe, you'll get the same leak you'd get had you used exceptions. The difference is that you'd also get a compile error if you forget to check for errors, which a C++ programmer can't forget to do in this case. This is similar to how you have to deal with object initialization if you don't use exceptions. Compare: class foo { bool init_called_; public: foo(): init_called_(false) { } result<void> init() { .... init_called_=true; } result<int> foo() { assert(init_called_); return compute_int(); } result<void> bar( int x ) { assert(init_called_); //use x } }; .... foo a; OUTCOME_TRY(a.init()); OUTCOME_TRY(x,a.foo()); OUTCOME_TRY(a.bar(x)); Now the C++ version: class foo { foo() { //initialize, throw on errors } int foo() { return compute_int(); } void bar( int x ) { //use x } } .... foo a; a.bar(a.foo()); Note, I am not only making the point that the Outcome version is more prone to errors and more verbose, I am saying that even if you make no logic errors writing it, the result is a program that is _semantically_ identical to the one a C++ programmer would write using exceptions, complete with the bugs that could creep in if your code is not exception-safe. Literally, there is no upside to that style of programming.
You can't forget to handle the error case. You can look at a function and you'll immediately know if this function can fail or not.
In C++, we use noexcept to mark functions that can not fail.
You can't draw conclusions only by looking at anectodal evidence like
this. If you want to demonstrate that Rust-style error handling is more robust, you have to have a control, the same real-world large scale project written/designed for using C++ exception handling. Neither of us has a control.
At least you dropped the "axiomatic belief". But I haven't relied on anecdotal evidence. I specifically told you to show me /any/ code on Rust that is as complex as the examples I gave.
Yes, I understand that you like Rust and dislike C++. :) I am well aware of the difficulties in writing C++ code. Like I said, with some exceptions, there is usually a good reason for that.
C++ allows complexity to just snowball and it is not a opt-in feature. It's always there. Therefore, you always maintain useless state in your head when analysing C++ code. Go ask C programmers what they think about C++ exceptions.
You're making my point, that C++ exception handling is not an opt-in feature, it is inseparable part of the language, central to its object encapsulation model, etc. etc. I know what C programmers think about C++ and about exceptions in particular. Do note that they don't propose C libraries to be added to Boost or C++, they don't want to touch either. By the way, I have a lot of respect for C and for people who choose it over C++. I've also learned the value of using C-style interfaces between modules in a large scale C++ projects.
So you have to be able to make your point in the abstract, which is very
difficult. It is especially difficult because using either approach, a high quality development team can produce a robust program. But just because Google managed to build a phone using Java, it doesn't mean it was a good choice.
You still ignore the fact that I'm talking about a qualitative (not quantitative) property: more errors go to compile time errors. I have been focusing on such aspect since the beginning of the discussion.
I demonstrated why this is false, but there is no such thing as free lunch. Everything has cost. It is important to keep this in mind when criticising C and C++.
It's fact that you can't ignore the error because the code will fail to compile. Error cases are explicit.
Yes, using exception handling you can make mistakes that would be less
likely to make otherwise. Also, using C++ you can make a whole lot of mistakes you can't make in Java, that's one reason why some people use Java and not C++, but this doesn't make them good C++ programmers -- nor it means that there is something wrong with C++.
Agreed.
There are very many examples of things being very difficult to express in
C++ while being trivial in another language. There are usually good reasons for this, and there are many examples to the contrary. The ability to use RAII and exceptions to enforce postconditions is one such advantage.
Not sure what kind of postcondition you're talking about this time.
What postcondition, that depends on the design, but the effect is that you're making it impossible for control to reach contexts for which it would be a logic error to execute in case a previous operation failed.
If it is class invariants, you can have them without exceptions.
You can have them, but you can't know if they're been established because in case of a failure, control may still reach code that requires them to be in place.
And just one note about RAII: Rust does have RAII. No GC. A lot about the language was inspired by C++ knowledge.
Yes, I get that you love Rust. :)
I'll point out again that in C++ it is impossible to report an error from
a constructor except by throwing, and this is a good thing. I assume you disagree -- so what do you propose instead? It can't be OUTCOME_TRY, can it?
I'm not proposing you to code differently.
My question still stands though. If you don't use exceptions, in C++, how do you protect users from calling member functions on objects that have not been initialized? Emil
On Wed, Jan 31, 2018 at 5:58 PM, Emil Dotchevski
My question still stands though. If you don't use exceptions, in C++, how do you protect users from calling member functions on objects that have not been initialized?
I mean haven't been initialized or failed to initialize. Emil
On 1/02/2018 15:38, Emil Dotchevski wrote:
My question still stands though. If you don't use exceptions, in C++, how do you protect users from calling member functions on objects that have not been initialized?
I mean haven't been initialized or failed to initialize.
The usual technique to manage that is to separate no-fail construction (constructor) from fail-possible initialisation (an init() method). Sometimes this requires weaker invariants than otherwise (eg. allowing an empty state). Often this two-phase construction is hidden from consumers by making both of them private and publishing a static factory method instead. Note this is also the same technique commonly employed by people who want to guarantee use of shared_ptr (for use with shared_from_this internally). The factory method technique also allows somewhat restoring a stronger invariant -- only the constructor and destructor need to cope with empty or otherwise uninitialised instances; other methods can be reasonably assured that no such instances escaped the factory method.
On Wed, Jan 31, 2018 at 8:24 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 1/02/2018 15:38, Emil Dotchevski wrote:
My question still stands though. If you don't use exceptions, in C++, how
do you protect users from calling member functions on objects that have not been initialized?
I mean haven't been initialized or failed to initialize.
The usual technique to manage that is to separate no-fail construction (constructor) from fail-possible initialisation (an init() method).
This does not protect users from calling member functions on objects that have not been initialized: foo a; a.bar(); //forgot to call init, now what? or that have failed to initialize: foo a; a.init(); //failed, but nobody noticed a.bar();
Sometimes this requires weaker invariants than otherwise (eg. allowing an empty state).
I can state this more precisely: it _always_ requires no-fail partial initialization with boolean semantics (e.g. a private bool member), which member functions can use as a base to build the logic needed to detect failures to establish the "real" invariants of the object, except in the case when the type has a natural no-fail empty state, in which case yes, member functions can safely assume all is good. The common trend you'll notice is that you'll be writing code which is either automatic or unnecessary if you use excptions to report failures. Of course, people who don't use exception handling don't always write this code (not to mention, writing it is prone to errors, especially under maintenance), which is to say they have no defense against this type of logic errors.
The factory method technique also allows somewhat restoring a stronger invariant -- only the constructor and destructor need to cope with empty or otherwise uninitialised instances; other methods can be reasonably assured that no such instances escaped the factory method.
This is true only if you use exceptions to report errors from the factory. Otherwise it is most definitely NOT reasonable for member functions to assume that the object has been initialized, because nothing guarantees that an error reported by the factory wasn't ignored. Emil
On 1/02/2018 19:09, Emil Dotchevski wrote:
The factory method technique also allows somewhat restoring a stronger invariant -- only the constructor and destructor need to cope with empty or otherwise uninitialised instances; other methods can be reasonably assured that no such instances escaped the factory method.
This is true only if you use exceptions to report errors from the factory. Otherwise it is most definitely NOT reasonable for member functions to assume that the object has been initialized, because nothing guarantees that an error reported by the factory wasn't ignored.
The factory itself provides that guarantee. One such pseudo-code implementation might be: static std::unique_ptr<A> A::create(args...) noexcept { // the constructor is itself noexcept and cannot fail std::unique_ptr<A> a(new (std::nothrow) A(arg1)); if (!a) return nullptr; if (!a->private_init_stuff(arg2, arg3, ...)) return nullptr; return a; } This can't return *why* it failed to create an A, but it cannot return an invalid A, assuming that private_init_stuff always returns false if it failed to satisfactorily init the object. Thus any public members of A (and basically anything other than the constructor, destructor and what is directly called from here or there) can always assume it has a fully valid instance, whatever that means. If you don't like the heap usage, the same concept applies if it returns an optional instead of a pointer (assuming it has a no-fail move). With Boost.Outcome, it can be implemented exactly as above (including not letting an invalid instance escape), but *also* indicate a failure reason to the caller, through a simple change to the return type. Are exceptions cleaner and shorter? Of course. But this still works, and some people will prefer it due to perceived latency issues with exceptions, or just personal issues with exceptions in general, or with "hidden code paths" expressly due to the code being shorter. (Whether those issues are real or not, I cannot say.)
On Wed, Jan 31, 2018 at 10:30 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 1/02/2018 19:09, Emil Dotchevski wrote:
The factory method technique also allows somewhat restoring a stronger
invariant -- only the constructor and destructor need to cope with empty or otherwise uninitialised instances; other methods can be reasonably assured that no such instances escaped the factory method.
This is true only if you use exceptions to report errors from the factory. Otherwise it is most definitely NOT reasonable for member functions to assume that the object has been initialized, because nothing guarantees that an error reported by the factory wasn't ignored.
The factory itself provides that guarantee. One such pseudo-code implementation might be:
static std::unique_ptr<A> A::create(args...) noexcept { // the constructor is itself noexcept and cannot fail std::unique_ptr<A> a(new (std::nothrow) A(arg1)); if (!a) return nullptr;
if (!a->private_init_stuff(arg2, arg3, ...)) return nullptr;
return a; }
This does not protect the user at all: std::unique_ptr<A> a=A::create(); a->foo(); //undefined behavior Compare to: static std::unique_ptr<A> A::create(args...) { std::unique_ptr<A> a(new A(arg1)); //new throws on error, A::A() throws on error return a; } And then: std::unique_ptr<A> a=A::create(); a->foo(); //Okay, a is guaranteed to be valid. Once again, you'll notice that if you use exceptions, the compiler "writes" the correct error checks for you, and the user is automatically protected. With Boost.Outcome, it can be implemented exactly as above (including not
letting an invalid instance escape), but *also* indicate a failure reason to the caller, through a simple change to the return type.
This is not an advantage over using exceptions, and besides it is only true in the simplest of cases. The problem is that Outcome requires users to fit any and all possible failures into a single error type, which is not always possible. In general, you can neither enumerate nor reason about all failures which may occur. Notably, the one Outcome feature that can deal with this problem is its ability to transport std::exception_ptr. :)
Are exceptions cleaner and shorter? Of course. But this still works, and some people will prefer it due to perceived latency issues with exceptions, or just personal issues with exceptions in general, or with "hidden code paths" expressly due to the code being shorter. (Whether those issues are real or not, I cannot say.)
Well, I can. :) Emil
On Feb 1, 2018 7:56 AM, "Emil Dotchevski via Boost"
On 1/02/2018 19:09, Emil Dotchevski wrote:
The factory method technique also allows somewhat restoring a stronger
invariant -- only the constructor and destructor need to cope with empty or otherwise uninitialised instances; other methods can be reasonably assured that no such instances escaped the factory method.
This is true only if you use exceptions to report errors from the factory. Otherwise it is most definitely NOT reasonable for member functions to assume that the object has been initialized, because nothing guarantees that an error reported by the factory wasn't ignored.
The factory itself provides that guarantee. One such pseudo-code implementation might be:
static std::unique_ptr<A> A::create(args...) noexcept { // the constructor is itself noexcept and cannot fail std::unique_ptr<A> a(new (std::nothrow) A(arg1)); if (!a) return nullptr;
if (!a->private_init_stuff(arg2, arg3, ...)) return nullptr;
return a; }
This does not protect the user at all: std::unique_ptr<A> a=A::create(); a->foo(); //undefined behavior Compare to: static std::unique_ptr<A> A::create(args...) { std::unique_ptr<A> a(new A(arg1)); //new throws on error, A::A() throws on error return a; } And then: std::unique_ptr<A> a=A::create(); a->foo(); //Okay, a is guaranteed to be valid. Yeah but you can write stupid code with anything: ``` std::unique_ptr<A> a; try { a =A::create(); } catch (...) {} a->foo(); // ups ```
On Wed, Jan 31, 2018 at 11:01 PM, Jonathan Müller < jonathanmueller.dev@gmail.com> wrote:
Yeah but you can write stupid code with anything:
``` std::unique_ptr<A> a; try { a =A::create(); } catch (...) {} a->foo(); // ups ```
I can optimize this: std::unique_ptr<A> a; a->foo(); :)
On 02/01/18 10:01, Jonathan Müller via Boost wrote:
On Feb 1, 2018 7:56 AM, "Emil Dotchevski via Boost"
wrote: On Wed, Jan 31, 2018 at 10:30 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 1/02/2018 19:09, Emil Dotchevski wrote:
The factory method technique also allows somewhat restoring a stronger
invariant -- only the constructor and destructor need to cope with empty or otherwise uninitialised instances; other methods can be reasonably assured that no such instances escaped the factory method.
This is true only if you use exceptions to report errors from the factory. Otherwise it is most definitely NOT reasonable for member functions to assume that the object has been initialized, because nothing guarantees that an error reported by the factory wasn't ignored.
The factory itself provides that guarantee. One such pseudo-code implementation might be:
static std::unique_ptr<A> A::create(args...) noexcept { // the constructor is itself noexcept and cannot fail std::unique_ptr<A> a(new (std::nothrow) A(arg1)); if (!a) return nullptr;
if (!a->private_init_stuff(arg2, arg3, ...)) return nullptr;
return a; }
This does not protect the user at all:
std::unique_ptr<A> a=A::create(); a->foo(); //undefined behavior
Compare to:
static std::unique_ptr<A> A::create(args...) { std::unique_ptr<A> a(new A(arg1)); //new throws on error, A::A() throws on error return a; }
And then:
std::unique_ptr<A> a=A::create(); a->foo(); //Okay, a is guaranteed to be valid.
Yeah but you can write stupid code with anything:
``` std::unique_ptr<A> a; try { a =A::create(); } catch (...) {} a->foo(); // ups ```
The important difference between using exceptions and error codes (or Boost.Outcome, I presume) is that in case of exceptions the user has to make an effort to write broken code and the correct code most of the time comes naturally, while with manual error checking it is the other way around. This is the reason why manual error checking is more prone to mistakes in error handling. PS: All that, of course, is given that RAII is ubiquitous. If it's not then error handling is difficult regardless of the tool you use.
On 01.02.2018 12:10, Andrey Semashev via Boost wrote:
The important difference between using exceptions and error codes (or Boost.Outcome, I presume) is that in case of exceptions the user has to make an effort to write broken code and the correct code most of the time comes naturally, while with manual error checking it is the other way around. This is the reason why manual error checking is more prone to mistakes in error handling.
PS: All that, of course, is given that RAII is ubiquitous. If it's not then error handling is difficult regardless of the tool you use.
This entire mistake would have been prevented, if a proper return type was used, for example a sane optional (that doesn't try to be a pointer): static optional<A> A::create(…) {…} Then, this will not compile: auto a = A::create(…); a.foo(); // error! So, you're forced to write: a.value().foo(); And accessing the value on an optional without checking it should always be a red flag and not just written "naturally" (That's why operator-> for optional is a mistake IMO). Yes, it's still prune to mistakes, but so are exceptions, it's just a little bit more work: Foo::Foo() { try { my_a = A::create(…); } catch (…) { log_error(); } } Foo::bar() { my_a->foo(); // ups? } So I won't necessarily say that manual error handling is more error prone, with properly designed facilities (and a little language support) it can be as good as exceptions (just look at Swift: it provides syntax sugar for error output parameters).
On 02/01/18 15:03, Jonathan Müller via Boost wrote:
On 01.02.2018 12:10, Andrey Semashev via Boost wrote:
The important difference between using exceptions and error codes (or Boost.Outcome, I presume) is that in case of exceptions the user has to make an effort to write broken code and the correct code most of the time comes naturally, while with manual error checking it is the other way around. This is the reason why manual error checking is more prone to mistakes in error handling.
PS: All that, of course, is given that RAII is ubiquitous. If it's not then error handling is difficult regardless of the tool you use.
This entire mistake would have been prevented, if a proper return type was used, for example a sane optional (that doesn't try to be a pointer):
static optional<A> A::create(…) {…}
Then, this will not compile:
auto a = A::create(…); a.foo(); // error!
So, you're forced to write:
a.value().foo();
And accessing the value on an optional without checking it should always be a red flag and not just written "naturally" (That's why operator-> for optional is a mistake IMO).
I disagree. The necessity to write ".value()" on every access to `a` is an overhead, both in user's effort and runtime performance. This, actually, illustrates my point. (And no, `operator->` is the most often used way to access the value stored in an optional in my code, as I'm sure it is in that of many other people. Test for presence once, use unchecked afterwards.)
Yes, it's still prune to mistakes, but so are exceptions, it's just a little bit more work:
Foo::Foo() { try { my_a = A::create(…); } catch (…) { log_error(); } }
Thing is, you don't usually write that code. You usually catch exceptions at the point where you can do something about them, which in this case is the client code that requested an object of `Foo`. So your typical code would be this: Foo::Foo() : my_a(A::create()) { } void Foo::bar() { // my_a is guaranteed to be valid } try { auto pFoo = std::make_unique< Foo >(); pFoo->bar(); // pFoo is guaranteed to be valid } catch (...) { // handle errors } Whereas, with manual error handling, all code between the error in `A::create` and the actual error handler and also `Foo::bar` etc. becomes "tainted" with checks.
On 02/01/18 16:32, Andrey Semashev wrote:
On 02/01/18 15:03, Jonathan Müller via Boost wrote:
On 01.02.2018 12:10, Andrey Semashev via Boost wrote:
The important difference between using exceptions and error codes (or Boost.Outcome, I presume) is that in case of exceptions the user has to make an effort to write broken code and the correct code most of the time comes naturally, while with manual error checking it is the other way around. This is the reason why manual error checking is more prone to mistakes in error handling.
PS: All that, of course, is given that RAII is ubiquitous. If it's not then error handling is difficult regardless of the tool you use.
This entire mistake would have been prevented, if a proper return type was used, for example a sane optional (that doesn't try to be a pointer):
static optional<A> A::create(…) {…}
Then, this will not compile:
auto a = A::create(…); a.foo(); // error!
So, you're forced to write:
a.value().foo();
And accessing the value on an optional without checking it should always be a red flag and not just written "naturally" (That's why operator-> for optional is a mistake IMO).
I disagree. The necessity to write ".value()" on every access to `a` is an overhead, both in user's effort and runtime performance. This, actually, illustrates my point.
(And no, `operator->` is the most often used way to access the value stored in an optional in my code, as I'm sure it is in that of many other people. Test for presence once, use unchecked afterwards.)
Yes, it's still prune to mistakes, but so are exceptions, it's just a little bit more work:
Foo::Foo() { try { my_a = A::create(…); } catch (…) { log_error(); } }
Thing is, you don't usually write that code. You usually catch exceptions at the point where you can do something about them, which in this case is the client code that requested an object of `Foo`. So your typical code would be this:
Foo::Foo() : my_a(A::create()) { }
void Foo::bar() { // my_a is guaranteed to be valid }
try { auto pFoo = std::make_unique< Foo >(); pFoo->bar(); // pFoo is guaranteed to be valid } catch (...) { // handle errors }
Whereas, with manual error handling, all code between the error in `A::create` and the actual error handler and also `Foo::bar` etc. becomes "tainted" with checks.
Which is much more opportunity to make a mistake.
On 01.02.2018 14:32, Andrey Semashev via Boost wrote:
On 02/01/18 15:03, Jonathan Müller via Boost wrote:
And accessing the value on an optional without checking it should always be a red flag and not just written "naturally" (That's why operator-> for optional is a mistake IMO).
I disagree. The necessity to write ".value()" on every access to `a` is an overhead, both in user's effort and runtime performance. This, actually, illustrates my point.
With my proposal, `value()` would be the unchecked (i.e. debug checked) access, so it has no effort on runtime performance. Yes, it is less convenient than exceptions, that's why you should still prefer exceptions without more language support for optionals and expected objects. But I don't think it is more error prone than exceptions, which is the point I'm trying to make.
On 02/01/18 16:40, Jonathan Müller via Boost wrote:
On 01.02.2018 14:32, Andrey Semashev via Boost wrote:
On 02/01/18 15:03, Jonathan Müller via Boost wrote:
And accessing the value on an optional without checking it should always be a red flag and not just written "naturally" (That's why operator-> for optional is a mistake IMO).
I disagree. The necessity to write ".value()" on every access to `a` is an overhead, both in user's effort and runtime performance. This, actually, illustrates my point.
With my proposal, `value()` would be the unchecked (i.e. debug checked) access, so it has no effort on runtime performance.
If your `value()` is unchecked then I really don't understand how writing `a.value().foo()` would be less error prone than the current `a->foo()` without a prior check.
Yes, it is less convenient than exceptions, that's why you should still prefer exceptions without more language support for optionals and expected objects.
But I don't think it is more error prone than exceptions, which is the point I'm trying to make.
The more code you have to type, the more places where you can make a mistake. My point is that since with exceptions you don't need to type any code to test for errors, except where you actually do, it is less error prone.
On 1/02/2018 19:55, Emil Dotchevski wrote:
This does not protect the user at all:
std::unique_ptr<A> a=A::create(); a->foo(); //undefined behavior
Sure, but you can always write daft code. If you are aware that it is legal for A::create() to return nullptr (and you should be aware, because it will be documented), then the above would be an obvious bug, and you should replace it with: if (auto a = A::create()) { a->foo(); } This is *much* more obviously safe than your suggested exception-enabled code, where you have to have external knowledge (that it will throw on failure and cannot ever return nullptr). And it's trivial to add logic for the failure case in an else block, vs. the more wordy try-catch. (Even if you don't know whether A::create() is supposed to be able to return nullptr or not, it is usually more sensible to defensively write the code as if it could, since it is a legal value of the return type, regardless of whether exceptions are enabled or not.) Granted, exceptions are better at telling you *why* it failed, which the above can't. And exceptions are better at hiding the error-handling paths for the cases where errors are expected to be exceptionally rare (pun intended), which can make the "real" logic not get lost in a maze of error-handling. Most people appreciate that -- but sometimes having explicit error paths is useful too. Nobody is forcing you to stop using exceptions, or even encouraging the majority of applications to stop using them. Outcome just provides a way to add back a little (or a lot, as needed) explanation to failure in cases where someone has already decided they can't or don't want to use exceptions for whatever reason -- especially when failure is expected, not unusual (I think everybody agrees that exceptions-as-control-flow is not good code design, right?).
This is not an advantage over using exceptions, and besides it is only true in the simplest of cases. The problem is that Outcome requires users to fit any and all possible failures into a single error type, which is not always possible. In general, you can neither enumerate nor reason about all failures which may occur. Notably, the one Outcome feature that can deal with this problem is its ability to transport std::exception_ptr. :)
One error type per context. You can have completely different error enumerations per method if you really want to, although it would probably be more common to have one error domain per library, or just use the closest approximation from a standard set, like POSIX errno. Or use error_code, which allows relatively seamless interoperation with all of these. Exceptions are actually a relatively poor method for transporting errors that need to be *reacted* to. (Not so much through any fault of exceptions themselves, but rather through common usage thereof.) In most code, the best you can do with them is catch them in some central place and log them for some developer to look at later. Too many methods just throw one of the standard types like std::runtime_error without defining custom subclasses or adding anything more programmatically useful beyond the text message -- which in itself can be a problem for multi-lingual applications, especially where the thread that generated the exception is running in a different language context from the developer and/or user. At least an error code is inherently an abstract thing that you know you need to look up a translation for at the last moment when actually presenting it to the user, or that logic can easily say "in this context, I was expecting that and I know how to deal with it". Sure, it's possible to add error codes and extra data fields to exceptions and allow the message to be constructed later from these pieces. And it's not possible to do this with error_code alone. But it will be possible to do this with Outcome. (And most people don't do it anyway, no matter the error transport.)
On Thu, Feb 1, 2018 at 5:29 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 1/02/2018 19:55, Emil Dotchevski wrote:
This does not protect the user at all:
std::unique_ptr<A> a=A::create(); a->foo(); //undefined behavior
Sure, but you can always write daft code. If you are aware that it is legal for A::create() to return nullptr (and you should be aware, because it will be documented), then the above would be an obvious bug, and you should replace it with:
if (auto a = A::create()) { a->foo(); }
This is *much* more obviously safe than your suggested exception-enabled code, where you have to have external knowledge (that it will throw on failure and cannot ever return nullptr). And it's trivial to add logic for the failure case in an else block, vs. the more wordy try-catch.
You don't use try-catch often at all in C++, very few places need it. In the case you need to handle the failure locally, it is not all that verbose: try { auto a=A::create(); a->foo(); } catch(...) { } vs. if( auto a=A::create() ) { a->foo(); } else { } Obviously you have to know that A::create() throws on error, however it is not inherently clearer to use the explicit if, because you don't know if you're checking for failure or checking for null ptr that isn't a failure. Yes, this is a bit silly in case of a function that is called create, hence the if is silly too if you know that it'll throw on error. Granted, exceptions are better at telling you *why* it failed, which the
above can't. And exceptions are better at hiding the error-handling paths for the cases where errors are expected to be exceptionally rare (pun intended)
It has nothing to do with how rare it is. If there is a useful postcondition to be enforced, you throw, even if it happens frequently. Nobody is forcing you to stop using exceptions, or even encouraging the
majority of applications to stop using them. Outcome just provides a way to add back a little (or a lot, as needed) explanation to failure in cases where someone has already decided they can't or don't want to use exceptions for whatever reason
I've expressed my objections already, but see below.
This is not an advantage over using exceptions, and besides it is only true
in the simplest of cases. The problem is that Outcome requires users to fit any and all possible failures into a single error type, which is not always possible. In general, you can neither enumerate nor reason about all failures which may occur. Notably, the one Outcome feature that can deal with this problem is its ability to transport std::exception_ptr. :)
One error type per context. You can have completely different error enumerations per method if you really want to, although it would probably be more common to have one error domain per library, or just use the closest approximation from a standard set, like POSIX errno. Or use error_code, which allows relatively seamless interoperation with all of these.
You're missing my point. I am not saying that one can't design an error
type to use with result
In most code, the best you can do with them is catch them in some central place and log them for some developer to look at later.
That is not true at all. You catch exceptions when you can recover from them. Semantically, it's as if after every function call the compiler automatically inserts: if( error ) return error; ...except in the case when you can handle the error, in which case you write a different if statement, in the form of try..catch. Nothing more, nothing less.
Too many methods just throw one of the standard types like std::runtime_error without defining custom subclasses or adding anything more programmatically useful beyond the text message
This is plain stupid. Equally stupid would be to return result
2018-02-02 4:58 GMT+01:00 Emil Dotchevski via Boost
Ideally, the erorr type should be erased, that is, it should not be a parameter of the result template, but of the result constructor, so that error-neutral contexts can just forward _any_ error to the caller. This is not easy to do. I tried to do this, but the result<T> type I ended up with wasn't efficient enough to return at every function call. This lead me to the realization that it's a bit silly to return the error object one level at a time, because usually the caller only needs it to return it to its caller. Ultimately, this lead me to using TLS, and the result is (Boost) Noexcept.
I agree practically with everything you say, except for the above paragraph. That is, in most applications, in most parts the situation is exactly as you describe: what you want to do is to forward the failure reason up and abort the current task. And you would look for solution where this is done by default. In contrast, Boost.Outcome is intended for usage in these rare cases were "forward up and abort" is not the thing you would like to do most of the time. Its goal is not to handle failures anywhere, but to solve a specific problem in specific places. I believe that the criticism of Boost.Outcome should be in this context of places where "forward up and abort" is not what you typically need to do. Regards, &rzej;
In contrast, Boost.Outcome is intended for usage in these rare cases were "forward up and abort" is not the thing you would like to do most of the time. Its goal is not to handle failures anywhere, but to solve a specific problem in specific places. I believe that the criticism of Boost.Outcome should be in this context of places where "forward up and abort" is not what you typically need to do.
And this really drives to heart of what I don't get about your arguments Emil. Outcome is really an abstraction layer for setting per-namespace rules for when to throw exceptions. Exception throwing is absolutely at the heart of Outcome. That's why Outcome != Expected, and why it ICEs older compilers, and why C++ 14 is needed. Let me give you an example literally from what I was doing last night. I just this week finally persuaded OS X to compile AFIO, and amazingly the test suite passed as well as on Linux except for the async i/o test which was failing.
From the build log on Travis, I could immediately see that the cause was a "resource temporarily unavailable" failure. An `io_result<>` was being constructed with that failure, returned to the i/o completion handler which in turn type erased it into an exception_ptr, put that into the future for the completion handler, and finally at the end of the test all accumulated failures were rethrown out out the futures for all the async i/o.
Outcome behaved exactly as AFIO told it to behave: expected errors were handled locally with an unmeasurably low overhead, unexpected errors were type erased into an exception_ptr and pushed up the call stack. Eventually after a fair bit of head scratching I discovered that OS X's implementation of `aio_select()` does not do what its man page says it does, so last night I committed a workaround and now async i/o works on OS X, so -3 failures at https://my.cdash.org/index.php?project=Boost.AFIO&date=2018-02-01 yay! The point I'm making here is that exception throws are what Outcome does. You're basically arguing against the premise of this library on ideological grounds, yet the library is fulfilling all of the rationales you have made on why exception throws are the right approach. Outcome is ticking all your boxes. Outcome is all about throwing exceptions. Will you consider now changing your peer review vote? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Fri, Feb 2, 2018 at 1:19 AM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
In contrast, Boost.Outcome is intended for usage in these rare cases were "forward up and abort" is not the thing you would like to do most of the time. Its goal is not to handle failures anywhere, but to solve a specific problem in specific places. I believe that the criticism of Boost.Outcome should be in this context of places where "forward up and abort" is not what you typically need to do.
And this really drives to heart of what I don't get about your arguments Emil. Outcome is really an abstraction layer for setting per-namespace rules for when to throw exceptions. Exception throwing is absolutely at the heart of Outcome. That's why Outcome != Expected, and why it ICEs older compilers, and why C++ 14 is needed.
Consider the following library:
template
So, my vote is based on (in no particular order):
1) In these discussions, I can clearly recognize the dislike for exception handling and even C++ (I don't mean by you personally) that I have been exposed to in the past, since for years I've been surrounded by people who falsely believed that they can't afford exceptions or smart pointers or proper serialization, and they have strong, if incorrect, opinions on what's wrong with C++. I believe that this attitude does not belong to Boost. It's possible that I got this wrong. It may be interesting to know how many of the current users of "standalone" Outcome use Boost in "low latency" environments or at all. Do you have an idea?
No, nor do I think it matters. The rationale to use Outcome for me in my code is to let the caller decide whether an exception should be thrown or not, rather than the function experiencing the failure hard coding an exception throw which invokes an unavoidable table search I'd like to avoid. That's my primary use case in my own code. My secondary use case is that rather than encode the logic for how to type erase/rethrow/convert the failure into an exception throw via a preprocessor macro which is how it's usually done e.g. BOOST_THROW_EXCEPTION(), I'd like to set rules via the type system for what is to happen. No macros needed. The policies are very useful for this use case. My tertiary use case is accumulation of unknown failures into my own test framework. Failures can come from anywhere, including the STL, and I preserve all the original information. No information loss at any point, and a complete chain of execution is recorded by the test framework which can be checked for correct handling of failure. The 16 bits of spare storage in Outcome combined with the ADL construction hooks and TLS is very useful for this use case. Now, lots of people have other use cases for Outcome, but those three are my main personal motivating use cases. And Outcome is a superb solution for my needs, else I'd not have invested so much effort into it.
2) Clearly, Outcome _does_ want to help pass errors across API boundaries, including in generic contexts. The problem is that
result
compute() noexcept; is very similar to
T compute() throw(E);
(yes, I know exception specifications are enforced dynamically, but that's not what's wrong with them, see the second question here: https://herbsutter.com/2007/01/24/questions-about-exception-specifications/.)
My reasoning is that if with Outcome you can always return the exact error type you've specified in your static interface, the same approach would work for (perhaps statically-enforced) exception specifications.
The exception specification analogy doesn't apply to Outcome. What
doomed exception specifications is indirect function calls combined with
the side channel exception throws operate through, so your function
which guarantees to never throw anything but E happens to call some
overriden virtual function which throws a different type, and boom
you've just called std::terminate.
That's because exception throws operate via a side channel outside the
normal flow of execution. Outcome doesn't have that - it returns via the
normal flow of execution. Therefore we can hard guarantee that if the
program compiles, the "exception specification" is met. Overriding a
virtual function will only compile if the return type matches
result
Logically, to address this concern you could:
- Demonstrate that there is a major flaw in my analogy, or
- demonstrate that exception specifications could be made practical, including in generic contexts, possibly by using some clever policy-based design, or
- provide an interface that can forward arbitrary errors ot the caller.
(I see these as mutually-exclusive).
As I pointed out to you during Outcome v1 review, you can implement TLS push and pop for Outcome just the same as your Noexcept library does. As an example, in AFIO, all errored results are recorded into a TLS ringbuffer which tracks the execution log, so for any given error, AFIO can tell you the exact sequence of API calls, including to internal functions, and their parameters, leading up to that point. It also can tell you exactly how AFIO handles the failure, every single function called, including internal ones, as the stack is unwound. I haven't implemented it yet, but it'll all designed to get fired into a file so it acts as an i/o validation audit log, but right now its main use is for validation testing, so the test suite can say if the correct execution paths were taken for some given failure scenario. Outcome doesn't come with such features builtin. But it provides a rich set of customisation points to let you roll any bespoke error and exception handling framework you like. It really is very handy to have in the toolbox, abstracting out the handling of failure into a generic framework is something we've not done much work on in C++ in recent years. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
My secondary use case is that rather than encode the logic for how to type erase/rethrow/convert the failure into an exception throw via a preprocessor macro which is how it's usually done e.g. BOOST_THROW_EXCEPTION(), ...
The macro is only needed to capture __FILE__ and __LINE__. If we get std::source_location, it won't be necessary.
What doomed exception specifications is indirect function calls combined with the side channel exception throws operate through, so your function which guarantees to never throw anything but E happens to call some overriden virtual function which throws a different type, and boom you've just called std::terminate.
No, you don't; the overrider is required to have a matching exception specification. What doomed exception specifications is that they just aren't useful. They solve a problem that doesn't need solving.
On Fri, Feb 2, 2018 at 4:20 PM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
So, my vote is based on (in no particular order):
1) In these discussions, I can clearly recognize the dislike for exception handling and even C++ (I don't mean by you personally) that I have been exposed to in the past, since for years I've been surrounded by people who falsely believed that they can't afford exceptions or smart pointers or proper serialization, and they have strong, if incorrect, opinions on what's wrong with C++. I believe that this attitude does not belong to Boost. It's possible that I got this wrong. It may be interesting to know how many of the current users of "standalone" Outcome use Boost in "low latency" environments or at all. Do you have an idea?
No, nor do I think it matters.
If a significant subset of the users of the library won't touch Boost, I think it does matter. The reason for this speculation is that I've seen this before, I know my people. :)
My secondary use case is that rather than encode the logic for how to type erase/rethrow/convert the failure into an exception throw via a preprocessor macro which is how it's usually done e.g. BOOST_THROW_EXCEPTION(), I'd like to set rules via the type system for what is to happen. No macros needed. The policies are very useful for this use case.
The macro only captures __FILE__ and __LINE__. The meat is in boost::throw_exception. If that was customizeable, it would render exceptions useless in Boost libraries, which are encouraged to use boost::throw_exception to throw. Let me know if you need me to elaborate why these are the correct semantics for boost::throw_exception: www.boost.org/doc/libs/release/libs/exception/doc/throw_exception.html
2) Clearly, Outcome _does_ want to help pass errors across API boundaries, including in generic contexts. The problem is that
result
compute() noexcept; is very similar to
T compute() throw(E);
(yes, I know exception specifications are enforced dynamically, but that's not what's wrong with them, see the second question here: https://herbsutter.com/2007/01/24/questions-about- exception-specifications/.)
My reasoning is that if with Outcome you can always return the exact error type you've specified in your static interface, the same approach would work for (perhaps statically-enforced) exception specifications.
The exception specification analogy doesn't apply to Outcome. What doomed exception specifications is indirect function calls combined with the side channel exception throws operate through
Granted, exceptions are hidden from view, so to speak, but that was exactly the motivation behind exception specifications, to make what functions may throw an official, "visible" part of their static interface, so the caller can reason based on that, just like in Outcome.
, so your function which guarantees to never throw anything but E happens to call some overriden virtual function which throws a different type, and boom you've just called std::terminate.
Virtual overrides must match the exception specification, but yes, in C++ exception specifications are enforced dynamically.
That's because exception throws operate via a side channel outside the normal flow of execution. Outcome doesn't have that - it returns via the normal flow of execution. Therefore we can hard guarantee that if the program compiles, the "exception specification" is met.
Agreed, the Outcome "exception specification" is statically enforced. The Herb Sutter page I linked explains that statically-enforced exception specifications are better than dynamically-enforced ones. The thing is (in terms of Outcome), if I've got an error of type E1 which I can't handle, there is nothing to gain if you force me to convert that to a type E2, which I also can't handle. Not only this conversion is not always possible (think generic contexts), but the fact that the error was of type E1 was lost in translation. And what is the caller to do with the false E2 I returned if he can't handle the error either? Translate it to E3, so by the time the error reaches someone who has to deal with it, he is completely clueless about what the error was to begin with? No, if you can't deal with the error at this level, you leave it alone but help as much as you can. This means adding context-specific information. Maybe you got a file read error from a low level function, and you happen to know the file name that you were reading, so you attach that to the error. And if the error you got wasn't a file read error but a parse error, what do you do? Attach the file name. What if it is an out of memory error? Same thing, attach the file name.
Logically, to address this concern you could:
- Demonstrate that there is a major flaw in my analogy, or
- demonstrate that exception specifications could be made practical, including in generic contexts, possibly by using some clever policy-based design, or
- provide an interface that can forward arbitrary errors ot the caller.
(I see these as mutually-exclusive).
As I pointed out to you during Outcome v1 review, you can implement TLS push and pop for Outcome just the same as your Noexcept library does.
This is true, but it doesn't make the complex policy-based translation machinery any more practical. The problem is the same as in statically-enforced exception specifications. I don't think that TLS is the only viable implementation. For example, it's possible to erase the error type through shared_ptr that uses a custom allocator to avoid dynamic memory allocation. I have not explored that design, but I think it has legs.
As an example, in AFIO, all errored results are recorded into a TLS ringbuffer which tracks the execution log, so for any given error, AFIO can tell you the exact sequence of API calls, including to internal functions, and their parameters, leading up to that point.
The TLS stack, complete with the ability to transport result<T> objects between threads, as implemented in Noexcept, is not trivial. It's the kind of tricky bit of code that library developers sweat over so others don't have to.
It also can tell you exactly how AFIO handles the failure, every single function called, including internal ones, as the stack is unwound. I haven't implemented it yet, but it'll all designed to get fired into a file so it acts as an i/o validation audit log
Yes logging is much more important if you don't use exceptions (and I don't mean this as a negative, sometimes the right thing to do is to not use exceptions). I think that the reason is that you can't enforce postconditions, so it is more likely to execute code which shouldn't execute, and the log is needed to help you figure out which curious parts of the program control reached this time around. Emil
2018-02-03 1:20 GMT+01:00 Niall Douglas via Boost
So, my vote is based on (in no particular order):
1) In these discussions, I can clearly recognize the dislike for exception handling and even C++ (I don't mean by you personally) that I have been exposed to in the past, since for years I've been surrounded by people who falsely believed that they can't afford exceptions or smart pointers or proper serialization, and they have strong, if incorrect, opinions on what's wrong with C++. I believe that this attitude does not belong to Boost. It's possible that I got this wrong. It may be interesting to know how many of the current users of "standalone" Outcome use Boost in "low latency" environments or at all. Do you have an idea?
No, nor do I think it matters. The rationale to use Outcome for me in my code is to let the caller decide whether an exception should be thrown or not, rather than the function experiencing the failure hard coding an exception throw which invokes an unavoidable table search I'd like to avoid. That's my primary use case in my own code.
But look like your rationale is different than the motivation section in the documentation. Maybe this is a communication problem. Again we may have failed in communicating to the people what this library is not.
My secondary use case is that rather than encode the logic for how to type erase/rethrow/convert the failure into an exception throw via a preprocessor macro which is how it's usually done e.g. BOOST_THROW_EXCEPTION(), I'd like to set rules via the type system for what is to happen. No macros needed. The policies are very useful for this use case.
But there is one notable inconvenience. Two different policies applied in two different places render two different instances of `result<T>`. These instances are not inter-convertible (or am I wrong) and you cannot use them freely together. No copy elision. I didn't mind them in the review because I concluded I could always go with the defaulut one. One you have added the narrow-contract `assume_value()` I no longer mind that `value()` throws, and I would never have any business in changing the defaults.
My tertiary use case is accumulation of unknown failures into my own test framework. Failures can come from anywhere, including the STL, and I preserve all the original information. No information loss at any point, and a complete chain of execution is recorded by the test framework which can be checked for correct handling of failure. The 16 bits of spare storage in Outcome combined with the ADL construction hooks and TLS is very useful for this use case.
Now, lots of people have other use cases for Outcome, but those three are my main personal motivating use cases. And Outcome is a superb solution for my needs, else I'd not have invested so much effort into it.
2) Clearly, Outcome _does_ want to help pass errors across API boundaries, including in generic contexts. The problem is that
result
compute() noexcept; is very similar to
T compute() throw(E);
(yes, I know exception specifications are enforced dynamically, but that's not what's wrong with them, see the second question here: https://herbsutter.com/2007/01/24/questions-about- exception-specifications/.)
My reasoning is that if with Outcome you can always return the exact error type you've specified in your static interface, the same approach would work for (perhaps statically-enforced) exception specifications.
The exception specification analogy doesn't apply to Outcome. What doomed exception specifications is indirect function calls combined with the side channel exception throws operate through, so your function which guarantees to never throw anything but E happens to call some overriden virtual function which throws a different type, and boom you've just called std::terminate.
That's because exception throws operate via a side channel outside the normal flow of execution. Outcome doesn't have that - it returns via the normal flow of execution. Therefore we can hard guarantee that if the program compiles, the "exception specification" is met. Overriding a virtual function will only compile if the return type matches result
. So overrides can't introduce hidden calls to std::terminate. Therefore exception specifications are the wrong analogy for result returning code.
I think what Emil meant is that if one library you use reports `result
Logically, to address this concern you could:
- Demonstrate that there is a major flaw in my analogy, or
- demonstrate that exception specifications could be made practical, including in generic contexts, possibly by using some clever policy-based design, or
- provide an interface that can forward arbitrary errors ot the caller.
(I see these as mutually-exclusive).
As I pointed out to you during Outcome v1 review, you can implement TLS push and pop for Outcome just the same as your Noexcept library does.
As an example, in AFIO, all errored results are recorded into a TLS ringbuffer which tracks the execution log, so for any given error, AFIO can tell you the exact sequence of API calls, including to internal functions, and their parameters, leading up to that point. It also can tell you exactly how AFIO handles the failure, every single function called, including internal ones, as the stack is unwound. I haven't implemented it yet, but it'll all designed to get fired into a file so it acts as an i/o validation audit log, but right now its main use is for validation testing, so the test suite can say if the correct execution paths were taken for some given failure scenario.
Outcome doesn't come with such features builtin. But it provides a rich set of customisation points to let you roll any bespoke error and exception handling framework you like. It really is very handy to have in the toolbox, abstracting out the handling of failure into a generic framework is something we've not done much work on in C++ in recent years.
Niall
-- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/ mailman/listinfo.cgi/boost
No, nor do I think it matters. The rationale to use Outcome for me in my code is to let the caller decide whether an exception should be thrown or not, rather than the function experiencing the failure hard coding an exception throw which invokes an unavoidable table search I'd like to avoid. That's my primary use case in my own code.
But look like your rationale is different than the motivation section in the documentation. Maybe this is a communication problem. Again we may have failed in communicating to the people what this library is not.
Do bear in mine the qualifier "for me in my code". I don't claim, nor would I claim, that my need case is anyone else's or ought to be. Hence the present formulation of the tutorial.
My secondary use case is that rather than encode the logic for how to type erase/rethrow/convert the failure into an exception throw via a preprocessor macro which is how it's usually done e.g. BOOST_THROW_EXCEPTION(), I'd like to set rules via the type system for what is to happen. No macros needed. The policies are very useful for this use case.
But there is one notable inconvenience. Two different policies applied in two different places render two different instances of `result<T>`. These instances are not inter-convertible (or am I wrong) and you cannot use them freely together.
Implicit construction is only permitted when value_type and error_type and Policy are the same. Explicit construction is permitted when A::value_type is constructible to B::value_type and A::error_type is constructible to B::error_type. Policy is ignored. ValueOrError construction is permitted when the input is (by the default rules) a foreign type but whose value_type and error_type are constructible to the local type's. Policy has no (default) role here. So, to answer your question, Policy only matters for implicit constructions only, and is otherwise ignored. We trust that if the programmer is doing an explicit construction, they know what they are doing. This applies to the default out-of-the-box rules. One can override the significance of Policy for a custom rule for type X doing a ValueOrError construction into type Y.
No copy elision. C++ 17 appears to allow the compiler to elide converting construction according to the usual copy elision rules. I say this only from examining assembler, I don't know what the standard says.
I think what Emil meant is that if one library you use reports `result
` and another reports `result ` you do not have a good type to return from the code that combines the two. Unless you apply a translation, but this way you loose information.
There's always `result
2018-02-02 23:26 GMT+01:00 Emil Dotchevski via Boost
Consider the following library:
template
class result; //contains variant conversion to bool returns true if result was initialized with T, false otherwise;
.value() returns T& or calls boost::throw_exception(error());
.error() returns E& or undefined behavor.
You could use this library together with exception handling: it helps with annoying exceptions when you can handle the error locally, otherwise you just call .value() and let exceptions do their thing.
The question is what to do under BOOST_NO_EXCEPTIONS. Presumably you could go with E=error_code, but that is inadequate for forwarding arbitrary errors, making the library impractical to use without exceptions -- and we want a library that is practical to use without exceptions. Correct me if I'm wrong, but I think that you and I agree on this.
I must admit I never had to write a program with exceptions disabled; but I assume that when you make this decision, you also sketch out the strategy for dealing with function failures. Maybe you just std::terminate on resource shortage, maybe something else; but I am pretty sure you cannot just say "disable exceptions, and we will deal with failures similarly". This requires the alternate design of the entire program that indicates how failures are communicated and propagated. Technically, this is possible with errno or returning int values representing errro conditions tangled with many if-statements. But that is really error-prone. Using Outcome here is definitely superior to errno and manual if-statements. You say that it is inferior to exceptions, but hey, we are compiling with exceptions disabled. We will accept some compromises in this case. Also, the programs where you disable exceptions would be doing low-level stuff. And there usually you would report failures through integers. I think this is the acceptable compromise when not using exceptions. Once you can accept this cost std::error_code gives you a convenient tool: two ints, one to identify the library, the other to identify the condition in this library. Once you accept this cost. The failure condition recorded in std::error_code never changes, it never needs to be translated to anything else. std::error code addresses your concern about potential translations of exceptions. It does not address your concern about communicating arbitrary data. But in the end it might turn out to be a reasonable trade-off for low-level libraries that disable exceptions. This is one of the applications of boost.Outcome. It is superior to naked std::error_code because it can additionally guarantee that no failure is accidentally ignored by the programmer. I think that Boost.Outcome could be used in tandem with Boost.Noexcept, to use the latter's TLS.
So, my vote is based on (in no particular order):
1) In these discussions, I can clearly recognize the dislike for exception handling and even C++ (I don't mean by you personally) that I have been exposed to in the past, since for years I've been surrounded by people who falsely believed that they can't afford exceptions or smart pointers or proper serialization, and they have strong, if incorrect, opinions on what's wrong with C++. I believe that this attitude does not belong to Boost. It's possible that I got this wrong. It may be interesting to know how many of the current users of "standalone" Outcome use Boost in "low latency" environments or at all. Do you have an idea?
I don't know. Maybe I pay too little attention to this social aspect. I sympathize with this view. Some people do not understand the philosophy, solutions, and performance implications of parts of C++ and rather than trying to understand them, they try to pretend that C++ is a different language. I admit I sometimes wish we didn't have `shared_ptr`. Do not get me wrong. I realize it is useful, and it is the best tool for doing certain things. But in all the projects I worked for, I have far more often seen it abused or overused, than I have seen used it properly. But if I ask myself if Boost and STD would be better without shared_ptr, I would not be able to say "yes". Some people will overuse it, but others will benefit from using it correctly. Maybe what could have been done better in Outcome is to lay this out clearer in the introduction. That the goal is not to turn C++ into Rust.
2) Clearly, Outcome _does_ want to help pass errors across API boundaries, including in generic contexts. The problem is that
result
compute() noexcept; is very similar to
T compute() throw(E);
(yes, I know exception specifications are enforced dynamically, but that's not what's wrong with them, see the second question here: https://herbsutter.com/2007/01/24/questions-about- exception-specifications/ .)
My reasoning is that if with Outcome you can always return the exact error type you've specified in your static interface, the same approach would work for (perhaps statically-enforced) exception specifications.
Logically, to address this concern you could:
- Demonstrate that there is a major flaw in my analogy, or
- demonstrate that exception specifications could be made practical, including in generic contexts, possibly by using some clever policy-based design, or
- provide an interface that can forward arbitrary errors to the caller.
(I see these as mutually-exclusive).
I can see the point you are making. The analogy to exception specifications
is a bit distracting, because I think there was more than one problem with
it. The goals you describe are mutually exclusive in the most general case.
However it looks like Outcome provides a solution for most of the practical
cases, while leaving the general case unsolved. Boost.Outcome is a set of
tools (rather than just one) and you are expected to choose one that best
solves your particular problem.
Remember that the goals you list and arguments that Herb Sutter draws apply
to a general failure object transportation mechanism present everywhere in
the program, in any program. In contrast, Outcome is not intended to be a
failure reporting mechanism in the entire program: it is either to be used
in isolated places (with particular conditions), or in programs with
extremely harsh execution constraints, where many inconveniences are
expected, including the inability to freely transport arbitrary amount of
failure information in arbitrary form.
So, case 1. You are using in your program a boost::filesystem2 library ("2"
because we hypothetically assume it uses Boost.Outcome to report failures).
Your program can freely use exceptions. But often the inability to write to
the file is not something you have to propagate up, but you know how to
handle it locally. This is not a generic context. I exactly know what
library I am using, and one level up there will be no `result<>`, there
will only be exceptions. In this case the most proper tool from Outcome
toolbox is to create your own type representing the failure code and two
file names, and the usage of `result<>` with your type: `result
On Sat, Feb 3, 2018 at 5:56 AM, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
This is one of the applications of boost.Outcome. It is superior to naked std::error_code because it can additionally guarantee that no failure is accidentally ignored by the programmer.
If all Outcome did was transport values, error_code and exception_ptr, I'd support it.
This is one of the applications of boost.Outcome. It is superior to naked std::error_code because it can additionally guarantee that no failure is accidentally ignored by the programmer.
If all Outcome did was transport values, error_code and exception_ptr, I'd support it.
Last review it was widely agreed that being able to transport arbitrary payload with error_code was a desirable thing. I appreciate that you're not the whole community Emil, but that's the feedback I was given last review, and this library implements what the community asked for last time we were here. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Sat, Feb 3, 2018 at 5:13 PM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
This is one of the applications of boost.Outcome. It is superior to naked std::error_code because it can additionally guarantee that no failure is accidentally ignored by the programmer.
If all Outcome did was transport values, error_code and exception_ptr, I'd support it.
Last review it was widely agreed that being able to transport arbitrary payload with error_code was a desirable thing. I appreciate that you're not the whole community Emil, but that's the feedback I was given last review, and this library implements what the community asked for last time we were here.
exception_ptr _is_ arbitrary payload.
If all Outcome did was transport values, error_code and exception_ptr, I'd support it.
Last review it was widely agreed that being able to transport arbitrary payload with error_code was a desirable thing. I appreciate that you're not the whole community Emil, but that's the feedback I was given last review, and this library implements what the community asked for last time we were here.
exception_ptr _is_ arbitrary payload.
exception_ptr type erases and calls malloc. Its use cannot be elided by the compiler, due to the use of atomics. The `trait::has_error_code_v<E>` mechanism lets you return arbitrary payload without those overhead, but at the cost of extra brittleness introduced into the type system. One can, at any time, further up the call stack do the type erasure then, and regain flexibility by removing from the type returned returned the arbitrary payload. I do this myself in my own code: as a stack unwinds, make type erased the unwind by throwing a custom exception type containing type erased arbitrary payload. All this sounds complex I am sure. But in practice when writing the code it all follows naturally, and the code is clear regarding its intent and purpose. It just is probably a bit new to some folk, that's all. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
std::error code addresses your concern about potential translations of exceptions. It does not address your concern about communicating arbitrary data. But in the end it might turn out to be a reasonable trade-off for low-level libraries that disable exceptions.
I entirely agree. There is a tradeoff here, a balance. I've always been of the opinion that if you want to return arbitrary data, either you encode it into the type system and thus increase coupling, or you type erase it usually using malloc. Exception throws are a very special case of this because the C++ runtime does the type erasure for you. Emil you have pointed out many times that the C++ compiler ought to optimise a try { throw x; } catch(x) { ...} by eliding the throw-try-catch in runtime. And I agree, but none of the major compilers do do that, and for a long list of good legacy reasons. Thus in terms of compiler technology available here and now and today - not a maybe tomorrow - there are plenty of use cases where taking on more inconvenience and ugliness in some parts of your code can be beneficial.
However it looks like Outcome provides a solution for most of the practical cases, while leaving the general case unsolved. Boost.Outcome is a set of tools (rather than just one) and you are expected to choose one that best solves your particular problem.
I was just about to say the same thing, but more pointed. Exception specifications were a bad idea because of the potential presence of unknown code (despite what Peter says, I do and did not find exception specifications to be useless in theory). That makes them a bad idea in *general* i.e. as a rule of thumb. But that says nothing about the local use case where no unknown code may operate, and all code potentially executable is known both to the programmer and the compiler. In that situation, I do believe that in a localised use case, exception specifications can add significant value. Knowing that a piece of code will never, ever see stack unwinding lets you skip handling stack unwinding. Hence `noexcept`. Furthermore, unlike with exception specifications which were unhelpfully checked at runtime, Outcome fails at compile time. That's a very different kettle of fish. You can't successfully compile code if your E types don't have interop specified for them. Again, Herb's article is right that statically checked exception specifications are a bad idea in *general*. But they can be very useful *locally*. Indeed, that's the whole point of the Expected proposal, and WG21 has greenlit that one. It's coming to future C++ whether you like it or not (bar some major surprise).
Remember that the goals you list and arguments that Herb Sutter draws apply to a general failure object transportation mechanism present everywhere in the program, in any program. In contrast, Outcome is not intended to be a failure reporting mechanism in the entire program: it is either to be used in isolated places (with particular conditions), or in programs with extremely harsh execution constraints, where many inconveniences are expected, including the inability to freely transport arbitrary amount of failure information in arbitrary form.
Absolutely agree. I always envisaged Outcome being useful only within a low level layer of code close to the bare metal. Code where a unknown potential ten thousand CPU cycles might matter. Anybody who doesn't care about that kind of stuff doesn't have much need for Outcome.
So, case 1. You are using in your program a boost::filesystem2 library ("2" because we hypothetically assume it uses Boost.Outcome to report failures). Your program can freely use exceptions. But often the inability to write to the file is not something you have to propagate up, but you know how to handle it locally. This is not a generic context. I exactly know what library I am using, and one level up there will be no `result<>`, there will only be exceptions. In this case the most proper tool from Outcome toolbox is to create your own type representing the failure code and two file names, and the usage of `result<>` with your type: `result
`. The question "how this interacts with `result `" is irrelevant, because there will never be such interaction.
Completely agree. And much of the additional "unnecessary" complexity in Outcome is to handle what happens when a programmer is faced with two third party libraries using Outcome/Expected with incommensurate types. Implementing a non-source-code-intrusive mechanism to let the programmer inject what to do comes with messy complexity, some of which Rob pointed out in his review. I don't expect that situation to emerge at all in most code for at least five years. But as a userbase grows, especially after Expected enters C++, then that situation will emerge frequently. And at that point Outcome has them covered (and unlike Expected or anything proposed currently to WG21 I might add).
There are probably more cases, but my point is:
1. Outcome is not meant to be a full failure-handling framework for every part of every program (even though it is technically possible to use it this way).
Absolutely agreed. I felt it was important that it is technically possible to completely replace exceptions with Outcome throughout a large codebase. I would recommend anybody to not do that though.
2. It addresses the specific cases through specific trade-offs where it does not have to address all the issues of the full failure handling framework.
Also absolutely agreed. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 02/04/18 01:53, Niall Douglas via Boost wrote:
*locally*. Indeed, that's the whole point of the Expected proposal, and WG21 has greenlit that one. It's coming to future C++ whether you like it or not (bar some major surprise).
This is an important point. The std::expected proposal is a vocabulary type that has not gathered much practical experience prior to being proposed for standardization, unlike other vocabulary types like std::optional, std::any, and std::variant. Boost.Outcome gives us an opportunity to gather such practical experience, which can then serve as input to the standardization process. If our community goals includes being an incubator for some C++ library proposals, then we should not evaluate Boost candidate libraries strictly on the merit of whether or not I personally can use this library for my own projects.
On Sun, Feb 4, 2018 at 11:46 AM, Bjorn Reese via Boost < boost@lists.boost.org> wrote:
If our community goals includes being an incubator for some C++ library proposals, then we should not evaluate Boost candidate libraries strictly on the merit of whether or not I personally can use this library for my own projects.
Yes our goals include "road testing" libraries that are intended for standardization. But as part of that it is also a goal to gate-keep what is useful to the C++ community. Which is why part of the review process involves getting community "approval" to even put a library in the review queue. So it's not out of the realm to question if something is useful even as late as the review itself. Because after all usability defines who uses the library. And hence what the library ought to do. -- -- Rene Rivera -- Grafik - Don't Assume Anything -- Robot Dreams - http://robot-dreams.net
On 2/4/18 9:46 AM, Bjorn Reese via Boost wrote:
On 02/04/18 01:53, Niall Douglas via Boost wrote:
If our community goals includes being an incubator for some C++ library proposals, then we should not evaluate Boost candidate libraries strictly on the merit of whether or not I personally can use this library for my own projects.
Personally, I would go further. I'm thinking that it can be part of Boost if some number reviewers find it to be useful, it embodies quality code and it follows boost rules, Robert Ramey
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
I have been following these discussions with some bewilderment as much of it has happened since then end of the review period. I made my position clear earlier. I want to add that whatever the outcome I have learned a lot from experimenting with the code and getting to try it out alongside code in which I am interested. Best wishes to all. John ________________________________________ From: Boost [boost-bounces@lists.boost.org] on behalf of Robert Ramey via Boost [boost@lists.boost.org] Sent: 04 February 2018 22:58 To: Bjorn Reese via Boost Cc: Robert Ramey Subject: Re: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018) On 2/4/18 9:46 AM, Bjorn Reese via Boost wrote:
On 02/04/18 01:53, Niall Douglas via Boost wrote:
If our community goals includes being an incubator for some C++ library proposals, then we should not evaluate Boost candidate libraries strictly on the merit of whether or not I personally can use this library for my own projects.
Personally, I would go further. I'm thinking that it can be part of Boost if some number reviewers find it to be useful, it embodies quality code and it follows boost rules, Robert Ramey
_______________________________________________ Unsubscribe & other changes: https://emea01.safelinks.protection.outlook.com/?url=http%3A%2F%2Flists.boost.org%2Fmailman%2Flistinfo.cgi%2Fboost&data=02%7C01%7CJ.P.Fletcher%40aston.ac.uk%7Cdee6a44a0e694cf2289808d56c22d694%7Ca9268f06fe1542e4bcb5b007094bddb4%7C0%7C0%7C636533819245059191&sdata=14VFSUp49VrHRZuAEQvAboYlcIUjrJcBLwAczLFYGGg%3D&reserved=0
_______________________________________________ Unsubscribe & other changes: https://emea01.safelinks.protection.outlook.com/?url=http%3A%2F%2Flists.boost.org%2Fmailman%2Flistinfo.cgi%2Fboost&data=02%7C01%7CJ.P.Fletcher%40aston.ac.uk%7Cdee6a44a0e694cf2289808d56c22d694%7Ca9268f06fe1542e4bcb5b007094bddb4%7C0%7C0%7C636533819245059191&sdata=14VFSUp49VrHRZuAEQvAboYlcIUjrJcBLwAczLFYGGg%3D&reserved=0
On February 3, 2018 8:56:35 AM EST, Andrzej Krzemienski via Boost
2018-02-02 23:26 GMT+01:00 Emil Dotchevski via Boost
: There are probably more cases, but my point is:
1. Outcome is not meant to be a full failure-handling framework for every part of every program (even though it is technically possible to use it this way). 2. It addresses the specific cases through specific trade-offs where it does not have to address all the issues of the full failure handling framework.
This sort of thing would have been useful in the docs to explain how outcome could fit into one's code. -- Rob (Sent from my portable computation device.)
2018-02-06 10:31 GMT+01:00 Rob Stewart via Boost
On February 3, 2018 8:56:35 AM EST, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
2018-02-02 23:26 GMT+01:00 Emil Dotchevski via Boost
: There are probably more cases, but my point is:
1. Outcome is not meant to be a full failure-handling framework for every part of every program (even though it is technically possible to use it this way). 2. It addresses the specific cases through specific trade-offs where it does not have to address all the issues of the full failure handling framework.
This sort of thing would have been useful in the docs to explain how outcome could fit into one's code.
Agreed. Regards, &rzej;
Gavin Lambert wrote:
If you don't like the heap usage, the same concept applies if it returns an optional instead of a pointer (assuming it has a no-fail move).
That's a big assumption, because if you had a suitable nofail moved-from state, you could just default-initialize to it and no factory or private_init_stuff would have been necessary.
Gavin Lambert wrote:
static std::unique_ptr<A> A::create(args...) noexcept
...
If you don't like the heap usage, the same concept applies if it returns an optional instead of a pointer (assuming it has a no-fail move).
With Boost.Outcome, it can be implemented exactly as above (including not letting an invalid instance escape), but *also* indicate a failure reason to the caller, through a simple change to the return type.
Following this train of thought to its logical conclusion is interesting. Suppose we need to copy. That would be unique_ptr<A> A::copy( A const & rhs, error_code & ec ) noexcept; because if construction can fail, copying most likely can fail, too. If we switch to optional, we'll have optional<A> A::create( error_code& ec ) noexcept; optional<A> A::copy( A const& rhs, error_code& ec ) noexcept; and then to result: result<A> A::create() noexcept; result<A> A::copy( A const& rhs ) noexcept; This implies that under this style of programming, a `result` is not copied with its copy constructor.
2018-02-01 2:58 GMT+01:00 Emil Dotchevski via Boost
On Wed, Jan 31, 2018 at 4:52 PM, Vinícius dos Santos Oliveira < vini.ipsmaker@gmail.com> wrote:
2018-01-31 20:25 GMT-03:00 Emil Dotchevski
: Where is the compile time error? Example?
Check the first Outcome tutorials.
This won't compile:
std::cout << (convert(text) / 2) << std::endl;
This will:
OUTCOME_TRY(foo, convert(text)); std::cout << (foo / 2) << std::endl;
Does this guarantee that your program is dealing with the error correctly? Not at all, if the context within which you call convert is not exception-safe, you'll get the same leak you'd get had you used exceptions. The difference is that you'd also get a compile error if you forget to check for errors, which a C++ programmer can't forget to do in this case.
The guarantee you get here is not that "failure-handling will be correct" but that "if you forget to mention explicitly what failure-handling you intend, the compiler will detect this". If 99% of your intended failure-handling is "just skip subsequent instructions" then you will appreciate exception handling, because this is what they do. But if you get accustomed to these defaults, and you are developing the remaining 1% case, and forget to state explicitly how you want to handle you will get a silent error, which compiler will not warn you about. Now, as you get to the lower layers, the ratio is no longer 99% to 1%, and in some cases may be like 60-40, and then the default offered by stack unwinding is not good anymore. The solution provided by Outcome requires you to be explicit about your intentions, so that the compiler can help detect omissions: not every bug, but simple omissions. This follows the philosophy "be explicit about your intentions". The example should have been this. You write code: std::cout << (convert(text) / 2) << std::endl; And compiler protests, that you forgot about failure-handling. And now you get the second chance to decide how you want to handle. Maybe go with default: ``` auto foo = convert(text); int a = foo ? foo.value() : 1; std::cout << a << std::endl; ``` Just output a different message: ``` if (auto foo = convert(text)) std::cout << *foo << std::endl; else std::cerr<< "sorry" << std::endl; ``` Or just propagate up as exceptions do: ``` OUTCOME_TRY(foo, convert(text)); std::cout << (foo / 2) << std::endl; ``` You are forced to make an explicit call. And this is desired in the isolated portions of the program where there are no good defaults for this.
This is similar to how you have to deal with object initialization if you don't use exceptions. Compare:
class foo { bool init_called_; public:
foo(): init_called_(false) { }
result<void> init() { .... init_called_=true; }
result<int> foo() { assert(init_called_); return compute_int(); }
result<void> bar( int x ) { assert(init_called_); //use x } };
.... foo a; OUTCOME_TRY(a.init()); OUTCOME_TRY(x,a.foo()); OUTCOME_TRY(a.bar(x));
Now the C++ version:
class foo { foo() { //initialize, throw on errors }
int foo() { return compute_int(); }
void bar( int x ) { //use x } }
.... foo a; a.bar(a.foo());
Note, I am not only making the point that the Outcome version is more prone to errors and more verbose, I am saying that even if you make no logic errors writing it, the result is a program that is _semantically_ identical to the one a C++ programmer would write using exceptions, complete with the bugs that could creep in if your code is not exception-safe.
Literally, there is no upside to that style of programming.
Your example is unfair because you are not using idioms that come with Outcome. The example with `result` would be: ``` class Foo { private: Foo() public: result<Foo> create(); // uses Foo's constructor, initializes, returns failures through result int foo() { return compute_int(); } void bar( int x ) { //use x } }; // .... OUTCOME_TRY (a, foo::create()); a.bar(a.foo()); ``` That is, provided that your intention is to propagate the failure up, but you may decide that there is a more adequate way of handling failure in your particular situation. For instance, you may have a secondary, safer way of creating the desired object: ``` auto a = foo::create(); if (a.has_error() && backup_may_succeed(a.error())) a = foo::create_backup(); // like a named constructor if (!a) return a.as_failure(); ``` If you are accustomed to stack-unwinding idioms too much, you might forget that "propagate up" is just one way of dealing with failures, and even if it is the best, it is not the only one.
You can't forget to handle the error case. You can look at a function and you'll immediately know if this function can fail or not.
In C++, we use noexcept to mark functions that can not fail.
this. If you want to demonstrate that Rust-style error handling is more robust, you have to have a control, the same real-world large scale
You can't draw conclusions only by looking at anectodal evidence like project
written/designed for using C++ exception handling. Neither of us has a control.
At least you dropped the "axiomatic belief". But I haven't relied on anecdotal evidence. I specifically told you to show me /any/ code on Rust that is as complex as the examples I gave.
Yes, I understand that you like Rust and dislike C++. :)
I am well aware of the difficulties in writing C++ code. Like I said, with some exceptions, there is usually a good reason for that.
It is not about "liking Rust". It is about choosing tools that best address problems at hand. Maybe what you feel is that those cases where any other failure-handling than "propagate up" are so very rare that they do not warrant the addition of the library to Boost. That would be a "quantitative" argument.
C++ allows complexity to just snowball and it is not a opt-in feature. It's always there. Therefore, you always maintain useless state in your head when analysing C++ code. Go ask C programmers what they think about C++ exceptions.
You're making my point, that C++ exception handling is not an opt-in feature, it is inseparable part of the language, central to its object encapsulation model, etc. etc. I know what C programmers think about C++ and about exceptions in particular. Do note that they don't propose C libraries to be added to Boost or C++, they don't want to touch either.
By the way, I have a lot of respect for C and for people who choose it over C++. I've also learned the value of using C-style interfaces between modules in a large scale C++ projects.
So you have to be able to make your point in the abstract, which is very
difficult. It is especially difficult because using either approach, a high quality development team can produce a robust program. But just because Google managed to build a phone using Java, it doesn't mean it was a good choice.
You still ignore the fact that I'm talking about a qualitative (not quantitative) property: more errors go to compile time errors. I have been focusing on such aspect since the beginning of the discussion.
I demonstrated why this is false, but there is no such thing as free lunch. Everything has cost. It is important to keep this in mind when criticising C and C++.
Are you saying you have demonstrated that using a tool like Outcome does not turn some omissions in failure-handling logic into compiler errors"? I do not think that you did. What you demonstrated is that one can use Outcome and still have a bug in one's code. And that is true, but no-one was making this claim.
It's fact that you can't ignore the error because the code will fail to compile. Error cases are explicit.
Yes, using exception handling you can make mistakes that would be less
likely to make otherwise. Also, using C++ you can make a whole lot of mistakes you can't make in Java, that's one reason why some people use Java and not C++, but this doesn't make them good C++ programmers -- nor it means that there is something wrong with C++.
Agreed.
There are very many examples of things being very difficult to express in
C++ while being trivial in another language. There are usually good reasons for this, and there are many examples to the contrary. The ability to use RAII and exceptions to enforce postconditions is one such advantage.
Not sure what kind of postcondition you're talking about this time.
What postcondition, that depends on the design, but the effect is that you're making it impossible for control to reach contexts for which it would be a logic error to execute in case a previous operation failed.
Just note that you do not need exceptions to "enforce" preconditions. Actually term "enforce" is very fuzzy. What you do is to: 1. Signal the inability to satisfy function postconditions, so the contract is "I either satisfy postconditions or throw an exception" 2. Ensure that subsequent operations are not invoked if the current function failed to satisfy its postconditions. But, in the isolated parts of the program, where you do not want to go with a default response to failure, you can handle this in another way: 1. Signal the inability to satisfy postconditions by returning `result` containing failed state. 2. Decide what you want to do next, when a function failed to satisfy postconditions, and have the compiler make sure that a decision has been made by the programmer: no default actions taken by compiler.
If it is class invariants, you can have them without exceptions.
You can have them, but you can't know if they're been established because in case of a failure, control may still reach code that requires them to be in place.
You can if you provide factory functions instead of constructors in your types. Factory functions have other benefits; e.g., they have names, so you do not get this uncertainty "which constructor got actually selected and how it interprets the arguments".
And just one note about RAII: Rust does have RAII. No GC. A lot about the language was inspired by C++ knowledge.
Yes, I get that you love Rust. :)
Choosing Outcome to solve certain problems is not about loving Rust, but about choosing the most adequate tools to the problem at hand.
I'll point out again that in C++ it is impossible to report an error from
a constructor except by throwing, and this is a good thing. I assume you disagree -- so what do you propose instead? It can't be OUTCOME_TRY, can it?
I'm not proposing you to code differently.
My question still stands though. If you don't use exceptions, in C++, how do you protect users from calling member functions on objects that have not been initialized?
You do not have to get such objects if you provide factory functions. You do not have to use constructors directly. And also. You do not use Outcome to replace exceptions in your code. You only use it in places when it is more adequate. Regards, &rzej;
On 02/01/18 01:26, Vinícius dos Santos Oliveira via Boost wrote:
How pushing a "I forgot to do proper error handling" to compile time errors becomes "this is just axiomatic belief and helps no-one"?
During this same review, Andrzej Krzemienski noted an improper exceptions handling of exceptions in the swap function:
Also, it is possible that while reswapping, another exception will be
thrown. In general, you cannot guarantee the roll-back, so maybe it would be cleaner for everyone if you just declared that upon throw from swap, one cannot rely on the state of `result`: it should be reset or destroyed.
Should we remember the variant saga maybe[1][2]? These are not isolated cases. They keep happening.
It's not the exceptions that made variant difficult. It's *the possibility of a failure* at certain points in some cases. You would have the same difficulty defining behavior in these cases with any error reporting mechanism, exceptions or not.
2018-01-31 21:02 GMT-03:00 Andrey Semashev via Boost
It's not the exceptions that made variant difficult. It's *the possibility of a failure* at certain points in some cases. You would have the same difficulty defining behavior in these cases with any error reporting mechanism, exceptions or not.
The way the language features interact makes this hard. It'd be much simpler if move constructors were forbidden to throw (I'm **not** saying this would be a good rule to have in the language). Yet you can write buggy code like the one Andrzej Krzemienski mentioned not long ago. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
On 02/01/18 04:06, Vinícius dos Santos Oliveira wrote:
2018-01-31 21:02 GMT-03:00 Andrey Semashev via Boost
mailto:boost@lists.boost.org>: It's not the exceptions that made variant difficult. It's *the possibility of a failure* at certain points in some cases. You would have the same difficulty defining behavior in these cases with any error reporting mechanism, exceptions or not.
The way the language features interact makes this hard.
Nope. Nothing about exceptions themselves makes variant hard. And the solution existed in Boost long before std::variant and Rust. It's that not everyone was happy with it and the other side was not happy with the alternatives.
It'd be much simpler if move constructors were forbidden to throw (I'm **not** saying this would be a good rule to have in the language).
Right, but as long as you allow move constructor/assignment to fail, you will have to define behavior when the failure happens. *That* is the difficult part, not handling exceptions or error codes or whatever.
It seems like Outcome wants to stay away from Boost. By that I mean that great care has been taken to decouple it from Boost, and I sense that this desire comes from the target audience: the so-called "low latency" crowd wouldn't touch Boost with a 9 foot pole. While it is generally a bad idea to speculate about such things, it seems to me the motivation for submitting Outcome for a Boost review is not to benefit the Boost community; for us the coupling with Boost is not a problem.
While this my response is slightly tangential to the review, I am concerned that your assertion about the "low latency" crowd might provide the wrong impression about the suitability of Boost in these environments. I am very much part of the "low latency" crowd and have been for a number of years in a few environments. I frequently care about single digit nanoseconds. Boost libraries have been individually and intelligently considered for use in all of these environments. While not all decisions make sense when latency requirements are an upper-most concern, your assertion does not fit with my experience. Boost libraries are frequently used and are frequently of enormous utility.
Emil
Neil Groves
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/ mailman/listinfo.cgi/boost
Herewith is my review of the Outcome library.
Design
The design of the library is complex. I'm not sure that a simpler design would be sufficient since it tries to do a lot of things. But therein lies the rub: it smacks of trying to be too many things for too many people, which requires it to be complex.
There are two principle templates, but potentially many different types using them. There are many policies and function templates to tweak the behavior of the library. The latter is somewhat disturbing as one must always include all moving parts for a particular outcome or result type in order to get all of the customizations. In practice, everything is likely to be defined in a single header, so that may not be a problem, but it seems complicated. What's more, some customizations are done using free functions, found via ADL, and others via policy classes. That's discomfiting, at least.
Implementation
I did not have time to examine the implementation.
Documentation
The docs are thorough, though in need of lots of editing. The tutorial is very long and often overly complex. A more typical approach is to use a tutorial as an introduction and to use a separate section of the documentation for more advanced topics and examples.
I generated a lot of comments while working my way through the documentation. I've noted that some examples are poor choices for justifying or illustrating the value of the library. I'll forward them via email rather than include them here as they are extensive (and I didn't even get through the entirety of the documentation).
Usefulness
I did not try to use the library, but I did consider it's usefulness in libraries I've written or used that have the traditional error_code/exception overloads. At the API level, a library writer can avoid massive overloading to account for throwing and non-throwing versions of each operation. However, from the perspective of the library user, that work is irrelevant. Being able to write "if (auto retval = foo()) { use foo.value() } else { react to error }" can make for a newly idiomatic, streamlined style when using such libraries (as opposed to writing "std::error_code error; auto value = foo(error); if (error) { use value } else { use error }"). However, when using the exception throwing overloads, the code gets worse: "foo().value()" vs. "foo()". IME, one makes relatively few calls to libraries of this sort, in any given block of code, so the relative verbosity is probably insignificant.
That result
The design of the library is complex. I'm not sure that a simpler design would be sufficient since it tries to do a lot of things. But therein lies the rub: it smacks of trying to be too many things for too many people, which requires it to be complex.
It really isn't complex, v2 is a marked simplification over v1 as the previous review requested. It is overwhelming to the uninitiated though, so I can see how it might appear to be complex. I would argue that being overwhelmed with the possibilities would be a characteristic of any vocabulary library. Outcome is not a niche purpose library like we usually review here in recent years. Its scope of application has a lot of knock on consequences. Most of the negative feedback posted in this review so far is clearly grounded in concerns over effects on the ecosystem, because if this thing enters Boost and it's broken, then all the stuff built with it is also going to be broken. Such is the burden of vocabulary libraries and language features.
There are two principle templates, but potentially many different types using them. There are many policies and function templates to tweak the behavior of the library. The latter is somewhat disturbing as one must always include all moving parts for a particular outcome or result type in order to get all of the customizations. In practice, everything is likely to be defined in a single header, so that may not be a problem, but it seems complicated.
The policy classes seem to be much more scary than I had anticipated.
Honestly, try implementing your own. You'll be done within ten minutes.
They're very simple.
I agree that the granularity of the header includes needs to be improved
so people can include
What's more, some customizations are done using free functions, found via ADL, and others via policy classes. That's discomfiting, at least.
The cause is the lack of C++ language support for something like https://github.com/ldionne/dyno. A key goal of Outcome is *non-intrusive* rule setting of how third party code ought to interact without requiring modification of third party source code. It unavoidably, given the limitations of the language, relies heavily on ADL customisation points. I will say that where I was able to avoid ADL, I did lift rule setting as much as possible into trait specialisation in its own namespaces so it is clearly delineated and has minimum chance of unexpected interaction. I agree that what has resulted looks like a dog's breakfast, and again it's overwhelming. I could have made everything ADL for purity, but I find ADL worrying. Too much chance of unexpected surprise. Best minimised to the absolute minimum in my book. But the consequence then is that dog's breakfast. I'm not sure what else one can do, other than ditch the non-intrusive interoperation support. And if reviewers did want that gone, it could be removed and that would make things appear much cleaner. The complexity would then, of course, merely be pushed onto the end user having to manually unpack and repack Expected into Outcome etc. Maybe that's better. I had hoped for more feedback from reviewers on whether it would be better or not.
The docs are thorough, though in need of lots of editing. The tutorial is very long and often overly complex. A more typical approach is to use a tutorial as an introduction and to use a separate section of the documentation for more advanced topics and examples.
I generated a lot of comments while working my way through the documentation. I've noted that some examples are poor choices for justifying or illustrating the value of the library. I'll forward them via email rather than include them here as they are extensive (and I didn't even get through the entirety of the documentation).
I look forward to receiving those. But please be aware that I have written a tutorial for Outcome five full times now. I am beginning to realise that a tutorial which makes even a majority happy is not possible given my very finite resources. I need to draw a line at some point for my sanity, there needs to be a life which is not working forever more writing Outcome documentation.
That result
is declared with [[nodiscard]] can be helpful. It means the return value of a function returning a result cannot be ignored. Unfortunately, the compiler doesn't require that the programmer do anything with the result except save it. (A warning might alert the programmer to do something with such a variable, but there's no enforcement.) The same is not documented for outcome, however. Thus, when using outcome, because one might need to convey an exception to the caller, there is no compiler help to ensure the programmer doesn't ignore the return value. Even with that fixed, there's still no library help to ensure the programmer inspects the return value.
The only solution to this that I am aware of is if outcome and result's destructors throw. I felt that unwise, but it could be implemented if reviewers felt it beneficial.
I find that I cannot vote to accept the library into Boost. I am not rejecting it, but I don't expect to make use of the library, so I can't bring myself to vote for its inclusion in Boost. The library seems sound, it seems like it could be useful, and it is reasonably well documented. Inclusion in Boost could provide important exposure that could shape the library before it is proposed for standardization, which Niall intends, but I'm not convinced that's reason enough to accept it into Boost.
Thank you for your review. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On February 3, 2018 8:41:33 PM EST, Niall Douglas via Boost
The design of the library is complex. I'm not sure that a simpler design would be sufficient since it tries to do a lot of things. But therein lies the rub: it smacks of trying to be too many things for too many people, which requires it to be complex.
It really isn't complex, v2 is a marked simplification over v1 as the previous review requested.
That it is less complex than v1 does not mean it isn't complex.
It is overwhelming to the uninitiated though, so I can see how it might appear to be complex.
Surely that is a sign of complexity. That doesn't mean it is unnecessary complexity, however.
I would argue that being overwhelmed with the possibilities would be a characteristic of any vocabulary library.
The same cannot be said for shared_ptr, though it probably could be said of chrono.
Outcome is not a niche purpose library like we usually review here in recent years. Its scope of application has a lot of knock on consequences. Most of the negative feedback posted in this review so far is clearly grounded in concerns over effects on the ecosystem, because if this thing enters Boost and it's broken, then all the stuff built with it is also going to be broken. Such is the burden of vocabulary libraries and language features.
There are two principle templates, but potentially many different types using them. There are many policies and function templates to tweak the behavior of the library. The latter is somewhat disturbing as one must always include all moving parts for a particular outcome or result type in order to get all of the customizations. In practice, everything is likely to be defined in a single header, so that may not be a problem, but it seems complicated.
The policy classes seem to be much more scary than I had anticipated. Honestly, try implementing your own. You'll be done within ten minutes. They're very simple.
Perhaps the solution is basic_result and basic_outcome, with result and outcome as special, simplifying cases. That way, most can use the normal, simpler templates, but those needing all of the knobs and levers for a custom use case can use the basic_* types.
What's more, some customizations are done using free functions, found via ADL, and others via policy classes. That's discomfiting, at least.
The cause is the lack of C++ language support for something like https://github.com/ldionne/dyno. A key goal of Outcome is *non-intrusive* rule setting of how third party code ought to interact without requiring modification of third party source code. It unavoidably, given the limitations of the language, relies heavily on ADL customisation points. I will say that where I was able to avoid ADL, I did lift rule setting as much as possible into trait specialisation in its own namespaces so it is clearly delineated and has minimum chance of unexpected interaction.
It still seems like one or the other would be better.
I agree that what has resulted looks like a dog's breakfast, and again it's overwhelming. I could have made everything ADL for purity, but I find ADL worrying. Too much chance of unexpected surprise. Best minimised to the absolute minimum in my book. But the consequence then is that dog's breakfast. I'm not sure what else one can do, other than ditch the non-intrusive interoperation support. And if reviewers did want that gone, it could be removed and that would make things appear much cleaner.
I'm not sure what you're implying as an alternative.
The complexity would then, of course, merely be pushed onto the end user having to manually unpack and repack Expected into Outcome etc. Maybe that's better. I had hoped for more feedback from reviewers on whether it would be better or not.
I have no idea to what you refer, so I can't compare the two.
The docs are thorough, though in need of lots of editing. The tutorial is very long and often overly complex. A more typical approach is to use a tutorial as an introduction and to use a separate section of the documentation for more advanced topics and examples.
I generated a lot of comments while working my way through the documentation. I've noted that some examples are poor choices for justifying or illustrating the value of the library. I'll forward them via email rather than include them here as they are extensive (and I didn't even get through the entirety of the documentation).
I look forward to receiving those. But please be aware that I have written a tutorial for Outcome five full times now. I am beginning to realise that a tutorial which makes even a majority happy is not possible given my very finite resources. I need to draw a line at some point for my sanity, there needs to be a life which is not working forever more writing Outcome documentation.
Good documentation can be difficult, and writing it can be all but impossible for many.
That result
is declared with [[nodiscard]] can be helpful. It means the return value of a function returning a result cannot be ignored. Unfortunately, the compiler doesn't require that the programmer do anything with the result except save it. (A warning might alert the programmer to do something with such a variable, but there's no enforcement.) The same is not documented for outcome, however. Thus, when using outcome, because one might need to convey an exception to the caller, there is no compiler help to ensure the programmer doesn't ignore the return value. Even with that fixed, there's still no library help to ensure the programmer inspects the return value. The only solution to this that I am aware of is if outcome and result's destructors throw. I felt that unwise, but it could be implemented if reviewers felt it beneficial.
For those that actually run debug builds, an assertion would work nicely. -- Rob (Sent from my portable computation device.)
On Mon, Feb 5, 2018 at 4:29 PM, Rob Stewart via Boost wrote: Perhaps the solution is basic_result and basic_outcome, with result and
outcome as special, simplifying cases. That way, most can use the normal,
simpler templates, but those needing all of the knobs and levers for a
custom use case can use the basic_* types. This piles up even more complexity. Here is how a user header file would
look like with what I think is the correct interface for this kind of
library:
namespace boost { template <class T> class result; }
boost::result<int> foo();
It is overwhelming to the uninitiated though, so I can see how it might appear to be complex.
Surely that is a sign of complexity. That doesn't mean it is unnecessary complexity, however.
Not at all. Floating point numbers are simple right? But they wouldn't be to someone who has never seen one before. They'd be overwhelming initially until you wrapped your head around them and stopped asking, "why can't this just be fixed point integer arithmetic?"
I would argue that being overwhelmed with the possibilities would be a characteristic of any vocabulary library.
The same cannot be said for shared_ptr, though it probably could be said of chrono.
I remember enormous confusion back in the day about shared_ptr vs unique_ptr and what was wrong with auto_ptr anyway? I remember Dave losing his temper with the argument "nothing is wrong with auto_ptr, don't see any need for any of this totally unnecessary additional complexity". Yet all that is gone now. One day it'll be the same with Outcome/Expected. It'll just be there, and widely understood. People will use it appropriately without even thinking about it.
Perhaps the solution is basic_result and basic_outcome, with result and outcome as special, simplifying cases. That way, most can use the normal, simpler templates, but those needing all of the knobs and levers for a custom use case can use the basic_* types.
Already agreed to this during the review. Tracked by https://github.com/ned14/outcome/issues/110 Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On February 6, 2018 4:19:16 AM EST, Niall Douglas via Boost
It is overwhelming to the uninitiated though, so I can see how it might appear to be complex.
Surely that is a sign of complexity. That doesn't mean it is unnecessary complexity, however.
Not at all.
Floating point numbers are simple right? But they wouldn't be to someone who has never seen one before. They'd be overwhelming initially until you wrapped your head around them and stopped asking, "why can't this just be fixed point integer arithmetic?"
You're saying Outcome isn't complex, it's just overwhelming at first, which is rather a contradiction. Floating point types are quite simple on the surface. Using them properly is not always so easy. Indeed, they can be misused rather easily, which is similar to my concern with not actually forcing inspection of the Outcome types.
I would argue that being overwhelmed with the possibilities would be a characteristic of any vocabulary library.
The same cannot be said for shared_ptr, though it probably could be said of chrono.
Please don't ignore my statements that the complexity may well be necessary.
I remember enormous confusion back in the day about shared_ptr vs unique_ptr and what was wrong with auto_ptr anyway? I remember Dave losing his temper with the argument "nothing is wrong with auto_ptr, don't see any need for any of this totally unnecessary additional complexity".
Yet all that is gone now. One day it'll be the same with Outcome/Expected. It'll just be there, and widely understood. People will use it appropriately without even thinking about it.
That may well be, but don't ignore Charley's point about std::error_code in recent discussions. Correct usage is not always as obvious as early proponents of something suppose. -- Rob (Sent from my portable computation device.)
Yet all that is gone now. One day it'll be the same with Outcome/Expected. It'll just be there, and widely understood. People will use it appropriately without even thinking about it.
That may well be, but don't ignore Charley's point about std::error_code in recent discussions. Correct usage is not always as obvious as early proponents of something suppose.
One thing I've learned in 25 years of providing and maintaining open source is that you can never anticipate what weird things end users end up doing. Like sticking a heavily modified edition of your code in a mission critical role e.g. a quarter of all ATMs in Britain. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2018-02-03 23:32 GMT+01:00 Rob Stewart via Boost
That result
is declared with [[nodiscard]] can be helpful. It means the return value of a function returning a result cannot be ignored. Unfortunately, the compiler doesn't require that the programmer do anything with the result except save it. (A warning might alert the programmer to do something with such a variable, but there's no enforcement.)
Let me expand on this a bit. I have the following program where I save the result, but not inspect it: ``` #include "outcome.hpp" namespace out = OUTCOME_V2_NAMESPACE; out::result<void> fun() { return out::success(); } int main() { auto r = fun(); } ``` When I compile with clang, with -Wall -Wextra -Werror, I get an error saying, "unused variable 'r'". See online examle: https://wandbox.org/permlink/WE7dK6r5XCIxJ83f When I compile with GCC, I do not get this warning. But in my work, w compile programs with GCC, but still use clang for the static analysis pass, and nonetheless this bug would be detected. Of course, you could still overcome all the safety precautions and trigger an UB: ``` #include "outcome.hpp" namespace out = OUTCOME_V2_NAMESPACE; out::result<void> fun() { return out::success(); } int main() { auto r = fun(); (void)r; } ``` But I would not expect protection from such conscious attempts of overcoming safety measures.
The same is not documented for outcome, however. Thus, when using outcome, because one might need to convey an exception to the caller, there is no compiler help to ensure the programmer doesn't ignore the return value.
When I test the following program, it does tell me that I am not inspecting the returned outcome: ``` #include "outcome.hpp" namespace out = OUTCOME_V2_NAMESPACE; out::outcome<int> fun() { return out::success(1); } int main() { fun(); } ``` So, you probably mean something else than I think. Could you illustrate your concerns with an example? Regards, &rzej;
On February 5, 2018 3:43:49 AM EST, Andrzej Krzemienski via Boost
2018-02-03 23:32 GMT+01:00 Rob Stewart via Boost
: That result
is declared with [[nodiscard]] can be helpful. It means the return value of a function returning a result cannot be ignored. Unfortunately, the compiler doesn't require that the programmer do anything with the result except save it. (A warning might alert the programmer to do something with such a variable, but there's no enforcement.) Let me expand on this a bit. I have the following program where I save the result, but not inspect it:
``` #include "outcome.hpp" namespace out = OUTCOME_V2_NAMESPACE;
out::result<void> fun() { return out::success(); }
int main() { auto r = fun(); } ```
When I compile with clang, with -Wall -Wextra -Werror, I get an error saying, "unused variable 'r'". See online examle: https://wandbox.org/permlink/WE7dK6r5XCIxJ83f
When I compile with GCC, I do not get this warning. But in my work, w compile programs with GCC, but still use clang for the static analysis pass, and nonetheless this bug would be detected.
IOW, one must use certain tools and options to get the protection. I think the library could, at least, assert in debug builds when a result or outcome is not inspected.
Of course, you could still overcome all the safety precautions and trigger an UB:
``` #include "outcome.hpp" namespace out = OUTCOME_V2_NAMESPACE;
out::result<void> fun() { return out::success(); }
int main() { auto r = fun(); (void)r; } ```
But I would not expect protection from such conscious attempts of overcoming safety measures.
I wouldn't expect protection then either.
The same is not documented for outcome, however. Thus, when using outcome, because one might need to convey an exception to the caller, there is no compiler help to ensure the programmer doesn't ignore the return value.
When I test the following program, it does tell me that I am not inspecting the returned outcome:
``` #include "outcome.hpp" namespace out = OUTCOME_V2_NAMESPACE;
out::outcome<int> fun() { return out::success(1); }
int main() { fun(); } ```
So, you probably mean something else than I think. Could you illustrate your concerns with an example?
I'm glad to see that, but note that I wrote that outcome was not documented as having that behavior. -- Rob (Sent from my portable computation device.)
2018-02-06 1:00 GMT+01:00 Rob Stewart via Boost
On February 5, 2018 3:43:49 AM EST, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
Let me expand on this a bit. I have the following program where I save the result, but not inspect it:
``` #include "outcome.hpp" namespace out = OUTCOME_V2_NAMESPACE;
out::result<void> fun() { return out::success(); }
int main() { auto r = fun(); } ```
When I compile with clang, with -Wall -Wextra -Werror, I get an error saying, "unused variable 'r'". See online examle: https://wandbox.org/permlink/WE7dK6r5XCIxJ83f
When I compile with GCC, I do not get this warning. But in my work, w compile programs with GCC, but still use clang for the static analysis pass, and nonetheless this bug would be detected.
IOW, one must use certain tools and options to get the protection.
I think the library could, at least, assert in debug builds when a result or outcome is not inspected.
Yes, that sounds like a useful addition. Regards, &rzej;
When I compile with clang, with -Wall -Wextra -Werror, I get an error saying, "unused variable 'r'". See online examle: https://wandbox.org/permlink/WE7dK6r5XCIxJ83f
IOW, one must use certain tools and options to get the protection.
If you're not using clang-tidy in your project, then you ought to start. I've managed to raise it on an ancient VS2008 codebase. It surprisingly works.
I think the library could, at least, assert in debug builds when a result or outcome is not inspected.
What does "not inspected" mean? In C and C++, historically when the programmer wishes to signal that they are intentionally throwing away a result, they write: ``` extern result<int> foo(); ... // I don't care if this succeeds or fails (void) foo(); ``` So okay, perhaps people should do that. But this is something people definitely do: ``` result<int> r = foo(); if(r) { x = r.value(); ... } ``` So now we need to make the explicit boolean operator become non-const in order to get it to set an internal flag so that the destructor can trip an assert on uninspected destruction. Now, I don't mind doing this, indeed I've left a free status bit for this sort of thing in the future. But are: - Non-const boolean operator - Non-const .has_value(), .has_error() etc - Non-const .value(), .error() etc observers ... a price worth paying for asserting an uninspected result, and this includes breaking the cast-to-void idiom? One could of course const cast off any const-ness, and flip the inspected bit on any form of inspection. But I personally think there is a much better way that what you propose Rob, and that's to use the construction hooks to log the construction of result/outcome in debug mode. This is what I do in my own code. See https://github.com/ned14/afio/blob/master/include/afio/v2.0/config.hpp#L321. This lets one detect, much more easily and without breaking behaviour semantics, unintentionally uninspected outcome/result. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2018-02-06 10:15 GMT+01:00 Niall Douglas via Boost
When I compile with clang, with -Wall -Wextra -Werror, I get an error saying, "unused variable 'r'". See online examle: https://wandbox.org/permlink/WE7dK6r5XCIxJ83f
IOW, one must use certain tools and options to get the protection.
If you're not using clang-tidy in your project, then you ought to start. I've managed to raise it on an ancient VS2008 codebase. It surprisingly works.
I think the library could, at least, assert in debug builds when a result or outcome is not inspected.
What does "not inspected" mean?
In C and C++, historically when the programmer wishes to signal that they are intentionally throwing away a result, they write:
``` extern result<int> foo(); ... // I don't care if this succeeds or fails (void) foo(); ```
So okay, perhaps people should do that. But this is something people definitely do:
``` result<int> r = foo(); if(r) { x = r.value(); ... } ```
So now we need to make the explicit boolean operator become non-const in order to get it to set an internal flag so that the destructor can trip an assert on uninspected destruction.
Now, I don't mind doing this, indeed I've left a free status bit for this sort of thing in the future. But are:
- Non-const boolean operator - Non-const .has_value(), .has_error() etc - Non-const .value(), .error() etc observers
... a price worth paying for asserting an uninspected result, and this includes breaking the cast-to-void idiom?
One could of course const cast off any const-ness, and flip the inspected bit on any form of inspection. But I personally think there is a much better way that what you propose Rob, and that's to use the construction hooks to log the construction of result/outcome in debug mode.
What you could do is to offer to compile your library in a special debugging mode. And only in this mode would you offer the additional run-time checks. In this mode you could just add a mutable member to track whether the error has been read or not. Regards, &rzej;
On February 6, 2018 4:15:14 AM EST, Niall Douglas via Boost
When I compile with clang, with -Wall -Wextra -Werror, I get an error saying, "unused variable 'r'". See online examle: https://wandbox.org/permlink/WE7dK6r5XCIxJ83f
IOW, one must use certain tools and options to get the protection.
If you're not using clang-tidy in your project, then you ought to start. I've managed to raise it on an ancient VS2008 codebase. It surprisingly works.
While that may be true, the library can't be designed with only that tool in mind to find problems in code.
I think the library could, at least, assert in debug builds when a result or outcome is not inspected.
What does "not inspected" mean?
Creating a variable and then doing nothing more with it.
In C and C++, historically when the programmer wishes to signal that they are intentionally throwing away a result, they write:
``` extern result<int> foo(); ... // I don't care if this succeeds or fails (void) foo(); ```
So okay, perhaps people should do that. But this is something people definitely do:
``` result<int> r = foo(); if(r) { x = r.value(); ... } ```
So now we need to make the explicit boolean operator become non-const in order to get it to set an internal flag so that the destructor can trip an assert on uninspected destruction.
The flag can be mutable.
Now, I don't mind doing this, indeed I've left a free status bit for this sort of thing in the future. But are:
- Non-const boolean operator - Non-const .has_value(), .has_error() etc - Non-const .value(), .error() etc observers
... a price worth paying for asserting an uninspected result, and this includes breaking the cast-to-void idiom?
The library should do what it can to ensure proper error handling. After all, one of the benefits of exceptions is that you cannot ignore them, so this library should strive for the same.
One could of course const cast off any const-ness, and flip the inspected bit on any form of inspection. But I personally think there is a much better way that what you propose Rob, and that's to use the construction hooks to log the construction of result/outcome in debug mode.
This is what I do in my own code. See https://github.com/ned14/afio/blob/master/include/afio/v2.0/config.hpp#L321. This lets one detect, much more easily and without breaking behaviour semantics, unintentionally uninspected outcome/result.
How does that help since there are no hooks for when value(), error(), etc. are called? -- Rob (Sent from my portable computation device.)
What does "not inspected" mean?
Creating a variable and then doing nothing more with it.
Sometimes that's unavoidable e.g. in a stack unwind. I've agreed to the idea though. Tracked at https://github.com/ned14/outcome/issues/134.
This is what I do in my own code. See https://github.com/ned14/afio/blob/master/include/afio/v2.0/config.hpp#L321. This lets one detect, much more easily and without breaking behaviour semantics, unintentionally uninspected outcome/result.
How does that help since there are no hooks for when value(), error(), etc. are called?
Of course there are hooks for those. Just use a custom policy. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On February 8, 2018 1:42:25 PM EST, Niall Douglas via Boost
What does "not inspected" mean?
Creating a variable and then doing nothing more with it.
Sometimes that's unavoidable e.g. in a stack unwind.
Of course, but then std::uncaught_exception(s) can control whether to assert.
How does that help since there are no hooks for when value(), error(), etc. are called?
Of course there are hooks for those. Just use a custom policy.
OK. I didn't recall those hooks when I wrote that. -- Rob (Sent from my portable computation device.)
What does "not inspected" mean?
Creating a variable and then doing nothing more with it.
Sometimes that's unavoidable e.g. in a stack unwind.
Of course, but then std::uncaught_exception(s) can control whether to assert.
Stack unwinding was but one example. There are many other real world use cases where it's absolutely right to not examine state of a returning object. This is why I said I don't personally see much usefulness for such a thing. However, I've agreed to implement it anyway. It might be useful in some codebases, simple ones. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Mon, Feb 5, 2018 at 6:00 PM, Rob Stewart via Boost wrote: I think the library could, at least, assert in debug builds when a result
or outcome is not inspected. That can be problematic. Suppose you have one that isn't inspected
(because the library you are calling uses outcome for return values) and an
exception is thrown (say, from a different library). Is it incorrect to
not have inspected it?
--
Nevin ":-)" Liber mailto:nevin@eviloverlord.com +1-847-691-1404
2018-02-08 18:56 GMT+01:00 Nevin Liber via Boost
On Mon, Feb 5, 2018 at 6:00 PM, Rob Stewart via Boost < boost@lists.boost.org
wrote:
I think the library could, at least, assert in debug builds when a result or outcome is not inspected.
That can be problematic. Suppose you have one that isn't inspected (because the library you are calling uses outcome for return values) and an exception is thrown (say, from a different library). Is it incorrect to not have inspected it? http://lists.boost.org/mailman/listinfo.cgi/boost
Another such case that does not involve exceptions. Suppose I have a
function template that can convert any T to any U, such as lexical_cast but
with different interface:
```
template
participants (24)
-
Andrey Semashev
-
Andrzej Krzemienski
-
Bjorn Reese
-
charleyb123 .
-
Daniel James
-
Daniela Engert
-
degski
-
Emil Dotchevski
-
Fletcher, John P
-
Gavin Lambert
-
Glen Fernandes
-
Jonathan Müller
-
Miguel Ojeda
-
Neil Groves
-
Nevin Liber
-
Niall Douglas
-
Paul A. Bristow
-
Peter Dimov
-
Rene Rivera
-
Rob Stewart
-
Robert Ramey
-
Steven Watanabe
-
Vinnie Falco
-
Vinícius dos Santos Oliveira