3

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?

Enlico
  • 23,259
  • 6
  • 48
  • 102
Nick Kovac
  • 487
  • 3
  • 10
  • 2
    Wait a moment, is it all `f1` in the 4 lines defining `ff1`...`ff4`? – Enlico Mar 30 '21 at 12:43
  • 1
    @Enlico Seems the question is still somehow valid after fixing, since only `ff2(f2)` fails to compile: https://godbolt.org/z/a3TcEhGvT – R2RT Mar 30 '21 at 12:49
  • `ff2(f2)` failing is the reason why I asked. A typo is ok, but the OP also wrote `// ok`, so probably he/she should clarify a bit. – Enlico Mar 30 '21 at 12:51
  • An `std::function` wraps a *callable object*. A function pointer is one kind of callable object; there are other kinds. A callable object in general doesn't have any signature. It is either callable with a given set of arguments, or not. – n. m. could be an AI Mar 30 '21 at 12:55
  • @Enlico: fixed typo from OP, I hope I don't denature the question by the fixes (title is still valid). – Jarod42 Mar 30 '21 at 12:56
  • f1, f3, and f4 can be called using `f(X{})` ... but f2 cannot. – Eljay Mar 30 '21 at 13:01
  • Sorry about the typos... but I think the question is still valid. – Nick Kovac Mar 30 '21 at 13:22

1 Answers1

6

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.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Thanks for your reply, but I'm still confused. Maybe you can point out where I'm going wrong? For std::function it would have operator()(X x), but then if it's bound to void f4(X&& x), for the operator() to call f4(), wouldn't there need to be an implicit conversion from X to X&& ? But I thought there is no implicit conversion from X to X&&, which is why X x; f4(x); doesn't compile? – Nick Kovac Mar 30 '21 at 23:04
  • Also, with your above explanation, I don't understand why ff2 doesn't compile? – Nick Kovac Mar 30 '21 at 23:08
  • @NickKovac For your first point, I edited the paragraph about forwarding. I also added a link that explains forwarding more in depth. I also added an explanation about why `f2` don't compile. Tell me if there's still something unclear. – Guillaume Racicot Mar 31 '21 at 00:10
  • @NickKovac by the way yes, an expression of type `X` such as `X{}` implicitly converts to `X&&`. The expression `(x)` is actually an lvalue, so it is of type `X&`. But this is not what happens in `std::function`. This wrapper uses forwarding, which casts parameter to the correct reference type. – Guillaume Racicot Mar 31 '21 at 00:12
  • Still confused :) If we have X x; std::function ff4(f4); ff4(x); then aren't we passing an lvalue (x) to ff4? So I'm confused how it makes sense to then forward this to f4(), which is only supposed to accept rvalue references? What temporary value is actually being passed to f4() in that case? We only provided x, which isn't temporary? Could you explain it in terms of the type of the std::function's operator(), and what it does to then call f4()? – Nick Kovac Mar 31 '21 at 03:21
  • Since std::function is a wrapper, when you call the std::function, you have to pass throught the wrapper. In the eyes of the compiler (and you!) any std::function can be called the same. No matter what it contains. Because it's hidden. The content of the std::function could be set in an unrelated cpp file somewhere – Guillaume Racicot Mar 31 '21 at 03:48
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/230573/discussion-between-guillaume-racicot-and-nick-kovac). – Guillaume Racicot Mar 31 '21 at 03:48