Search code examples
c++std-function

Why is a std::function with signature void (X) allowed to bind to a function void f(X&&)?


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;
}

Demo

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?


Solution

  • 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). If f 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 unless f is Callable for argument types Args... and return type R.

    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;
    }
    

    Live on compiler explorer

    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.