使用 lambda 创建 std::function 会导致多余的 lambda 对象复制 - 为啥?

Posted

技术标签:

【中文标题】使用 lambda 创建 std::function 会导致多余的 lambda 对象复制 - 为啥?【英文标题】:Creating std::function with lambda causes superfluous copying of the lambda object - why?使用 lambda 创建 std::function 会导致多余的 lambda 对象复制 - 为什么? 【发布时间】:2014-06-24 06:26:44 【问题描述】:

当我用捕获值的 lambda 构造 std::function 时,它会额外复制(移动)这些参数(实际上是我猜的整个 lambda 对象)。 代码:

#include <iostream>
#include <functional>

// Testing class - just to see constructing/destructing.
class T 
private:
    static int idCounter; // The global counter of the constructed objects of this type.
public:
    const int id; // Unique object ID 

    inline T() : id(++idCounter)  
        std::cout << "  Constuctor Id=" << id << std::endl;
    ;
    inline T(const T& src) : id(++idCounter) 
        std::cout << "  Copy constructor Id=" << id << std::endl;
    
    inline T(const T&& src) : id(++idCounter) 
        std::cout << "  Move constructor Id=" << id  << std::endl;
    
    inline void print() const 
        std::cout << "  Print is called for object with id=" << id << std::endl;
    
    inline ~T() 
        std::cout << "  Destructor Id=" << id << std::endl;
    
;

int T::idCounter=0; 

// Declare type of the std::function to store our lambda.
typedef std::function<int (void)> Callback;

int main()
 
    std::cout << "Let's the game begin!" << std::endl;
    T obj; // Custruct the first object.
    std::cout << "Let's create a pointer to the lambda." << std::endl;
    // Make a labmda with captured object. (The labmda prints and returns object's id).
    // It should make one (local) copy of the captured object but it makes it twice - why?!
    const Callback* pcb= new Callback( [obj]() -> int  
        obj.print();
        return obj.id; 
     );
    std::cout << "Now let's print lambda execution result."  << std::endl;
    std::cout << "The functor's id is " << (*pcb)() << std::endl;
    std::cout << "Destroying the lambda." << std::endl;
    delete pcb;
    std::cout << "Terminating." << std::endl;
    return 0;


输出是:

让我们的游戏开始! 构造函数 ID=1 让我们创建一个指向 lambda 的指针。 复制构造函数 ID=2 移动构造函数 Id=3 析构函数 ID=2 现在让我们打印 lambda 执行结果。 为 id=3 的对象调用打印 函子的 id 是 3 销毁 lambda。 析构函数 ID=3 终止。 析构函数 ID=1

我用捕获的对象制作了一个带有 lambda 的 std:function。它应该为 lambda 制作对象的本地副本,但它会复制两次(查看移动构造函数调用 - 以粗体突出显示)。实际上它会复制整个 lambda 对象。为什么?我怎样才能避免这种情况? 我正在使用 lambdas 进行线程间事件处理,它们可能会捕获大量的日期,所以我试图找到一种方法来避免不必要的复制。所以任务很简单——以最少的费用将构造的 lambda 传递给函数——如果它会为每个构造的 lambda 复制两次数据,我会寻找另一种处理事件的方法。 我正在使用强制 GNU C++11 的 GCC v4.7.2。

【问题讨论】:

移动 std::function 的构造函数的初始化列表中的 lambda 时完成移动。这个 moving-the-lambda 强制捕获的对象也移动(即递归移动!)> @op,移动不是复制(当然你可以这样实现,但你为什么要这样做呢?)。您的测试类的一个明智的实现是不增加 id,而是将移动(临时)对象的 id 带到新实例。 在复杂项目的现实生活中,您不能保证搬家是便宜的。您正在使用第三方库、多线程问题等。例如 - 使用 10k 字符串移动 sdt:vector 是否便宜? @user3544995 是的。大约四个指针分配。 【参考方案1】:

好吧,输出令人困惑,因为编译器执行了一次复制省略。所以为了理解行为,我们需要暂时禁用复制省略。编译代码时使用-fno-elide-constructors标志:

$ g++ -std=c++11 -fno-elide-constructors main.cpp

现在它给出了这个输出 (demo-without-copy-elision):

Let's create a pointer to the lambda.
  Copy constructor Id=2
  Move constructor Id=3
  Move constructor Id=4
  Destructor Id=3
  Destructor Id=2

嗯,这是意料之中的。 copy 在创建 lambda 时完成:

 [obj]() -> int  

//^^^^ COPY!

    obj.print();
    return obj.id; 

嗯,这太明显了!

现在来到不明显的事情:两个移动操作!

first move是在将lambda传递给std::function的构造函数时完成的,因为lambda是一个rvalue,因此move -构造函数被调用。请注意,-fno-elide-constructors 也禁用了移动省略(毕竟这只是所谓的更快的复制版本!)。

second move 完成,当在构造函数初始化中写入(当然是movingstd::function 的成员数据时-列表。

到目前为止一切顺利。

现在,如果您删除 -fno-elide-constructors,编译器会优化掉 first move(因为它不会调用 move 构造函数),这就是为什么输出是这样的:

Let's create a pointer to the lambda.
  Copy constructor Id=2
  Move constructor Id=3
  Destructor Id=2

见demo-with-copy-elision。

现在你看到的 move 是因为将 lambda 移动到 std::function 的成员数据中。你不能避免这个移动

另请注意,复制/移动 lambda 也会导致复制/移动捕获的数据(即递归复制/移动)。

无论如何,如果您担心复制捕获的对象(假设它是一个巨大的对象),那么我建议您使用new 创建捕获的对象,以便复制捕获的对象意味着复制指针(4或 8 个字节!)。那应该很好用!

希望对您有所帮助。

【讨论】:

我可以避免吗?我可以在不(第二次)移动的情况下指向 lambda 吗? 指向 lambda 的指针?我认为没有任何干净的方法可以做到这一点(甚至不可能)。无论如何,如果您担心复制捕获的对象(假设它是巨大的对象),那么我建议您使用 new 创建捕获的对象,以便复制捕获的对象意味着复制一个指针(4 或 8 个字节! )。【参考方案2】:

它不会复制两次。搬家被认为是一种廉价的操作,实际上在 99% 的情况下都是如此。对于“计划旧数据”类型(结构、整数、双精度、...),双重复制不是问题,因为大多数编译器会消除冗余副本(数据流分析)。对于容器来说,移动是一种非常便宜的操作。

【讨论】:

我正在使用大而复杂的外部数据对象。 “搬家就是廉价操作”不是重点。有时它可能很便宜。但可能不是。而且信仰不是编程的好方法。正如我所见,在 GCC C++11 中,如果不进行双重复制,就不可能使用 lambdas :( “外部”是什么意思?你不能为你的数据对象写一个“便宜”的移动构造函数吗? 在“外部”中,我的意思是捕获的对象是在创建 lambda 的函数之外创建的,因此通常不能像本地创建的对象那样优化它们。并且正在使用第三方库,所以我不能确定所有数据都有效地支持移动构造函数。是的,我可以编写包装器或使用共享指针,但令我惊讶的是,没有双重复制就无法使用(存储)lambda。【参考方案3】:

正如 cmets 中的Nawaz 所提到的,当 lambda 表达式移动到 std::function&lt;int(void)&gt;(类型定义为 Callback )。

const Callback* pcb= new Callback( [obj]() -> int  
    obj.print();
    return obj.id; 
 );

这里的对象obj是通过值(复制构造)传递给lambda表达式,但另外,整个lambda表达式作为r值传递给Callbackstd::function)的构造函数,并且是因此移动复制到 std::function 对象中。移动 lambda 时,所有状态也必须一起移动,因此 obj 也被移动(实际上涉及 obj 的两个移动构造,但其中一个通常由编译器优化)。

等效代码:

auto lambda = [obj]() -> int                         // Copy obj into lambda.
    obj.print();
    return obj.id; 
;

const Callback* pcb= new Callback(std::move(lambda)); // Move lambda (and obj).

移动操作被认为是廉价的,不会导致任何昂贵的数据复制(在大多数情况下)。

您可以在此处阅读有关移动语义的更多信息:What are move semantics?。

最后如果您不想复制obj,那么只需在 lambda 中通过 reference 捕获它:

const Callback* pcb= new Callback( [&obj]() -> int  
    obj.print();
    return obj.id; 
 );

【讨论】:

是否可以在不重复复制的情况下使用 lambda?我所需要的只是一个“指向 lambda 的指针”。使用 std:function 双重复制是不可避免的,不是吗?这是个坏消息。 @user3544995 您不必将 lambda 封装在 std::function 中。只需直接调用它,例如auto pcb = [&amp;obj] /* ... */ ; std::cout &lt;&lt; "The functor's id is " &lt;&lt; pcb() &lt;&lt; std::endl;。这样就不必再制作副本了。 Heh :) 直接调用它...那么存储并将它们传递给其他函数呢?对于事件处理,我需要将 lambda 存储到事件对象中,传递给另一个函数(实际上是另一个线程)然后执行。如何使用自动类型来做到这一点?包含 lambda 的对象字段将具有什么类型? @user3544995 通过使用模板,编译器可以推断出 lambda 的类型,以便您可以在函数之间传递对它的引用。如果涉及线程并且您必须制作本地副本以避免数据竞争,那么您必须无论如何都要制作副本。 @user3544995 引用任何可调用对象(如 lambda)并调用它的函数示例:template &lt;typename T&gt; void func(const T&amp; t) t(); 。不涉及复制。

以上是关于使用 lambda 创建 std::function 会导致多余的 lambda 对象复制 - 为啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用承担角色从 lambda 访问 S3

使用 terraform 创建 lambda 函数

来自 Lambda 的 AWS Cognito adminCreateUser,使用 Amplify CLI 创建

如何创建可变参数泛型 lambda?

使用 Java 8 Lambda 创建 ArrayList

使用 cloudformation 基于 lambda 持续时间指标创建 cloudwatch 警报