On Mon, 3 Oct 2022 at 23:15, Arthur O'Dwyer <arthur.j.odwyer@gmail.com> wrote:
On Mon, Oct 3, 2022 at 5:23 PM Patrice Roy via SG14 <sg14@lists.isocpp.org> wrote:
What about making the constructor conditionally noexcept? The implementation knows whether a given type will be stored on the heap, and if that type can be constructed without throwing, so can easily expose that.

What people care about is a property of the constructor, so why not make it part of the constructor signature?

You can static assert is_nothrow_constructible.

If the SG14 contributors like this, we could turn it into a paper. It would (IMO) have a good chance of being accepted. This would let people use std::function without any risk of allocating in the cases where they care about this, and at essentially no cost. And if someone thinks all std::function cases in their codebase should be protected that way, they could envision writing a make_function() factory that would do this at no cost either, being essentially

template <class F> std::function<F> make_function(F && f) {
   return { std::forward<F>(f) };

(1) You mean this:

    template<class Sig, class T>
    std::function<Sig> make_function(T&& t) noexcept {
        static_assert(noexcept( std::function<Sig>(std::forward<T>(t)) ));
        return std::function<Sig>(std::forward<T>(t));

(2) Notice that "The ctor doesn't throw" implies "The ctor doesn't allocate," but not vice versa. For example
    struct Empty {
        Empty() {}
        Empty(const Empty&) {}
        Empty& operator=(const Empty&) { return *this; }
    auto lam = [e = Empty()](){};
    auto f = make_function<void()>(lam);  // static-assert fails because copying `lam` is potentially throwing, regardless of whether it fits in the small buffer
Therefore there are annoying false positives.

(3) Not all std::function creation can be funneled through `make_function`. Consider
    void call_it(std::function<void()> g);
    auto f = make_function<int()>([x=42]() { return x; });  // OK
    call_it(f);  // not OK; the converting constructor from function<int()> to function<void()> will allocate
Therefore there are dangerous false negatives.

(4) In implementing this idea for libc++ just now, I rediscovered something we already know: adding noexcept-specifications is really really error-prone. Libc++ will use the small buffer for any type T that is small enough and nothrow-copy-constructible. However, in the process of constructing the innards into the small buffer, libc++ happens to invoke T's move constructor. If T's copy constructor is non-throwing, but T's move constructor throws, then the exception will slam into function::function(T)'s `noexcept` firewall and terminate the program. This is non-conforming. I fixed this by saying, "Okay, libc++ no longer stores T in the small buffer unless T is nothrow-copy-constructible and nothrow-move-constructible"... but that's no basis for a system of government.

Changing the condition for using the buffer affects ABI. You can just make the noexcept-specifier false for this case instead. It would use the buffer, but doing so might throw. So constructing the std::function can throw.

Yes, that means sometimes is_nothrow_constructible is false even though it doesn't use the buffer ... for types that have non-throwing copies but throwing moves ... i.e. almost no types in the real world.


I think it would be reasonable for a vendor (such as libstdc++) to add conditional-noexcept to std::function's converting constructor. But it isn't sufficient to get you what you want, and it is very likely to introduce subtle corner-case bugs into the vendor's implementation IMHO.  Therefore I don't think it would be good to force vendors to do it.

I intend to get the libc++ patch working and make it available on my fork
hopefully by the end of the week (yet not within the next couple of hours). Then people could (theoretically) play around with it and see whether it suffices for them. I very much bet that it won't.