将捕获变量的lambda存入std::function产生额外拷贝如何解决?
额外拷贝产生的原理
修改后版本的2次拷贝来源
- 第一次拷贝是预期内的行为:
foo函数中按值捕获const Test& t时,将入参t拷贝到lambda的成员变量中。 - 第二次拷贝来自
std::function的构造过程:你将捕获了Test实例的lambda传递给queue的std::function<void()>形参时,std::function需要将lambda存储到自身的内部存储空间。由于你的Test类没有为移动构造函数标注noexcept,为了保证异常安全,std::function会选择拷贝lambda对象,连带拷贝lambda内部存储的Test成员,这就是多出来的额外拷贝。
最初模板版本多1次拷贝的原因
最初的模板版queue将lambda完美转发给std::function构造时,会额外触发一次lambda的拷贝:第一次是lambda捕获时的拷贝,第二次是构造临时std::function时的拷贝,第三次是把临时std::function赋值给全局queueFunction时的拷贝。修改为值传递std::function后,赋值阶段改为移动std::function(std::function的移动操作仅转移内部指针,不会拷贝存储的对象),所以减少了一次拷贝。
核心约束
std::function的类型擦除机制要求存储的可调用对象必须满足可拷贝构造的要求,哪怕你从未拷贝过这个std::function实例,这个静态约束依然存在。当传入的可调用对象是右值时,只要其移动构造是noexcept的,std::function会优先选择移动而不是拷贝来构造自身,不会触发内部存储对象的拷贝。
优化方案(仅保留1次拷贝)
方案1:为Test的移动操作添加noexcept标注
修改Test类的移动构造和移动赋值函数,添加noexcept关键字:
Test(Test&& other) noexcept : _a(std::move(other._a)) { std::cout << "Test: Move Constructor" << std::endl; } Test& operator=(Test&& other) noexcept { if (this != &other) { _a = std::move(other._a); std::cout << "Test: Move Assignment Operator" << std::endl; } return *this; }
修改后std::function构造时会移动lambda对象,额外的拷贝会替换为无开销的移动操作,仅保留捕获时的1次拷贝。
方案2:值传递+移动捕获(C++14及以上)
修改foo的入参为值传递,结合C++14的lambda初始化捕获将参数移动到lambda中:
void foo(Test t) { std::cout << "foo()" << std::endl; queue([t = std::move(t)](){ _foo(t); }); }
如果调用foo时传入右值Test对象,全程没有拷贝操作,只有移动;如果传入左值Test对象,仅会在入参构造时发生1次拷贝,后续都是移动操作。
方案3:用智能指针包装
如果无法修改Test类的定义,可以用std::shared_ptr<Test>包装对象后捕获,只会拷贝指针,Test实例仅拷贝1次:
void foo(const Test& t) { std::cout << "foo()" << std::endl; auto pt = std::make_shared<Test>(t); queue([pt](){ _foo(*pt); }); }
内容的提问来源于stack exchange,提问作者Patrick Wright

