In the code below, why is a std::function<void (X)>
allowed to bind to a function void f(X&&)
?
#include <functional>
struct X {};
void f1(X x) {}
void f2(X& x) {}
void f3(const X&) {}
void f4(X&& x) {}
int main()
{
X x;
f1(x); // ok
f2(x); // ok
f3(x); // ok
// f4(x); // doesn't compile
std::function<void (X)> ff1(f1); // ok
//std::function<void (X)> ff2(f2); // doesn't compile
std::function<void (X)> ff3(f3); // ok
std::function<void (X)> ff4(f4); // ok... why?
return 0;
}
What's the intuition behind why this is allowed, how would it be implemented by std::function
, and what does it actually mean when you pass an lvalue parameter to the std::function
, which then calls the function that accepts an rvalue reference?
As you might know, the std::function
type is a polymorphic wrapper around any function-like types. That include function pointer, classes with operator()
and lambdas.
Since it has to implement type erasure, it only checks if what you send is callable using the arguments it receives. The type erasure will simply do the conversion implicitly, just as any function calls.
If you look at the cppreference page for std::function::function
, you'll notice this requirement:
5) Initializes the target with
std::move(f)
. Iff
is a null pointer to function or null pointer to member,*this
will be empty after the call. This constructor does not participate in overload resolution unlessf
is Callable for argument typesArgs...
and return typeR
.
Indeed, an expression of type X
can totally be bound to an rvalue reference of type X&&
. Try this:
X&& x = X{};
// likewise, both prvalue and xvalue works:
f4(X{});
f4(std::move(x));
In this example, X{}
is a prvalue of type X
. A rvalue-reference can be bound to such temporary.
Inside the std::function
, every parameter is forwarded. Forwarding is exactly as it sounds: it forward the parameter to call the wrapped function, just as if you called it directly. However forwarding don't keep the prvalue-ness, but forwards arguments as xvalue. xvalues and prvalues both are special cases of rvalue.
In the context of std::function
and forwarding, passing parameter to the wrapped function use the same expression for X
and X&&
. prvalue-ness cannot be forwarded without downsides.
To know more about forwarding, see What are the main purposes of using std::forward and which problems it solves?
Implicit conversion can go even further: from a double
to an int
. Indeed, if you send a floating type to a function that takes an int
, C++ will, sadly, perform an implicit conversion. The std::function
implementation is not immune to this effect. Consider this example:
#include <functional>
#include <cstdio>
int main() {
auto const f = std::function<void(double)>{
[](int a) {
std::printf("%d", a);
}
};
f(9.8); // prints 9
return 0;
}
This is possible only because std::function
is a wrapper around function objects. Function pointers would need to be the exact type.
Also, f2
doesn't compile because mutable reference to lvalue (X&
) cannot bound a temporary. So X& x = X{}
won't work, and X& x2 = std::move(x1)
won't work either.
Also, f4(x)
don't compile since x
is not a temporary. the expression (x)
is actually X&
. X&&
only bound to temporary, such as X{}
or std::move(x)
. To cast a variable to a temporary, you need to use std::move
, which preform the cast. See Why do I have to call move on an rvalue reference? for more details.