
2018-02-02 23:26 GMT+01:00 Emil Dotchevski via Boost <boost@lists.boost.org> :
Consider the following library:
template <class T,class E> class result; //contains variant<T,E>
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<T,E> 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<T, FilesystemFailureType>`. The question "how this interacts with `result<T, SomeOtherType>`" is irrelevant, because there will never be such interaction. Case 2. We are dealing with the number of low-level libraries, system libraries, which are so low-level that they cannot decide whether their failure will also be treated as a failure in your program. This is a generic context. Luckily it is acceptable by you that std::error_code is sufficient to carry the information about the failure. You can accept that there will be no other context available in the failure objects. In this case, you would use `result<T>` (second argument defaulted). This can handle arbitrary number of libraries. Case 3. In some higher level program you are using a number of low level libraries which report failures through `result<T>` (Case 2) but additionally other low level libraries throw exceptions. But you are writing a task processing framework, and in this framework using exceptions would be clumsy, because all of the time you have to make decisions if this condition affects task processing or not. In such case you go with outcome<T>, which can store both `std::error_code` and `std::exception_ptr`. 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. Regards, &rzej;