为什么允许带有签名 void (X) 的 std::function 绑定到函数 void f(X&&)?

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

在下面的代码中,为什么允许 std::function<void (X)> 绑定到函数 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

为什么允许这样做背后的直觉是什么,std::function 将如何实现它,以及当您将左值参数传递给 std::function 时它的实际含义是什么,然后调用接受右值引用的函数?

如您所知,std::function 类型是任何类函数类型的多态包装器。这包括函数指针,类 with operator() 和 lambdas.

由于它必须实现类型擦除,它只检查您发送的内容是否可以使用它接收的参数调用。类型擦除将简单地隐式地进行转换,就像任何函数调用一样。

如果您查看 std::function::function 的 cppreference 页面,您会注意到此要求:

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.

确实,X 类型的表达式完全可以绑定到 X&& 类型的右值引用。试试这个:

X&& x = X{};

// likewise, both prvalue and xvalue works:
f4(X{});
f4(std::move(x));

在此示例中,X{}X 类型的纯右值。右值引用可以绑定到此类临时文件。

std::function内部,每个参数都被转发。转发顾名思义:它转发参数来调用包装函数,就像你直接调用它一样。然而,转发不保持纯右值,而是将参数作为 xvalue 转发。 xvalues 和 prvalues 都是右值的特例。

std::function 和转发的上下文中,将参数传递给包装函数对 XX&& 使用相同的表达式。纯右值性不能在没有缺点的情况下转发。

要了解有关转发的更多信息,请参阅What are the main purposes of using std::forward and which problems it solves?

隐式转换可以更进一步:从 doubleint。实际上,如果您将浮点类型发送到采用 int 的函数,C++ 将遗憾地执行隐式转换。 std::function 实现并不能避免这种影响。考虑这个例子:

#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

这是可能的,因为 std::function 是函数对象的包装器。函数指针需要是准确的类型。


此外,f2 无法编译,因为对左值 (X&) 的可变引用无法绑定临时变量。所以X& x = X{}不行,X& x2 = std::move(x1)也不行。

此外,f4(x) 不编译,因为 x 不是临时文件。表达式 (x) 实际上是 X&X&& 只绑定临时的,比如X{}std::move(x)。要将变量转换为临时变量,您需要使用 std::move,它会执行转换。有关详细信息,请参阅