【C++11】lambda 表达式

概述
C++ 11中引入了新的lamdba表达式,使用也很简单,我最喜欢的是不用给函数取名称,每次给函数取名称都感觉自己读书太少~

1、lambda表达式
lambda表达式可以理解为一个匿名的内联函数。和函数一样,lambda表达式具有一个返回类型、一个参数列表和一个函数体。与函数不一样的是lambda必须使用尾置返回类型。一个lambda表达式表示一个可调用的代码单元。

语法:[capture list] (parameter list) -> return type {function body}

capture list:表示捕获列表,是一个lambda所在函数中定义的局部变量列表
parameter list:表示参数列表
return type:返回类型
function body:函数体

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体,忽略参数列表等价于指定一个空函数列表,忽略返回类型,lambda会根据函数体中的代码推断出来(如果函数体直接return,则是void类型)。例如:

auto f = [ ] {return 42;};
cout << f() << endl;

lambda的调用方式与普通函数的调用方式相同。

与函数的几点不同在于:

lambda表达式不能有默认参数。因此,一个lambda表达式调用的实参数目永远与形参数目相等。
所有参数必须有参数名。
不支持可变参数。
2、捕获列表
如果没有进行捕获,则lambda表达式函数体内只能使用参数列表中的变量。捕获就是明确的指明lambda能使用的局部变量(指调用lambda的地方局部变量)。

这里记住两点就行了:

lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
捕获列表只用于非静态局部变量,lambda可以直接使用静态局部变量和在它所在函数之外声明的名字。
例如:

void func()
{
static int i = 10;
int j = 20;
auto f1 = [ ] () { return j; }; //编译出错,没有进行捕获,函数体内不能使用
auto f2 = [ ] () { return i; }; //静态变量无需捕获
auto f3 = [j] () { return j; }; //进行了捕获,函数体内可以使用了
};

与函数参数传递类似,变量的捕获方式也可以是值或者引用。这里列出不同的捕获列表的方式,后面进行解释:

2.1、值捕获
与传递参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,不捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。例如:

void func()
{
size_t v1 = 42;
auto f = [ v1 ] { return v1; }; //使用了值捕获,将v1拷贝到名为f的可调用对象
v1 = 0;
auto j = f(); //j为42,f保存了我们创建它是v1的拷贝
}

由于被捕获的值实在lambda创建时拷贝,因此在随后对其修改不会影响到lambda内部对应的值。

默认情况下:如果以传值方式捕获外部变量,则在Lambda表达式函数体中不能修改该外部变量的值。

2.2、引用捕获
和函数引用参数一样,一个引用类型的变量在函数体内改变时,实际上使用的是引用所绑定的对象。

void func()
{
size_t v1 = 42;
auto f = [ &v1 ] { return v1; }; //引用捕获,将v1拷贝到名为f的可调用对象
v1 = 0;
auto j = f(); //j为42,f保存了我们创建它是v1的拷贝
}

如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,这里就会出现问题。

有一些不可拷贝对象,只能使用引用捕获的方式,比如ostream对象。

2.3、隐式捕获
除了显示列出我们希望使用的来自所在函数的局部变量之外,我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。

隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量

int main()
{
int a = 123;
auto f = [ = ] { cout << a << endl; }; //值捕获
f(); // 输出:123

auto f1 = [ & ] { cout << a++ << endl; }; //引用捕获
f1(); //输出:123(采用了后++)

cout << a << endl; //输出 124
}

2.4、混合方式捕获
lambda还支持混合方式捕获,即同时使用显示捕获和隐式捕获。

混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引用捕获 。

需要注意的是:显示捕获的变量必须使用和默认捕获不同的方式捕获。例如:

void func()
{
int i = 10;
int j = 20;
auto f1 = [ =, &i] () { return j + i; }; //正确,默认值捕获,显示是引用捕获
auto f2 = [ =, i] () { return i + j; }; //编译出错,默认值捕获,显示值捕获,冲突了
auto f3 = [ &, &i] () { return i +j; }; //编译出错,默认引用捕获,显示引用捕获,冲突了
};

2.5、修改值捕获的值
在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。

如果你希望被值捕获的值被改变,就必须在参数列表首加上关键字mutable。

语法变为:[capture list] (parameter list) mutable -> return type {function body}

int main()
{
int a = 123;
auto f = [a]()mutable { cout << ++a; }; // 不会报错

cout << a << endl; // 输出:123
f(); // 输出:124
}

3、返回类型
在默认的规则下,返回类型如下:

如果只包含单一的return语句,那么根据return 的类型确定返回类型。
如果除了return 还有别的语句,那么返回void。
所以,有返回类型时,一定要自己显示的进行说明。

4、lambda表达式的本质
当我们编写了一个lambda之后,编译器将该表达式翻译成一个未命名类的未命名对象。该类含有一个重载的函数调用运算符。

4.1、采用值捕获
在采用值捕获时,lambda形成的类相当于含有自己的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。例如两个数加法的方法:

int func()
{
int a =10;
int b = 20;
auto addfun = [=] (const int c ) -> int { return a+c; };

int c = addfun(b);
cout << c << endl;
};

就等同于:

class Myclass
{
public:
Myclass( int a ) : m_a(a){}; //该形参对应捕获的变量

//该调用运算符的返回类型、形参和函数体都与lambda一致
int operator()(const int c) const
{
return a + c;
}

private:
int m_a; //该数据对应通过值捕获的变量
};

lambda表达式产生的类不含有默认构造函数、赋值运算符及默认析构函数。因为不含默认构造函数,因此要想使用这个类必须提供一个实参。

默认情况下,由lambda产生类当中的调用运算符是一个const成员函数,所以值捕获的值不能修改。如果加上mutable相当于去掉const。这样上面的很多限制就能讲通了。

4.2、采用引用捕获
如果lambda采用引用捕获的方式,编译器可以直接使用该引用而无须在lambda对象产生的类中将其存储为数据成员。

唯一需要注意的是,变量将由程序负责确保执行时引用的对象确实存在。

感谢大家,我是假装很努力的YoungYangD(小羊)。

参考资料:
《C++ primer 第五版》
https://www.cnblogs.com/lustar/p/7531605.html

原文地址:https://www.cnblogs.com/SchrodingerDoggy/p/14654401.html