On Mon, Sep 4, 2023 at 2:13 AM Yexuan Xiao via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
I noticed that Microsoft STL originally added conditional noexcept for std::greater::operator():
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 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, which can lead to a rogue std::terminate if swapping the underlying containers or comparators throws. See also "Attribute noexcept_verify" (2018).)
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