2017-01-16 16:50 GMT+01:00 Niall Douglas
Many thanks in advance for any feedback received.
I intend to review the docs again. So far I had only a glimpse at the initial page, and it really looks good. I was now able to grasp the idea of the library in less than a minute.
Cool. The simplest primitives are often the hardest to convey the use case for.
One thing I wanted to bring up immediately, is not really related to your library but to a detail in the example. It uses `noexcept` to illustrate that the function does not throw itself:
``` bo::outcome<int> getConfigParam(std::string name) noexcept; ```
I strongly believe that this is the wrong thing to do (or at least controversial) to annotate a function that can fail-but-not-throw as noexcept. It is more in the spirit of noexcept intentions to indicate a no-fail guarantee rather than the no-throw guarantee.
I have provided the justification for this claim in the following post: https://akrzemi1.wordpress.com/2014/04/24/noexcept-what-for/
And conversely, only because you guarantee that a given function never throws, it does not immediately imply that a function should be declared as noexcept.
I think that your example would not suffer if the noexcept is removed. It still does the good job of illustrating the intent. But you would avoid certain controversies that might divert the reader's attention from the main point.
Most of what you write in your blog I would generally agree with and you saved Outcome from a design mistake in https://akrzemi1.wordpress.com/2014/12/02/a-gotcha-with-optional/.
But regarding https://akrzemi1.wordpress.com/2014/04/24/noexcept-what-for/ I think you're wide of the mark, and actually because I recognise that your opinion is not uncommon, it's one of the reasons why the tutorial bangs on about error handling in C++ in such lengthy detail plus presenting the error handling design patterns in does in such hand wavy and superlative language terms. Indeed I *am* trying to sell something, and it is the enormous value of making all your extern APIs noexcept which I call "sea of noexcept, islands of throwing" in the tutorial.
Quoting your blog post:
Why do you need to know?
Is this an important information for you if a given function may throw or not? If so, why? Some possible answers include the following:
1. Because if it throws and I do not catch it, std::terminate will be called and I do not want this to happen.
2. Because I need this function to provide no-throw exception safety guarantee.
3. Because this may allow certain compiler optimizations.
I agree with you that answers 1 and 3 ought to be discounted for the large majority of C++ programmers. If you are using noexcept for those reasons, then don't because it's a lousy solution to not the problem you think you have. About answer 2 you say:
If your motivation is (2), noexcept will also not help you much. Suppose you detect that the function is not declared as noexcept, what do you do? Don’t use it? It may still not throw exceptions. A function can throw nothing and still be declared noexcept(false). This is the case for the std::swap specialization for STL containers: following the recommendation from N3248 it is guaranteed not to throw, but is declared noexcept(false). For this reason, the example I gave in my other post with function nofail_swap is wrong. It doesn’t take into account that swap on STL containers is no-fail. You cannot check the no-fail or no-throw guarantee with a compile-time expression, because in some cases it is just announced informally in the documentation.
Also, if a function is declared noexcept it can’t throw, but it can call std::terminate. Do you consider this behaviour suitable for a component that is supposed to be “exception safe”?
Here is where I think you've missed the beat. In everything you say above you are 100% correct. noexcept has a lousy, poorly thought through implementation in C++ and it shows every bit of having been tacked on at the last minute in the C++ 11 standard, and I said so at the time to anyone who would listen in 2010. But do you see that the lousy implementation has nothing to do with this:
2. Because I need this function to provide no-throw exception safety guarantee.
Your argument against using noexcept to make a function provide a no-throw exception safety guarantee was all about the lousy implementation of noexcept, and nothing about whether explicitly guaranteeing that calling some function will not invert control flow is a good or bad thing.
You are absolutely right that marking a function with noexcept means it simply adds a call to std::terminate which is usually not what the programmer intended. That's that lousy implementation again, not least that this stupid piece of code:
void somehow_this_is_not_a_compile_failure() noexcept { throw "foo"; // i.e. std::terminate() }
... is legal, and worse, many compilers don't even warn, you need to run clang-tidy on it to get any indication of this being very unlikely to be what the programmer intended. But that's a problem for the library implementor, and a very different viewpoint arrives at the library *user*.
One of the main reasons you'd want to use Outcome is because *you really don't want control flow to invert most of the time*. There is a lot of buy in for this amongst SG14 members and others with really big C++ codebases, but for STL maintainers and Boost devs it is not as widely appreciated how much more costly debugging and maintaining multi translation unit code which can invert control flow is. That said, there is recognition even amongst the hardcore of SG14 members that being able to use the standard STL rather than the EA custom STL more frequently would be useful, so if one could create small, localised islands where exception throws can happen just within that translation-unit-local island then one could use the STL just within that island. The island is then guarded by a catch all try catch because you can't safely have anything else in a noexcept function. This leads to the "sea of noexcept" design pattern described in the Outcome tutorial which is:
extern outcome<void> some_public_api() noexcept; ... outcome<void> some_public_api() noexcept { try { ... STL using code ... return {}; // return empty outcome } catch(...) { // return exceptioned outcome, defaults to using std::current_exception() return make_exceptional_outcome<>(); } }
All the above requires some programmer discipline, but I would argue much less programmer discipline than writing exception safe code which is correct and bugfree.
Also, the tooling will catch up. clang-tidy is getting better at warning you when you forget the catch all try catch wrapping a noexcept function. I also intend, at some point, to add Outcome-awareness to clang-tidy so you'll get a much harder error if you leak exceptions out of an outcome returning noexcept function. Or, in other words, by returning outcomes the programmer really does not intend exception throws to be aliased into std::terminate() for them by the compiler.
Finally back this statement of yours:
I strongly believe that this is the wrong thing to do (or at least controversial) to annotate a function that can fail-but-not-throw as noexcept. It is more in the spirit of noexcept intentions to indicate a no-fail guarantee rather than the no-throw guarantee.
On this I think you're just plain wrong. noexcept has a very specific meaning on an API for the *users* of that API: calling this API will not invert control flow. Not EVER. And you can write code calling it assuming that in the strongest sense possible. It does NOT mean that the function cannot fail.
The reason why is the heritage from C. Any C function does not throw exceptions, yet they definitely can fail. I would say most C++ programmers would agree therefore that marking a function with noexcept means "no exceptions", not "no failures".
So, tl;dr and all that, I agree with your opinion if I'm wearing my library developer's hat. I disagree with you if I'm wearing my library user's hat. Hopefully all that above actually made some sense.
I am confused about the usage of term "inversion of control" in the context of throwing exceptions. Maybe I am missing something obvious; but what do you mean when you say that "calling noexcept never causes an unexpected inversion of control"? Regards, &rzej;