815. std::function and reference_closure do not use perfect forwarding

Section: 23.14.13.2.4 [func.wrap.func.inv] Status: Resolved Submitter: Alisdair Meredith Opened: 2008-03-16 Last modified: 2016-02-10

Priority: Not Prioritized

View all other issues in [func.wrap.func.inv].

View all issues with Resolved status.

Discussion:

std::function and reference_closure should use "perfect forwarding" as described in the rvalue core proposal.

[ Sophia Antipolis: ]

According to Doug Gregor, as far as std::function is concerned, perfect forwarding can not be obtained because of type erasure. Not everyone agreed with this diagnosis of forwarding.

[ 2009-05-01 Howard adds: ]

Sebastian Gesemann brought to my attention that the CopyConstructible requirement on function's ArgTypes... is an unnecessary restriction.

template<Returnable R, CopyConstructible... ArgTypes>
class function<R(ArgTypes...)>
...

On further investigation, this complaint seemed to be the same issue as this one. I believe the reason CopyConstructible was put on ArgTypes in the first place was because of the nature of the invoke member:

template<class R, class ...ArgTypes>
R
function<R(ArgTypes...)>::operator()(ArgTypes... arg) const
{
    if (f_ == 0)
        throw bad_function_call();
    return (*f_)(arg...);
}

However now with rvalue-refs, "by value" no longer implies CopyConstructible (as Sebastian correctly points out). If rvalue arguments are supplied, MoveConstructible is sufficient. Furthermore, the constraint need not be applied in function if I understand correctly. Rather the client must apply the proper constraints at the call site. Therefore, at the very least, I recommend that CopyConstructible be removed from the template class function.

Furthermore we need to mandate that the invoker is coded as:

template<class R, class ...ArgTypes>
R
function<R(ArgTypes...)>::operator()(ArgTypes... arg) const
{
    if (f_ == 0)
        throw bad_function_call();
    return (*f_)(std::forward<ArgTypes>(arg)...);
}

Note that ArgTypes&& (the "perfect forwarding signature") is not appropriate here as this is not a deduced context for ArgTypes. Instead the client's arguments must implicitly convert to the non-deduced ArgType type. Catching these arguments by value makes sense to enable decay.

Next forward is used to move the ArgTypes as efficiently as possible, and also with minimum requirements (not CopyConstructible) to the type-erased functor. For object types, this will be a move. For reference type ArgTypes, this will be a copy. The end result must be that the following is a valid program:

#include <functional>
#include <memory>
#include <cassert>

std::unique_ptr<int>
f(std::unique_ptr<int> p, int& i)
{
    ++i;
    return std::move(p);
}

int main()
{
    int i = 2;
    std::function<std::unique_ptr<int>(std::unique_ptr<int>,
                                       int&> g(f);
    std::unique_ptr<int> p = g(std::unique_ptr<int>(new int(1)), i);
    assert(*p == 1);
    assert(i == 3);
}

[ Tested in pre-concepts rvalue-ref-enabled compiler. ]

In the example above, the first ArgType is unique_ptr<int> and the second ArgType is int&. Both must work!

[ 2009-05-27 Daniel adds: ]

in the 2009-05-01 comment of above mentioned issue Howard

  1. Recommends to replace the CopyConstructible requirement by a MoveConstructible requirement
  2. Says: "Furthermore, the constraint need not be applied in function if I understand correctly. Rather the client must apply the proper constraints at the call site"

I'm fine with (a), but I think comment (b) is incorrect, at least in the sense I read these sentences. Let's look at Howard's example code:

function<R(ArgTypes...)>::operator()(ArgTypes... arg) const
{
   if (f_ == 0)
       throw bad_function_call();
   return (*f_)(std::forward<ArgTypes>(arg)...);
}

In the constrained scope of this operator() overload the expression "(*f_)(std::forward<ArgTypes>(arg)...)" must be valid. How can it do so, if ArgTypes aren't at least MoveConstructible?

[ 2009-07 Frankfurt: ]

Leave this open and wait until concepts are removed from the Working Draft so that we know how to write the proposed resolution in terms of diffs to otherwise stable text.

[ 2009-10 Santa Cruz: ]

Leave as open. Howard to provide wording. Howard welcomes any help.

[ 2009-12-12 Jonathan Wakely adds: ]

23.14.13.2 [func.wrap.func] says

2 A function object f of type F is Callable for argument types T1, T2, ..., TN in ArgTypes and a return type R, if, given lvalues t1, t2, ..., tN of types T1, T2, ..., TN, respectively, INVOKE (f, t1, t2, ..., tN) is well formed (20.7.2) and, if R is not void, convertible to R.

N.B. lvalues, which means you can't use function<R(T&&)> or function<R(unique_ptr<T>)>

I recently implemented rvalue arguments in GCC's std::function, all that was needed was to use std::forward<ArgTypes> in a few places. The example in issue 815 works.

I think 815 could be resolved by removing the requirement that the target function be callable with lvalues. Saying ArgTypes need to be CopyConstructible is wrong, and IMHO saying MoveConstructible is unnecessary, since the by-value signature implies that already, but if it is needed it should only be on operator(), not the whole class (you could in theory instantiate std::function<R(noncopyable)> as long as you don't invoke the call operator.)

I think defining invocation in terms of INVOKE already implies perfect forwarding, so we don't need to say explicitly that std::forward should be used (N.B. the types that are forwarded are those in ArgTypes, which can differ from the actual parameter types of the target function. The actual parameter types have gone via type erasure, but that's not a problem - IMHO forwarding the arguments as ArgTypes is the right thing to do anyway.)

Is it sufficient to simply replace "lvalues" with "values"? or do we need to say something like "lvalues when Ti is an lvalue-reference and rvalues otherwise"? I prefer the former, so I propose the following resolution for 815:

Edit 23.14.13.2 [func.wrap.func] paragraph 2:

2 A function object f of type F is Callable for argument types T1, T2, ..., TN in ArgTypes and a return type R, if, given lvalues t1, t2, ..., tN of types T1, T2, ..., TN, respectively, INVOKE (f, t1, t2, ..., tN) is well formed (20.7.2) and, if R is not void, convertible to R.

[ 2009-12-12 Daniel adds: ]

I don't like the reduction to "values" and prefer the alternative solution suggested using "lvalues when Ti is an lvalue-reference and rvalues otherwise". The reason why I dislike the shorter version is based on different usages of "values" as part of defining the semantics of requirement tables via expressions. E.g. 20.5.3.1 [utility.arg.requirements]/1 says "a, b, and c are values of type const T;" or similar in 26.2.1 [container.requirements.general]/4 or /14 etc. My current reading of all these parts is that both rvalues and lvalues are required to be supported, but this interpretation would violate the intention of the suggested fix of #815, if I correctly understand Jonathan's rationale.

[ 2009-12-12 Howard adds: ]

"lvalues when Ti is an lvalue-reference and rvalues otherwise"

doesn't quite work here because the Ti aren't deduced. They are specified by the function type. Ti might be const int& (an lvalue reference) and a valid ti might be 2 (a non-const rvalue). I've taken another stab at the wording using "expressions" and "bindable to".

[ 2010-02-09 Wording updated by Jonathan, Ganesh and Daniel. ]

[ 2010-02-09 Moved to Tentatively Ready after 5 positive votes on c++std-lib. ]

[ 2010-02-10 Daniel opens to improve wording. ]

[ 2010-02-11 This issue is now addressed by 870. ]

[ 2010-02-12 Moved to Tentatively NAD Editorial after 5 positive votes on c++std-lib. Rationale added below. ]

Rationale:

Addressed by 870.

Proposed resolution:

Edit 23.14.13.2 [func.wrap.func] paragraph 2:

2 A function object f of type F is Callable for argument types T1, T2, ..., TN in ArgTypes and a return type R, if, given lvalues t1, t2, ..., tN of types T1, T2, ..., TN, respectively, the expression INVOKE(f, declval<ArgTypes>()..., Rt1, t2, ..., tN), considered as an unevaluated operand (8 [expr]), is well formed (20.7.2) and, if R is not void, convertible to R.