std::function - 值参数 vs. 引用参数

5

在使用值参数和使用值参数的const引用时,std::function之间是否存在实际差异?考虑以下代码:

auto foo = [] (VeryBigType i) {     
};

auto bar = [] (const VeryBigType& i) {
};

std::function<void(VeryBigType)> a;
a = foo;
a = bar;

std::function<void(const VeryBigType&)> b;
b = foo;
b = bar;

这段代码编译没有问题,并且运行得非常好。 我知道传递值和引用有性能差异,因此foobar的行为不同。 但是根据std :: function模板类型是否有差异? 例如,std :: function<void(VeryBigType)>(bar)std :: function<void(const VeryBigType&)>(bar)之间是否存在任何实现和/或行为和/或性能差异? 这些结构是否等效?
2个回答

6

cppreference 表示 std::function<R(Args...)>::operator() 具有以下签名:

R operator()(Args... args) const;

它调用存储的可调用对象f,基本上是通过f(std::forward<Args>(args)...)来实现的。性能特征取决于模板参数和lambda的参数类型,我认为只要看到所有可能发生的情况就会有所帮助。在您的情况下,您有2个std::function类型、2个可调用对象和3种可能的值类别作为参数,共有12种可能性。

  • std::function<void(VeryBigType)> f = [](VeryBigType i) { }

    • If you call this with an lvalue, like

      VeryBigType v;
      f(v);
      

      This will copy v into the argument of operator(), and then operator() will pass an rvalue to the lambda, which will move the value into i. Total cost: 1 copy + 1 move

    • If you call this with a prvalue, like

      f(VeryBigType{});
      

      Then this will materialize the prvalue into the argument of operator(), then pass an rvalue to the lambda, which will move it into i. Total cost: 1 move

    • If you call this with an xvalue, like

      VeryBigType v;
      f(std::move(v));
      

      This will move v into the argument of operator(), which will pass an rvalue to the lambda, which will move it again into i. Total cost: 2 moves.

  • std::function<void(VeryBigType)> f = [](VeryBigType const &i) { }

    • If you call this with an lvalue, this will copy once into the argument of operator(), and then the lambda will be given a reference to that argument. Total cost: 1 copy.

    • If you call this with a prvalue, this will materialize it into the argument of operator(), which will pass a reference to that argument to the lambda. Total cost: nothing.

    • If you call this with an xvalue, this will move it into the argument of operator(), which will pass a reference to that argument to the lambda. Total cost: 1 move.

  • std::function<void(VeryBigType const&)> f = [](VeryBigType i) { }

    • If you call this with an lvalue or xvalue (i.e. with a glvalue), operator() will receive a reference to it. If you call this with a prvalue, it will be materialized into a temporary, and operator() will receive a reference to that. In any case, the inner call to the lambda will always copy. Total cost: 1 copy.
  • std::function<void(VeryBigType const&)> f = [](VeryBigType const &i) { }

    • Again, no matter what you call this with, operator() will receive just a reference to it, and the lambda will just receive the same reference. Total cost: nothing.
所以,我们学到了什么?如果std::function和lambda表达式都采用引用方式,你可以避免任何额外的复制和移动。尽可能地使用这种方法。然而,将按值lambda放在按-const-左值引用的std::function中是个坏主意(除非你必须这样做)。实质上,左值引用“忘记”了参数的值类别,lambda表达式的参数总是被复制。将一个按-const-左值引用的lambda放在一个按值传递的std::function中在性能上相当不错,但只有在调用其他期望按值传递的代码时才需要这样做,否则按引用传递的std::function能够以更少的复制和移动完成相同的工作。将按值lambda放入按值std::function中比将按-const-左值引用的lambda放入其中稍微差一些,因为所有调用都会有一个额外的移动。最好的方法是使用按右值引用获取lambda的参数,这基本上与使用按-const-左值引用获取参数相同,除了你仍然可以像按值方式一样改变参数。

简而言之,在std::function模板参数中按值和右值引用的参数应与放入其中的lambda表达式的按右值引用或按-const-左值引用的参数相对应。在类型中按左值引用的参数应与lambda表达式中的按左值引用的参数相对应。其他任何方式都会增加额外的复制或移动,仅在需要时才应使用。


0

你的问题有些令人困惑,因为你似乎知道值参数和引用参数之间存在非常大的性能差异。

但你似乎不知道的是,function 对象的模板类型决定了如何将参数传递给 lambda 函数,而不是 lambda 函数本身,这是由于 cdecl 调用约定:调用者在堆栈上传递参数,然后执行清理操作,并通过你的 std::function 对象调用。

因此,a 总是会分配一个新的对象副本并传递其引用,然后清理它,而 b 总是会传递原始对象的引用。

编辑:至于为什么无论如何定义 lambda 函数都可以工作,这是因为 cdecl,两个函数都期望第一个参数是指针,并对它们进行操作。类型周围的其余声明(类型大小、constness、引用等)仅用于验证函数内部的代码,并验证该函数本身可以被你的 function 对象调用(即,function 将发送指针作为第一个参数)。


请注意,虽然我特别提到了 cdecl,但同样适用于 stdcall(相同,但函数清理堆栈),fastcall(使用寄存器而不是堆栈)和 thiscall(在 x86 上与 cdecl 相同,在 x64 上与 fastcall 相同,但它还使用 ECX/RCX 发送 this)。 - Blindy
3
你为什么要谈论调用约定?你不需要涉及实现细节来弄清楚std::function是如何工作的。 - HTNW
那是你的观点。 - Blindy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接