C++ Logo

sg14

Advanced search

Re: Interesting idea for non-throwing std::function

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Mon, 3 Oct 2022 18:15:01 -0400
On Mon, Oct 3, 2022 at 5:23 PM Patrice Roy via SG14 <sg14_at_[hidden]>
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) {
> static_assert(std::is_nothrow_constructible_v<std::function<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.

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
https://p1144.godbolt.org/z/hja8sdT6b
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.

–Arthur

Received on 2022-10-03 22:15:15