使用 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 完成,当在构造函数初始化中写入(当然是moving)std::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<int(void)>
(类型定义为 Callback
)。
const Callback* pcb= new Callback( [obj]() -> int
obj.print();
return obj.id;
);
这里的对象obj
是通过值(复制构造)传递给lambda表达式,但另外,整个lambda表达式作为r值传递给Callback
(std::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 = [&obj] /* ... */ ; std::cout << "The functor's id is " << pcb() << std::endl;
。这样就不必再制作副本了。
Heh :) 直接调用它...那么存储并将它们传递给其他函数呢?对于事件处理,我需要将 lambda 存储到事件对象中,传递给另一个函数(实际上是另一个线程)然后执行。如何使用自动类型来做到这一点?包含 lambda 的对象字段将具有什么类型?
@user3544995 通过使用模板,编译器可以推断出 lambda 的类型,以便您可以在函数之间传递对它的引用。如果涉及线程并且您必须制作本地副本以避免数据竞争,那么您必须无论如何都要制作副本。
@user3544995 引用任何可调用对象(如 lambda)并调用它的函数示例:template <typename T> void func(const T& t) t();
。不涉及复制。以上是关于使用 lambda 创建 std::function 会导致多余的 lambda 对象复制 - 为啥?的主要内容,如果未能解决你的问题,请参考以下文章