-
C++11 中 lambda 函数捕获参数的语义只有两种:按值捕获
[=]
和按引用捕获[&]
,而且只能声明变量名,不支持表达式。这导致了一个问题:无法使用移动构造的参数。这对于拷贝成本高而移动成本低的对象(如STL中的大部分容器)以及只能移动构造的对象(如std::unique_ptr
和std::future
)是很不友好的。 -
C++14 对此做了改进,引入了一种新的捕获机制:初始化捕获(init capture),或者叫广义 lambda 捕获(generalized lambda capture),允许我们以
[param = expr]
的形式使用右侧的表达式来初始化左侧的 lambda 函数的参数。例如:
class Widget { // some useful type
...
}
...
auto pw_var = std::make_unique<Widget>(); // std::unique_ptr
// is move-only type
// configure pw_var...
auto func = [pw_param = std::move(pw_var)]{ // pw_var is move-constructed
return pw_param == nullptr; // into pw_param
};
func();
实际上等号右侧可以是任意表达式,因此上例中如果不需要在创建 func
前对 pw_var
做任何操作,也可以写成:[pw_param = std::make_unique<Widget>()]
。
-
如果不能使用 C++14,使用 C++11 也有解决办法。具体来说有两种:一是使用自定义类(仿函数),二是使用 lambda +
std::bind
。 -
第一种方法:使用仿函数,其实就是C++98时代实现带状态函数的方法。尽管本例不需要用到状态,但我们还是可以借用其灵活性。以上代码等价于:
class WidgetIsNullptr {
public:
using DataType = std::unique_ptr<Widget>;
IsNullptr(DataType&& ptr) : pw(std::move(ptr)) {}
bool operator()()
{ return pw == nullptr; }
private:
DataType pw;
};
...
auto func = WidgetIsNullptr(std::make_unique<Widget>());
func();
- 第二种方法:使用
std::bind
。std::bind
接受一个可调用(Callable)对象f
和若干个参数args
,返回一个新的可调用对象,调用该对象等价于调用f(args)
。其中args
既可以包含调用bind
时立刻给出的参数,也可以包含在调用bind
产生对象时才给出的参数,使用std::placeholders
命名空间下的_1, _2, ...
占位符声明。这里我们把原来的 lambda 函数作为f
,需要移动的对象作为args
调用std::bind
。
如此看起来只是在 lambda 函数外多套了一层能够实现效果的原理是:bind
产生的对象会存储一份入参的拷贝,由于我们提供的入参是std::move(data)
,所以拷贝是通过调用vector
的移动构造函数实现的。而 lambda 函数按引用接受参数——注意,是参数不是从外部域捕获变量,而这个参数由std::bind
绑定到了刚才的(通过移动构造的)std::bind
的入参拷贝上,所以在调用func
时,仍和之前一样不需任何参数。
std::vector<int> data{ 1, 2, 3, 4 };
auto func = std::bind(
[](const std::vector<int>& data)
{ /* uses of data */ },
std::move(data));
func();
尽管在 Item 34 中会说明为何应倾向于使用 lambda 函数而不是 std::bind
,这里介绍的的确是 std::bind
很方便的一种场景。当然,如果能用C++14,那么初始化捕获以及 auto
参数类型会消灭这样的场景。
总结
- 使用 C++14 的初始化捕获以实现将对象移动至闭包(lambda函数)中。
- 在 C++11 中,通过手写类(仿函数)或
std::bind
模仿上面的效果。