【问题标题】:Should I copy an std::function or can I always take a reference to it?我应该复制一个 std::function 还是总是可以参考它?
【发布时间】:2012-02-01 10:35:02
【问题描述】:

在我的 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 生成代码,并在应用程序运行时将此可执行代码保存在“虚拟”内存中。因此可执行代码永远不会被“删除”,我可以安全地存储对它的引用。但这真的是真的吗?

【问题讨论】:

  • std::function 包含的内容无关紧要...如果您保存对一个的引用,而被引用的那个超出范围,您就会遇到问题。
  • @Alex 包装函数对象的类型,lambda 或其他,没有区别。与可以形成引用(包括指针)的任何对象完全相同的原则适用于此处。因此,如果您稍后尝试调用 (A) 一个 std::function 通过 std::reference_wrapper 传递了一个函子,被引用的函子的生命周期已经结束,和/或 (B) 对 std::function 的引用,其中被引用者的生命周期已经结束。

标签: c++ lambda std-function


【解决方案1】:

由于 std::function 只是一个函数指针并且函数的“可执行代码”保证保留在内存中,我可以将函数存储为引用吗?

std::function 不仅仅是一个函数指针。它是任意可调用对象的包装器,并管理用于存储该对象的内存。与任何其他类型一样,存储引用是安全的只有在您有其他方法来保证所引用的对象在使用该引用时仍然有效。

除非您有充分的理由存储引用,并且有办法保证它仍然有效,否则按值存储它。

通过const 引用传递给构造函数是安全的,并且可能比传递值更有效。通过非const 引用传递是个坏主意,因为它会阻止您传递临时值,因此用户不能直接传递 lambda、bind 的结果或除 std::function&lt;int(int)&gt; 本身之外的任何其他可调用对象.

【讨论】:

  • 确实如此。我刚刚意识到(感谢您的回答) std::function 是函数指针的包装器,即使函数或 lambda 保留在内存中,它周围的包装器也不必是。可能(可能?)编译器在将 lambda 传递给我的构造函数时动态生成一个 std::function ,这意味着它确实在调用之后消失了。谢谢。
  • 按值传递给构造函数,然后 std::move 进入成员。 cpp-next.com/archive/2009/08/want-speed-pass-by-value
  • “不做任何工作总比做一些工作要好 - Going Native 2013”​​。创建对象(1 个工作),通过 const ref(无工作),使用(1 个工作)。创建对象(1 个工作),按值传递和移动(1 个工作),使用(1 个工作)。所以在这种情况下,通过 const ref 传递更好。
  • @Jagannath 你假设所有工作的成本都相同,这是微不足道的错误。 Pass-by-value-then-move 很受欢迎,因为这意味着可以将临时对象移入。通过const&amp; 传递会复制它们。按值传递非临时值仍然会复制(到 arg)然后移动。但是搬家通常比复制便宜,至少在两者的成本都很重要的地方。因此,当(a)在某些情况下它的工作量较少时,(a)任何额外的工作都可能是微不足道的,(b)真正的表现取决于在整个程序中传递了什么,以何种比例传递
  • 函数的捕获(例如,按值捕获= 与按引用捕获&amp;)对这个建议有影响吗?
【解决方案2】:

如果您通过引用将函数传递给构造函数,并且不对其进行复制,那么当函数超出此对象之外的范围时,您将不走运,因为引用将不再有效。前面的答案已经说了这么多。

我想补充的是,您可以通过而不是引用将函数传递给构造函数。为什么?好吧,无论如何您都需要它的副本,因此如果您按值传递,编译器可以优化在传入临时值时制作副本的需要(例如就地编写的 lambda 表达式)。

当然,无论您做什么,当您将传入的函数分配给变量时,您可能会制作另一个副本,因此请使用std::move 来消除该副本。示例:

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

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

private:
   MyFunction m_myFunction;
};

因此,如果它们将右值传递给上面,编译器会将第一个副本优化到构造函数中,而 std::move 会删除第二个 :)

如果您的(唯一)构造函数采用 const 引用,则您需要在函数中复制它,无论它是如何传入的。

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

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;
};

现在,您以不同的方式处理右值,并通过显式处理它(而不是让编译器为您处理它)消除在这种情况下复制的需要。可能比第一个更高效,但代码也更多。

(可能在这里看到了很多)相关参考资料(并且非常好读): http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

【讨论】:

    【解决方案3】:

    我建议你复印一份:

    MyFunction m_myFunction; //prefferd and safe!
    

    这是安全的,因为如果原始对象超出范围自毁,则副本仍将存在于类实例中。

    【讨论】:

      【解决方案4】:

      尽可能多地复制。它是可复制的。标准库中的大多数算法都要求函子是。

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

      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
      };
      

      通过常量或右值引用传递你向调用者保证你不会修改函数,但你仍然可以调用它。这可以防止您错误地修改函数,并且通常应该避免故意这样做,因为它比使用返回值可读性差。

      编辑:我最初在上面说“常量或右值”,但戴夫的评论让我查了一下,确实右值引用不接受左值。

      【讨论】:

      • Uhhh... 通过右值引用传递强制用户将move 发送给您 - 绝对不是“您不会修改函数的承诺”。此外,更现代的做法是,如果您要存储它(并将 std::move 它存储到您的存储成员中),则按值接受,或者如果您不打算存储它,则通过常量引用传递。
      • @Dave:你是对的。因此,如果您需要一个函数来获取 l-or-rvalue 就像方法在调用者中所做的那样,那么它仍然很痛苦。糟糕。
      【解决方案5】:

      作为一般规则(特别是如果您将这些用于某些高度线程化的系统),请按值传递。确实没有办法从线程中验证底层对象是否仍然存在引用类型,因此您将面临非常讨厌的竞争和死锁错误。

      另一个考虑因素是 std::function 中的任何隐藏状态变量,对其进行修改不太可能是线程安全的。这意味着即使底层函数调用是线程安全的,std::function 包装器的“()”调用也可能不是。您可以通过始终使用 std::function 的线程本地副本来恢复所需的行为,因为它们每个都有一个独立的状态变量副本。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-02-05
        • 1970-01-01
        • 1970-01-01
        • 2011-04-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多