lambda 使得 c++ 更有魅力,使用 lambda 可以创建一个可调用的对象,一般用于以下场合:
快速创建一个简短的函数,仅在父函数中使用,避免繁琐的创建成员函数的过程。
为标准库的算法(std::find_if,std::sort
等),优雅的创建一个可调用对象。
快速为标准库创建删除器,比如std::unique_ptr,std::shared_ptr
等
避免使用默认捕获 默认捕获会被编码者带来一定的误导,忽视一些错误捕获或无法捕获的场景。
google 编码规范中,也 提到了这点 。
引用捕获要注意被捕获对象的作用域 如果引用捕获对象的作用域是局部作用域,而 lambda 对象的使用超出了该作用域则会导致引用指向的对象无意义,最终会导致 undefined behavior。
比如设计一个容器,里面包含了函数对象:
1 2 3 using FilterContainer = std::vector<std::function<bool (int )>>;FilterContainer filters;
然后在一个函数中,插入可调用对象到容器中:
1 2 3 4 5 6 7 8 void addDivisorFilter () { auto calc1 = computeSomeValue1 (); auto calc2 = computeSomeValue2 (); auto divisor = computeDivisor (calc1, calc2); filters.emplace_back ( [&](int value) { return value % divisor == 0 ; } ); }
由于divisor
变量在退出函数后,其栈内存就被回收了,所以当该 lambda 被调用时,就是 undefined behavior。
为了避免这种情况,应该使用 passed by value 的形式形成闭包:
1 2 3 4 5 6 7 8 void addDivisorFilter () { auto calc1 = computeSomeValue1 (); auto calc2 = computeSomeValue2 (); auto divisor = computeDivisor (calc1, calc2); filters.emplace_back ( [=](int value) { return value % divisor == 0 ; } ); }
同理,如果捕获的参数是一个指针,也需要注意指针指向的内存被释放的情况。
使用默认捕获很容易让人忽视这类问题,而在捕获位置明确指出需要捕获的对象,则更容易提醒编码人员。
捕获成员变量 需要理解的是:类中的成员变量的完整形式其实是:this->member_variable
比如下面这段代码是会编译错误的:
1 2 3 4 5 6 7 8 9 10 11 12 class Hello {public : void DoSomething (void ) { auto func = [val_]() { std::cout << "The value of val is " << val_ << "\n" ; }; func (); } private : int val_ = {10 }; };
因为val_
变量实际上是this->val_
。
而使用默认捕获是可以编译通过的:
1 2 3 4 5 6 7 8 9 10 11 12 class Hello {public : void DoSomething (void ) { auto func = [=]() { std::cout << "The value of val is " << val_ << "\n" ; }; func (); } private : int val_ = {10 }; };
实际上,这就等同于:
1 2 3 4 5 6 7 8 void DoSomething (void ) { auto obj_ptr = this ; auto func = [obj_ptr]() { std::cout << "The value of val is " << obj_ptr->val_ << "\n" ; }; func (); }
这就容易出现问题了,如果代码也是按照前面那一节将lambda
对象放入容易,以后再来调用。 那么该对象完全可能在调用之前就被析构了,后来的操作就又是 undefined behavior。
最好的方式就是来捕获成员变量的拷贝:
1 2 3 4 5 6 7 8 9 void DoSomething (void ) { int val_copy = val_; auto func = [val_copy]() { std::cout << "The value of val is " << val_copy << "\n" ; }; func (); }
c++ 14 还有更加简单的写法:
1 2 3 4 5 6 7 8 void DoSomething (void ) { auto func = [val_ = val_]() { std::cout << "The value of val is " << val_ << "\n" ; }; func (); }
静态存储变量与捕获 实际上,lambda 无法捕获静态存储变量而形成闭包:
这里的静态存储变量包括:全局变量、在命名空间内的变量、static
修饰的变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <cmath> #include <chrono> #include <cstdio> #include <vector> #include <iostream> int main () { static int v = 10 ; auto func = [=]() { std::cout << "The value of v is " << v << "\n" ; }; v = 3 ; func (); return 0 ; }
输出为:
使用初始捕获来完成移动对象的闭包 如果有些对象(比如容器)以拷贝的方式形成闭包,其效率太低了。这种情况下应该以移动的方式来形成闭包。
c++14 有现成的语法支持,称之为初始捕获(init capture)
基于 c++ 14 的初始捕获 所谓的初始捕获,其实简单来讲就是:使用局部变量来初始化 lambda 表达式闭包中的变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream> #include <string> #include <utility> #include <vector> #include <array> int main (int argc, char *argv[]) { std::vector<int > vec = {1 , 2 , 3 , 4 , 5 }; auto func = [v = std::move (vec)]() { std::cout << "The contenes of v are:\n" ; for (auto val : v) { std::cout << val << "," ; } std::cout << "\n" ; }; func (); std::cout << "vec size " << vec.size () << "\n" ; return 0 ; }
基于 c++ 11 的初始捕获 由于 c++ 11 没有语法支持,所以需要借助std::bind
来完成这个需求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <string> #include <utility> #include <vector> #include <array> #include <functional> int main (int argc, char *argv[]) { std::vector<int > vec = {1 , 2 , 3 , 4 , 5 }; auto func = std::bind ( [](const std::vector<int >& v) { std::cout << "The contenes of v are:\n" ; for (auto val : v) { std::cout << val << "," ; } std::cout << "\n" ; }, std::move (vec) ); func (); std::cout << "vec size " << vec.size () << "\n" ; return 0 ; }
lambda 与完美转发 c++14 中,lambda 的参数可以由auto
推导,这就如同模板一样:
1 2 3 4 5 6 7 8 9 10 auto f = [](auto x){ return func (normalize (x)); };class SomeCompilerGeneratedClassName {public : template <typename T> auto operator () (T x) const { return func (normalize (x)); } … };
但需要注意的是,如果 lambda 中调用了其他对象,且具有左值及右值对应的版本那么就需要使用完美转发:
1 2 auto f = [](auto && x) { return func (normalize (std::forward<???>(x))); };
但问题是,std::forward
中并无法确定参数类型。
这个时候就可以使用decltype
来推导类型:
1 2 auto f = [](auto && x) { return func (normalize (std::forward<decltype (x)>(x))); };
除此之外,c++14 的 lambda 还支持变参数:
1 2 3 4 5 6 auto f = [](auto &&... params) { return func (normalize (std::forward<decltype (params)>(params)...)); };
lambda 优于 std::bind lambad 综合上优于 std::bind
,最主要的是其优异的可读性。
比如要编写一个声音报警程序,先声明以下类型:
1 2 3 4 5 6 7 8 9 using Time = std::chrono::steady_clock::time_point;enum class Sound { Beep, Siren, Whistle };using Duration = std::chrono::steady_clock::duration;void setAlarm (Time t, Sound s, Duration d) ;
然后以 lambda 的方式来封装,产生一个 1 小时后响铃 30 秒的可调用对象:
1 2 3 4 5 6 7 8 9 10 11 12 auto setSoundL = [](Sound s) { using namespace std::chrono; setAlarm (steady_clock::now () + hours (1 ), s, seconds (30 )); };
基于 c++ 14 的话,还可以再次简化:
1 2 3 4 5 6 7 8 9 auto setSoundL = [](Sound s) { using namespace std::chrono; using namespace std::literals; setAlarm (steady_clock::now () + 1 h, s, 30 s); };
但使用std::bind
来实现同样的可调用对象,则要麻烦得多,并且可读性很差:
1 2 3 4 5 6 7 8 9 10 11 12 using namespace std::chrono; using namespace std::literals;using namespace std::placeholders; auto setSoundB = std::bind (setAlarm, std::bind (std::plus<>(), steady_clock::now (), 1 h), _1, 30 s);