C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Automatic conditional noexcept: noexcept(auto)

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Tue, 5 Sep 2023 10:02:46 -0400
On Mon, Sep 4, 2023 at 2:13 AM Yexuan Xiao via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> I noticed that Microsoft STL originally added conditional noexcept for
> std::greater::operator()
> <https://github.com/microsoft/STL/blob/6c69a73911b33892919ec628c0ea5bbf0caf8a6a/stl/inc/xutility#L409>
> :

export template <class _Ty = void>struct greater {
> [[nodiscard]] constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const
> noexcept(noexcept(_Fake_copy_init<bool>(_Left > _Right))) /* strengthened */ {
> return _Left > _Right;
> }
> };
>
> and std::invoke
> <https://github.com/microsoft/STL/blob/6c69a73911b33892919ec628c0ea5bbf0caf8a6a/stl/inc/type_traits#L1728C1-L1728C1>
> has the following implementation:
>
> export template <class _Callable>constexpr auto invoke(_Callable&& _Obj) noexcept(noexcept(static_cast<_Callable&&>(_Obj)()))
> -> decltype(static_cast<_Callable&&>(_Obj)()) {
> return static_cast<_Callable&&>(_Obj)();
> }// and other overloaded versions
>
> These uses of the noexcept operator are undoubtedly redundant and ugly: noexcept(_Fake_copy_init<bool>(_Left
> > _Right)) and noexcept(static_cast<_Callable&&>(_Obj)()).
>
Are they? Notice that `invoke` is a straightforward application of the "You
must write it three times" pattern: even if you replaced the
noexcept-clause with `noexcept(auto)`, that snippet would still have two
instances of `static_cast<_Callable&&>(_Obj)()`.

These two examples are different in spirit:
- `greater::operator()` is specified on paper to be non-noexcept; Microsoft
is going out of its way to strengthen the spec
- `invoke` is specified on paper to have a specific conditional
noexcept-spec; Microsoft is simply implementing the required spec

Your `noexcept(auto)` syntax would help a great deal with the former case
(where the vendor doesn't care what the answer is, as long as it's
reasonably "form-fitting" and never incorrect). But it wouldn't help at
all with the latter case (where the vendor is trying to match the specified
noexcept-spec exactly).

The cause of this problem is an unfortunate historical issue: C++11
> invented the noexcept specifier, but until C++17, the
> noexcept-specification became part of the function type. Therefore, before
> C++17, we did not have a good way to propagate this information, which
> caused some confusion.
>
This paragraph seems misguided. There was no problem propagating
conditional noexcept "before C++17"; it worked just fine. All C++17 did was
allow you to represent the noexceptness of indirect calls through *function
pointers*, which are vanishingly rare in library code.


> Also, manually writing noexcept specifications is a painful thing: if a
> function needs to conditionally throw exceptions but makes an incomplete
> check, it will cause the program abort, which is very dangerous.
>
This paragraph is the main reason why the STL avoids conditional noexcept:
it's very easy to get wrong.
(E.g. flat_set::swap is currently noexcept
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2767r1.html#noexcept-swap>,
which can lead to a rogue std::terminate if swapping the underlying
containers or comparators throws. See also "Attribute noexcept_verify"
(2018)
<https://quuxplusone.github.io/blog/2018/06/12/attribute-noexcept-verify/>.)
The STL has two criteria for writing noexcept-specs:
- They must not lead to rogue std::terminates.
- They must be predictable: the user must be able to look up the
noexcept-spec of a given function in the paper standard, not just by
inspecting their vendor's implementation.

`noexcept(auto)` successfully avoids rogue std::terminates, but fails to
deliver predictability. As Giuseppe pointed out, once a function is marked
`noexcept(auto)`, any minor change to the function's implementation can
affect its noexceptness. And since C++17, this will also affect the
function's type!

    int stl_f(int x) noexcept(auto) { return (x > 0) ? x : -x; } //
version 1 of the library open-codes the implementation
    int (*client_pf)(int) noexcept = stl_f; // OK

    int stl_f(int x) noexcept(auto) { return std::abs(x); } // version 2
of the library switches to using std::abs
    int (*client_pf)(int) noexcept = stl_f; // Error, stl_f is no longer
noexcept; our client code no longer compiles

I propose to add noexcept(auto) as a placeholder for
> noexcept-specification, which is equivalent to noexcept(false), except in
> the following cases where it is equivalent to noexcept(true):
>
> - It is not a non-throwing function as specified by the standard, and
> - Any function call within the function body or member initializer
> list does not throw exceptions, or
> - The function body is a try block, and the last catch catches all
> exceptions, or
> - Within the function body, any function call that throws an exception
> is wrapped by a try block, and the last catch catches all exceptions
>
> It seems to me that you shouldn't be looking at "function calls" at all.
You should be looking at the noexceptness of each non-discarded
full-expression involved in the function, for some definition of "involved
in." For example,

    template<class T> void f(T t) noexcept(auto) {
        t += 1;
        throw t;
    }

should be treated as if noexcept(auto) were replaced with
noexcept(noexcept(t += 1) && noexcept(throw t)).
You just have to figure out what to do about non-linear control flow. For
example,

    template<class T> void g(T t) noexcept(auto) {
        if (noexcept(t += 1)) t += 1;
    }

"obviously" doesn't evaluate any throwing expression; but how does the
compiler know that? (Most likely we don't care about this example. We just
say "the programmer should have used `if constexpr`" and move on. But are
there similar examples that are less trivial?)


This proposal is a pure syntactic extension, it does not conflict with any
> existing code, and it is foreseeable that it will not in the future. It
> does not force the standard library to make any updates, nor does it cause
> any API or ABI breaks.
>
But this proposal *does* affect the library implementation, right? It
sounds as if you're proposing that e.g. `std::invoke` should start using
`noexcept(auto)`. Which means two things:
- you have an "acceptance criterion": the proposal isn't ready for prime
time until you have proved it's suitable for implementing `std::invoke`
- you are essentially proposing that all vendors change their
implementation of `std::invoke`, so you must invite discussion from those
vendors about whether they *want* to change `std::invoke`'s implementation
like that. If `noexcept(auto)` wouldn't actually be used for its intended
purpose, then either it shouldn't be accepted (see point 1 above) or else
you need to find a different motivation — change its "intended purpose" to
something that it might actually be used for.

I would like to thank Xiaoyu Yan for providing the information about
> std::max.


Nit: Your proposal never mentions the word "max".

HTH,
Arthur

Received on 2023-09-05 14:03:04