Boost logo

Boost :

Subject: Re: [boost] [outcome] Requesting second pre-review of Boost.Outcome tutorial
From: Niall Douglas (s_sourceforge_at_[hidden])
Date: 2017-01-16 10:50:27


>> 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.

Niall

-- 
ned Productions Limited Consulting
http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/

Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk