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

Nic*_*vac 3 c++ std-function

在下面的代码中,为什么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;
}
Run Code Online (Sandbox Code Playgroud)

演示

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

Gui*_*cot 6

您可能知道,该std::function类型是任何类似函数的类型的多态包装器。这包括函数指针、带有operator()lambda 的类。

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

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

5) 用 初始化目标std::move(f)。如果f是指向函数的空指针或指向成员的空指针,*this则调用后将为。此构造不参与重载解析,除非f可调用的参数类型Args...和返回值类型R

实际上, type 的表达式X可以完全绑定到 type 的右值引用X&&。尝试这个:

X&& x = X{};

// likewise, both prvalue and xvalue works:
f4(X{});
f4(std::move(x));
Run Code Online (Sandbox Code Playgroud)

在此示例中,X{}是 类型 的纯右值X。右值引用可以绑定到这样的临时对象。

在 内部std::function,每个参数都被转发。转发就像听起来一样:它转发参数来调用被包装的函数,就像你直接调用它一样。但是转发不会保留纯右值,而是将参数作为 xvalue 转发。xvalues 和 prvalues 都是右值的特例。

std::functionand 转发的上下文中,将参数传递给被包装的函数对Xand使用相同的表达式X&&。没有缺点就不能转发纯价值。

要了解有关转发的更多信息,请参阅使用 std::forward 的主要目的是什么以及它解决了哪些问题?

隐式转换可以更进一步:从 adouble到 an int。事实上,如果你将一个浮点类型发送给一个接受 的函数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;
}
Run Code Online (Sandbox Code Playgroud)

在编译器浏览器上直播

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


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

另外,f4(x)不要编译,因为x它不是临时的。表达式(x)实际上是X&. X&&只绑定到临时的,例如X{}or std::move(x)。要将变量转换为临时变量,您需要使用std::move,它执行转换。请参阅为什么我必须对右值引用调用移动?更多细节。