[outcome] "sea of noexcept, islands of throwing"
Niall, I moved this to another thread, as I believe it is quite independent of the documentation review. I am trying to understand the concept of "sea of noexcept, islands of throwing", and its significance. First, is this programming technique the primary motivation for Boost.Outcome? In the docs, I only saw a small mention of it, but from your replies it looks like this is an important part of the library. But I might be wrong. This "sea and islands" are quite similar to what is described in P0364R0 ("Report on Exception Handling Lite (Disappointment) from SG14"). The authors argue that if the exception control flow is indicated explicitly, the compiler has a better chance of performing optimizations. I understand that your goal (with the Outcome library) is to offer something more modest, but implementable in C++. You are saying, that the C++ compiler can implement similar optimizations, provided that all the try-catch-throw flows are confined to one translation unit. Did I get it right? The authors of P0364R0 claim that in some business domains, like video games, if you encounter a failure to acquire resources, you might as well terminate the program. For this reason RAII is not really that important there: you want to shut down the app rather than clean up and continue. Is it also what you are saying? Do you also intend to support something like 'panic mode' in Rust? I do not know Rust, but I understand that this 'panic mode' is shut down the program but with resource cleanup. Sorry, my questions are quite random. But I am trying to understand the idea. Regards, &rzej;
Niall, I moved this to another thread, as I believe it is quite independent of the documentation review.
I am trying to understand the concept of "sea of noexcept, islands of throwing", and its significance.
First, is this programming technique the primary motivation for Boost.Outcome? In the docs, I only saw a small mention of it, but from your replies it looks like this is an important part of the library. But I might be wrong.
It is one of the primary motivations. I've been writing "sea of noexcept" code for some time now. I've been very pleased with its balance between letting you break into STL usage inside the implementation of some function, yet code calling that extern function can be compiled with C++ exceptions disabled. I just came out of a contract working in the games industry and that design pattern gained a lot of adherents quickly because basically, it's very useful and doesn't disturb existing practice.
This "sea and islands" are quite similar to what is described in P0364R0 ("Report on Exception Handling Lite (Disappointment) from SG14"). The authors argue that if the exception control flow is indicated explicitly, the compiler has a better chance of performing optimizations.
I understand that your goal (with the Outcome library) is to offer something more modest, but implementable in C++. You are saying, that the C++ compiler can implement similar optimizations, provided that all the try-catch-throw flows are confined to one translation unit. Did I get it right?
I think that SG14 paper is now a little out of date. Members of SG14 have tried on many occasions to prove C++ exceptions are bad in real world code bases. One fellow even went through his financial trading program replacing all his error codes with exception throws, recompiled with the expectation it would be horrendous, and surprise surprise he could find no statistically significant performance difference. Some on SG14 still believe exceptions are bad for performance no matter what, but I think given the accumulating evidence that claim needs to be qualified with "for certain hot code paths". And I think that claim is fair, it's totally possible that for some hot code path exception throws will murder performance. Equally, they might make it much quicker. Depends on the hot code path. The main goal for Outcome is to make it *easier* to write code which is C++ exceptions disabled through the trunk of the tree, but branches and especially leaves may use exceptions extensively. Outcome just wraps up the existing facilities in C++ 11 (error_code, exception_ptr) into a more convenient package. It does no magic, and I would expect code using Outcome to not become magically faster than code not using Outcome. What I do claim and hope and expect is that code using Outcome is not *slower* than code not using Outcome, both in compile times and in runtime performance. I've thrown everything I know at ensuring both. In terms of whether your particular application *ought* to not throw exceptions in its "trunk", that really depends on the application. I think for large, complex C++ codebases there is a very strong argument that exceptions ought to never be thrown across an extern boundary. Not doing this reduces the number of possible execution paths through a large code base exponentially. That means fewer surprises when someone modifies some code five years from now in a ten million line code base.
The authors of P0364R0 claim that in some business domains, like video games, if you encounter a failure to acquire resources, you might as well terminate the program. For this reason RAII is not really that important there: you want to shut down the app rather than clean up and continue. Is it also what you are saying?
Not at all. If anything, the opposite. I've never been fond of ignoring errors, not ever, even in high performance code.
Do you also intend to support something like 'panic mode' in Rust? I do not know Rust, but I understand that this 'panic mode' is shut down the program but with resource cleanup.
I leave that entirely up to the programmer using Outcome. Outcome's purpose is to provide an ultra ultra light weight common error handling infrastructure that loses no information. So if some random third party library linked far far away throws some custom exception, Outcome based code will transmit that totally unknown and unknowable error state through code which has no knowledge of it without losing information.
Sorry, my questions are quite random. But I am trying to understand the idea.
It's no problem. Most on the committee think exceptions are the correct way of reporting errors because they clutter up code the least. I think that's right for a range of code bases. I also think that's wrong for a range of code bases. Depends on the code base. In particular, for some code bases you really must *require* the programmer to explicitly code what happens when an error occurs. What you see before you is always taken more seriously by other programmers than automatic execution paths chosen silently for you by the compiler. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-01-17 13:27 GMT+01:00 Niall Douglas
It's no problem. Most on the committee think exceptions are the correct way of reporting errors because they clutter up code the least. I think that's right for a range of code bases. I also think that's wrong for a range of code bases. Depends on the code base. In particular, for some code bases you really must *require* the programmer to explicitly code what happens when an error occurs. What you see before you is always taken more seriously by other programmers than automatic execution paths chosen silently for you by the compiler.
Thanks for a detailed reply. This is what I understood so far. One of the main goals of Boost.Outcome (apparently not the only one) is to make it easy for you to go from an island of exceptions into the sea of noexcept (and probably the other way around also). The reason you would encourage the explicit control path over the implicit one (where exceptions are thrown around) is for the correctness of the program -- not necessarily performance. Did I summarize it correctly? If yes, there comes another question. One of the main reasons exceptions were introduced in the first place was to be able to report failures from functions that cannot use the return value for this purpose: constructors, conversion operators, and other operators. How do you deal with these functions in the sea of noexcept? Regards, &rzej;
Hi,
If yes, there comes another question. One of the main reasons exceptions were introduced in the first place was to be able to report failures from functions that cannot use the return value for this purpose: constructors, conversion operators, and other operators. How do you deal with these functions in the sea of noexcept?
I would argue that failing to create an object is breaking so many invariants that this awards an exception. On the other hand, we do have an example of objects which can be constructed in a failed state, e.g. std::ifstream which then come with their own error reporting mechanism. Best, Oswin
If yes, there comes another question. One of the main reasons exceptions were introduced in the first place was to be able to report failures from functions that cannot use the return value for this purpose: constructors, conversion operators, and other operators. How do you deal with these functions in the sea of noexcept?
I would argue that failing to create an object is breaking so many invariants that this awards an exception. On the other hand, we do have an example of objects which can be constructed in a failed state, e.g. std::ifstream which then come with their own error reporting mechanism.
It's definitely a rich source of bugs to handle constructors which throw correctly. It's safer to make constructors very simple forwarding only affairs and construct their contents from a static init function such that the only way you can publicly construct such a class is exclusively via its static init function. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Thanks for a detailed reply. This is what I understood so far. One of the main goals of Boost.Outcome (apparently not the only one) is to make it easy for you to go from an island of exceptions into the sea of noexcept (and probably the other way around also).
Correct. Outcome basically provides a less clunky to use and hard coded
std::expected
The reason you would encourage the explicit control path over the implicit one (where exceptions are thrown around) is for the correctness of the program -- not necessarily performance.
Did I summarize it correctly?
Not quite. "is for the correctness of the program" => "is to make the programmer explicitly state in code the correctness of the program" For some code bases e.g. a filing system it is really important the programmer explicitly writes out what will happen when something goes wrong. For example, originally NTFS was written using SEH and that turned into a maintenance nightmare. So (I'm told) they gutted the SEH implementation and put back C error codes because they force people changing the code to think deep and hard about handling errors at the point of changing the code. I've no idea if that's true or not, but a NT kernel programmer working at Microsoft told me that at a conference once. It sounds plausible.
If yes, there comes another question. One of the main reasons exceptions were introduced in the first place was to be able to report failures from functions that cannot use the return value for this purpose: constructors, conversion operators, and other operators. How do you deal with these functions in the sea of noexcept?
I assume you really mean here "what about classes in extern space?".
Outcome has no role in those. You'll need to make your constructors all
thin forward only affairs, noexcept and private, and provide a static
init function to construct them which does all the real work. Same as
since forever in C++. For example in AFIO v2 to open a file returning an
afio::file_handle:
static result
2017-01-17 18:57 GMT+01:00 Niall Douglas
Thanks for a detailed reply. This is what I understood so far. One of the main goals of Boost.Outcome (apparently not the only one) is to make it easy for you to go from an island of exceptions into the sea of noexcept (and probably the other way around also).
Correct. Outcome basically provides a less clunky to use and hard coded std::expected
> wrapped up with convenience macros and other ease of use bits. That's all. The reason you would encourage the explicit control path over the implicit one (where exceptions are thrown around) is for the correctness of the program -- not necessarily performance.
Did I summarize it correctly?
Not quite.
"is for the correctness of the program" => "is to make the programmer explicitly state in code the correctness of the program"
For some code bases e.g. a filing system it is really important the programmer explicitly writes out what will happen when something goes wrong. For example, originally NTFS was written using SEH and that turned into a maintenance nightmare. So (I'm told) they gutted the SEH implementation and put back C error codes because they force people changing the code to think deep and hard about handling errors at the point of changing the code. I've no idea if that's true or not, but a NT kernel programmer working at Microsoft told me that at a conference once. It sounds plausible.
If yes, there comes another question. One of the main reasons exceptions were introduced in the first place was to be able to report failures from functions that cannot use the return value for this purpose: constructors, conversion operators, and other operators. How do you deal with these functions in the sea of noexcept?
I assume you really mean here "what about classes in extern space?".
Outcome has no role in those. You'll need to make your constructors all thin forward only affairs, noexcept and private, and provide a static init function to construct them which does all the real work. Same as since forever in C++. For example in AFIO v2 to open a file returning an afio::file_handle:
static result
file(path_type _path, mode _mode = mode::read, creation _creation = creation::open_existing, caching _caching = caching::all, flag flags = flag::none) noexcept; No surprises there.
For operators and conversion, the chances are if those are important for your code base in an extern pan-TU space, then the sea of noexcept design pattern is a bad fit for your code base. Sea of noexcept suits code whose classes abstract out operating system specific implementation e.g. a windowing framework. It definitely would not and does not suit code which say parses text and where parsed things need to be compared and converted.
I'm really not claiming sea of noexcept suits most code, rather it suits some types of code, particularly very large code bases where you need an insane focus on worst case performance i.e. you can't allow C++ to call malloc for you. Let me put this another way: if your classes are easily wrapped into Microsoft COM objects because they use so few modern C++ features like allocating memory :), sea of noexcept is definitely for you. If the limitations of a COM object would make your classes infeasible, sea of noexcept is a bad fit for your code.
(For reference, a COM object is basically a C++ class with only virtual functions returning a HRESULT and only accepting integral types or other COM objects. You initialise one with a static Init() function, and they are always reference counted as if by shared_ptr. Very straightforward and very easily implemented in C too)
Does that make more sense?
Let's see. So now, I my understanding is the following. If a programmer decides that certain section of the program (the see of noexcept) requires explicit control flow when it comes to handling "disappointments", Boost.Outcome is just the tool he needs because it offers a set of convenient types, tailored for optimum performance + convinience operators + a dedicated control flow statement hidden under macro BOOST_OUTCOME_TRY. did I get it right? Regards, &rzej;
Does that make more sense?
Let's see. So now, I my understanding is the following. If a programmer decides that certain section of the program (the see of noexcept) requires explicit control flow when it comes to handling "disappointments", Boost.Outcome is just the tool he needs because it offers a set of convenient types, tailored for optimum performance + convinience operators + a dedicated control flow statement hidden under macro BOOST_OUTCOME_TRY.
did I get it right?
Basically yes. If a programmer is feeling that they want to use
something like expected
participants (3)
-
Andrzej Krzemienski
-
Niall Douglas
-
Oswin Krause