在类成员中存储带捕获Lambda的最佳实践与常见疑问
构造函数参数的正确声明方式
优先用模板参数接受任意可调用对象,或者退而求其次用传值+std::move的方式,不推荐单独用左值/右值引用。
三种参数方式的优劣:
std::function<int(int)>(传值):
兼容性最强,左值(已命名的std::function变量)和右值(临时Lambda)都能接。缺点是对左值会多一次拷贝,但结合std::move能把开销降到最低。std::function<int(int)>&(左值引用):
只能接受左值,无法直接传临时Lambda(临时对象是右值),限制太大,基本不用。std::function<int(int)>&&(右值引用):
只能接受右值,没法传入已存在的std::function变量,灵活性不足。
最优写法推荐:
写法1:模板完美转发(效率最高)
直接接受任意可调用对象,避免提前构造std::function的开销:
template<typename Callable> FuncCaller(Callable&& callable) : m_func(std::forward<Callable>(callable)) {}
这个写法会把传入的Lambda、函数指针、其他std::function等可调用对象完美转发给成员m_func的构造函数,最大程度减少拷贝/移动。
写法2:传值+std::move(简单易用)
如果不想用模板,就用传值参数,再在初始化列表里move:
FuncCaller(std::function<int(int)> call_later) : m_func(std::move(call_later)) {}
对于右值(比如临时Lambda),参数call_later会通过移动构造生成,再move给成员;对于左值,会先拷贝参数,再move给成员,总开销比直接拷贝成员小很多。
存入成员变量时是否要用std::move()
必须用。
std::function内部会持有可调用对象的副本,拷贝std::function可能涉及堆内存的分配和复制。而构造函数的参数call_later是局部对象,我们不需要保留它的内容,用std::move()可以直接把call_later的内部资源转移到成员m_func,完全避免拷贝操作,开销几乎为0。
对比两种初始化方式:
- 不用move:
m_func(call_later)→ 触发std::function的拷贝构造,开销大; - 用move:
m_func(std::move(call_later))→ 触发std::function的移动构造,仅转移内部指针,开销极小。
按引用捕获的Lambda被复制后会发生什么
按引用捕获的Lambda,内部存储的是对原对象的引用,而非对象的副本。当Lambda被复制时,复制后的Lambda仍然指向同一个原对象。
这会带来致命风险:如果原对象的生命周期比Lambda短,调用Lambda时就会访问悬空引用,导致程序崩溃或出现未定义行为。
比如你的例子:
ComplexNumber cn(1, 4); FuncCaller another_one([&cn](int a) { return (int)(cn.rational * a); });
如果cn在another_one之前被销毁(比如cn是局部变量,出了作用域),调用another_one.DoThings()时,访问cn.rational就是访问已销毁的内存,后果不堪设想。
解决办法:
- 确保原对象生命周期长于Lambda:比如把
cn声明为全局变量,或者和another_one同生命周期; - 改用值捕获:把
[&cn]改成[cn],这样Lambda会持有cn的副本,即使原对象销毁,Lambda仍能正常工作。
修正后的完整示例代码
#include <functional> class ComplexNumber { public: int rational; ComplexNumber(int r, int) : rational(r) {} }; class FuncCaller { public: // 模板完美转发版本(推荐) template<typename Callable> FuncCaller(Callable&& callable) : m_func(std::forward<Callable>(callable)) {} // 或者传值+move版本 // FuncCaller(std::function<int(int)> call_later) : m_func(std::move(call_later)) {} int DoThings(int i) { return m_func(i); } private: std::function<int(int)> m_func; }; int main() { float b = 5.3; // 值捕获Lambda,无生命周期隐患 FuncCaller c([b](int a){ return (int)(a * b); }); ComplexNumber cn(1, 4); // 按引用捕获时,确保cn的生命周期不短于another_one FuncCaller another_one([&cn](int a) { return (int)(cn.rational * a); }); }
内容的提问来源于stack exchange,提问作者bpeikes

