【问题标题】:Why is a std::function with signature void (X) allowed to bind to a function void f(X&&)?为什么允许带有签名 void (X) 的 std::function 绑定到函数 void f(X&&)?
【发布时间】:2021-03-30 12:30:06
【问题描述】:

在下面的代码中,为什么允许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 时它实际上意味着什么,然后它会调用接受右值引用的函数?

【问题讨论】:

  • 等一下,定义 ff1...ff4 的 4 行中都是 f1 吗?
  • @Enlico 似乎问题在修复后仍然有效,因为只有 ff2(f2) 无法编译:godbolt.org/z/a3TcEhGvT
  • ff2(f2) 失败是我问的原因。一个错字是可以的,但 OP 也写了// ok,所以他/她应该澄清一下。
  • 一个std::function 包装了一个可调用对象。函数指针是一种可调用对象;还有其他种类。可调用对象通常没有任何签名。它可以使用给定的一组参数调用,也可以不调用。
  • @Enlico:修复了 OP 的错字,我希望我不会因为修复而使问题变质(标题仍然有效)。

标签: c++ std-function


【解决方案1】:

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

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

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

5) 用std::move(f) 初始化目标。如果f 是指向的空指针 函数或指向成员的空指针,*this 在 称呼。此构造函数不参与重载决议 除非 f 对于参数类型 Args... 和返回类型 RCallable

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

X&& x = X{};

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

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

std::function 中,每个参数都被转发。转发就像听起来一样:它转发参数以调用被包装的函数,就像你直接调用它一样。但是转发不保持纯正确性,而是将参数作为 xvalue 转发。 xvalues 和 prvalues 都是 rvalue 的特例。

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

要了解更多关于转发的信息,请参阅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&amp;) 的可变引用无法绑定临时值。所以X&amp; x = X{} 不起作用,X&amp; x2 = std::move(x1) 也不起作用。

另外,f4(x) 不要编译,因为x 不是临时的。表达式(x) 实际上是X&amp;X&amp;&amp; 只绑定到临时的,如X{}std::move(x)。要将变量转换为临时变量,您需要使用 std::move,它会执行转换。详情请见Why do I have to call move on an rvalue reference?

【讨论】:

  • 感谢您的回复,但我仍然很困惑。也许你可以指出我哪里错了?对于 std::function 它会有 operator()(X x),但是如果它绑定到 void f4(X&& x),对于 operator() 来调用 f4(),不会有需要从 X 到 X&& 的隐式转换吗?但我认为没有从 X 到 X&& 的隐式转换,这就是为什么 X x; f4(x);不编译?
  • 另外,根据你上面的解释,我不明白为什么 ff2 不能编译?
  • @NickKovac 对于您的第一点,我编辑了关于转发的段落。我还添加了一个链接,更深入地解释了转发。我还添加了关于为什么 f2 不编译的解释。如果还有什么不清楚的地方告诉我。
  • @NickKovac 顺便说一句,是的,X 类型的表达式,例如 X{} 隐式转换为 X&amp;&amp;。表达式(x) 实际上是一个左值,所以它的类型是X&amp;。但这不是std::function 中发生的事情。此包装器使用转发,它将参数转换为正确的引用类型。
  • 仍然很困惑 :) 如果我们有 X x; std::function ff4(f4); ff4(x);那么我们不是将左值(x)传递给ff4吗?所以我很困惑将它转发给 f4() 有什么意义,它只应该接受右值引用?在这种情况下,实际上将什么临时值传递给 f4() ?我们只提供了 x,这不是临时的吗?你能解释一下 std::function 的 operator() 的类型,以及它调用 f4() 的作用吗?
猜你喜欢
  • 2012-08-09
  • 2016-06-11
  • 2013-12-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-11
  • 2017-12-05
相关资源
最近更新 更多