我应该复制std::function还是总是可以使用它的引用?

77

在我的C++应用程序中(使用Visual Studio 2010),我需要存储一个std::function,类似于以下代码:

class MyClass
   {
   public:
      typedef std::function<int(int)> MyFunction;
      MyClass (Myfunction &myFunction);
   private:
      MyFunction m_myFunction;    // Should I use this one?
      MyFunction &m_myFunction;   // Or should I use this one?
   };

如您所见,我在构造函数中将函数参数添加为引用。

但是,最佳的存储函数方法是什么?

  • 既然 std::function 只是一个函数指针且函数的“可执行代码”保证会留在内存中,是否可以将函数存储为引用?
  • 如果传递了 lambda 并调用者返回,是否必须进行复制?

我的直觉告诉我可以安全地存储引用(甚至是 const 引用)。我希望编译器在编译时生成 lambda 的代码,并在应用程序运行时将此可执行代码保存在“虚拟”内存中。因此,可执行代码永远不会被“删除”,我可以安全地将其引用存储起来。但这是真的吗?


4
如果你保存了一个指向std::function的引用,而被引用的那个std::function超出了其作用域,那么它所包含的内容并不重要,你就会遇到问题。请注意,需要保持原意并简化表述,不得添加解释。 - Xeo
@Alex 包装的函数对象类型,lambda或其他类型都没有关系。这里适用于任何可以形成引用(包括指针)的对象的相同原则。因此,如果您稍后尝试调用(A)通过std::reference_wrapper传递了一个functor的std::function,而所引用的functor的生命周期已经结束,和/或(B)对std::function的引用的参考方的生命周期已经结束,那么将会出现可怕的情况。 - underscore_d
5个回答

76
我可以将函数作为引用存储吗?因为std::function只是一个函数指针,并且保证函数的“可执行代码”留在内存中。
std::function绝不仅仅是一个函数指针。它是一个围绕任意可调用对象的包装器,并管理用于存储该对象的内存。与任何其他类型一样,仅当您有其他方法来保证被引用的对象在每次使用该引用时仍然有效时,才能安全地存储引用。
除非您有存储引用的充分理由和保证其保持有效的方法,否则按值存储。通过const引用传递给构造函数是安全的,而且可能比传递值更高效。通过非const引用传递是不好的,因为它会阻止您传递临时对象,因此用户不能直接传递lambda、bind的结果或任何其他可调用对象,除了std::function本身。

2
确实。我刚意识到(感谢您的回答),std::function是函数指针的包装器,即使函数或lambda保留在内存中,它周围的包装器也不必存在。可能(很可能?)编译器在将lambda传递给我的构造函数时动态生成std::function,这意味着调用后它确实消失了。谢谢。 - Patrick
8
传递值给构造函数,然后使用std::move传递到成员变量中。http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ - Sebastian Redl
4
"不做工作比做一些工作更好 - Going Native 2013"。 创建对象(1个工作),通过常量引用传递(没有工作),使用(1个工作)。创建对象(1个工作),通过值传递并移动(1个工作),使用(1个工作)。因此,在这种情况下,通过常量引用传递更好。 - Jagannath
2
@Jagannath 您假设所有工作的成本相同,这是显然错误的。传递值然后移动(pass-by-value-then-move)很受欢迎,因为它意味着可以移动临时对象。通过 const& 传递会复制它们。将非临时对象按值传递仍然会复制(到参数),然后移动。但是移动通常比复制更便宜,至少在任何一种成本问题都很重要的情况下是如此。因此,将值然后移动作为“3个工作”而不是“2个工作”的错误概括陈述,当(a)在某些情况下它的工作量较少,(b)额外的工作可能微不足道,(c)实际性能取决于整个程序中传递的内容以及比例。 - underscore_d
函数的捕获方式(例如按值=捕获还是按引用&捕获)对此建议有任何影响吗? - Rufus

21
如果您通过引用将函数传递给构造函数,而不进行副本,当该函数在此对象之外的作用域中退出时,引用将不再有效,您将运气不佳。前面的答案已经说过这一点。我想补充的是,相反地,您可以通过而不是引用将函数传递给构造函数。为什么呢?因为您需要它的一个副本,所以如果通过值传递,编译器可以优化掉传递临时变量(例如内联编写的lambda表达式)时不必要的副本。当您将传入的函数分配给变量时,无论如何都可能会产生另一个副本,因此请使用std::move来消除该副本。示例:
class MyClass
{
public:
   typedef std::function<int(int)> MyFunction;

   MyClass (Myfunction myFunction): m_myfunction(std::move(myFunction))
       {}

private:
   MyFunction m_myFunction;
};

所以,如果他们将一个rvalue传递给上面的函数,编译器会优化掉构造函数中的第一个副本,而std::move会删除第二个副本 :)

如果你(仅有的)构造函数采用const引用,无论如何它被传递进来,你都需要在函数中复制它。

另一种方法是定义两个构造函数,分别处理lvalue和rvalue:

class MyClass
{
public:
   typedef std::function<int(int)> MyFunction;

   //takes lvalue and copy constructs to local var:
   MyClass (const Myfunction & myFunction): m_myfunction(myFunction)
       {}
   //takes rvalue and move constructs local var:
   MyClass (MyFunction && myFunction): m_myFunction(std::move(myFunction))
       {}

private:
   MyFunction m_myFunction;
};

现在,你可以通过明确处理rvalues(而不是让编译器为你处理)来消除在这种情况下复制的需要。可能比第一种方法略微更有效,但代码量也更大。

相关参考资料(在这里可能经常看到)(同时也是一个非常好的阅读材料): http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/


3
作为一般规则(特别是在使用高度线程化系统时),应当按值传递(pass by value)。对于引用类型,从线程内部无法验证基础对象是否仍然存在,因此会导致非常严重的竞争和死锁问题。
另一个考虑因素是 std::function 中的任何隐藏状态变量,修改这些变量很可能不是线程安全的。这意味着即使底层函数调用是线程安全的,std::function 包装器中的 "()" 调用仍可能不是线程安全的。可以通过始终使用 std::function 的线程本地副本来恢复所需的行为,因为它们将各自具有状态变量的隔离副本。

3

可以随意复制。它是可复制的。标准库中的大多数算法都要求使用函数对象。

然而,在非平凡情况下,通过引用传递可能会更快,因此建议通过常量引用传递并通过值存储,这样您就不必担心生命周期管理。所以:

class MyClass
{
public:
    typedef std::function<int(int)> MyFunction;
    MyClass (const Myfunction &myFunction);
          // ^^^^^ pass by CONSTANT reference.
private:
    MyFunction m_myFunction;    // Always store by value
};

通过传递常量或rvalue引用,您承诺调用者在仍然可以调用函数的情况下不修改函数。这可以防止您错误地修改函数,并且通常应该避免有意使用它,因为与使用返回值相比,可读性较差。
编辑:我最初说的是“CONSTANT or rvalue”,但Dave的评论让我查了一下,实际上rvalue引用不接受lvalue。

2
嗯...通过 rvalue 引用传递会迫使用户将其“移动”到你这里 - 绝对不是“承诺不修改函数”的说法。另外,更现代的做法是如果要存储,则通过值进行接受(并将其“std :: move”到您的存储成员中),或者通过常量引用进行传递,如果打算不进行存储。 - David
@Dave:你说得对。所以,如果你需要一个函数像方法一样接受左值或右值,那么这仍然是个麻烦。呸。 - Jan Hudec

3
我建议您制作一份副本:
MyFunction m_myFunction; //prefferd and safe!

这是安全的,因为如果原始对象超出其作用域并销毁,则复制品仍将存在于类实例中。


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