lambda表达式是现代编程语言的一个基础特性,比如LISP、Python、C#等具备该特性。而C++在C++11标准才正式支持lambda表达式。
一、定义
C++11中,lambda表达式的语法非常简单:
[ captures ] ( params ) specifiers exception -> ret { body }
举个例子
int main() {
int x = 5;
auto foo = [x](int y)->int { return x * y; };
std::cout << foo(8) << std::endl;
}
-
captures
捕获列表,上例中捕获了变量x
-
params
可选参数列表,上例中添加了参数y
-
specifiers
可选限定符,C++11中可以用mutable
,它允许我们在lambda表达式函数体内改变按值捕获的变量,或者调用非const的成员函数 -
exception
可选异常说明符,可以使用noexcept
来指明lambda是否会抛出异常 -
ret
可选返回值类型 -
body
lambda表达式的函数体
由于大量的内容都是可选的,所以最简单的lambda表达式是 []()
,虽然很奇怪,但是它合法。
二、捕获列表
2.1、作用域
捕获列表中的变量存在于两个作用域——lambda表达式定义的函数作用域以及lambda表达式函数体的作用域。前者是为了捕获变量,后者是为了使用变量。
捕获的变量必须是一个自动存储类型(非静态局部变量)。
如果我们想要在lambda中使用静态变量,或者一个全局变量怎么办,直接用就好了,本身就在这些变量的作用域内
int x = 1;
int main() {
int y = 2;
static int z = 3;
auto foo = [y] { return x + y + z; };
std::cout << foo() << std::endl;
}
2.2、捕获引用
前面我们都是在捕获值,现在我们来捕获引用
void bar1() {
int x = 5, y = 8;
auto foo = [x, y] {
x += 1; // 编译失败,无法改变捕获变量的值
y += 2; // 编译失败,无法改变捕获变量的值
return x * y;
};
std::cout << foo() << std::endl;
}
void bar2() {
int x = 5, y = 8;
auto foo = [&x, &y] {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}
为什么bar1
会出现编译错误,因为lambda表达式的一个特性:捕获的变量默认为常量。在捕获引用的情况下,捕获变量实际上是一个引用,我们在函数体内改变的并不是引用本身,而是引用的值,所以并没有被编译器拒绝。
mutable
说明符可以移除lambda表达式的常量性:
void bar3() {
int x = 5, y = 8;
auto foo = [x, y] () mutable {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}
上述lambda增加说明符mutable
,还多了一对()
,这是因为语法规定lambda表达式如果存在说明符,那么形参列表不能省略
编译运行bar2和bar3两个函数会输出相同的结果,但这并不代表两个函数是等价的,捕获值和捕获引用还是存在着本质区别。当lambda表达式捕获值时,表达式内实际获得的是捕获变量的复制,我们可以任意地修改内部捕获变量,但不会影响外部变量。
对于捕获值的lambda表达式还有一点需要注意,捕获值的变量在lambda表达式定义的时候已经固定下来了,无论函数在lambda表达式定义后如何修改外部变量的值,lambda表达式捕获的值都不会变化
int main() {
int x = 5, y = 8;
auto foo = [x, &y]() mutable {
x += 1;
y += 2;
std::cout << "lambda x = " << x << ", y = " << y << std::endl;
return x * y;
};
x = 9;
y = 20;
foo();
}
运行结果为:
lambda x = 6, y = 22
2.3、广义捕获
广义捕获有两种方式,第一种是C++11中支持的简单捕获:
1.[this] —— 捕获this指针,捕获this指针可以让我们使用this类型的成员变量和函数。
2.[=] —— 捕获lambda表达式定义作用域的全部变量的值,包括this。
3.[&] —— 捕获lambda表达式定义作用域的全部变量的引用,包括this。
第二种是C++14中支持的初始化捕获:他解决了简单捕获中只能捕获lambda表达式上下文变量,无法捕获表达式结果以及无法自定义捕获变量名的问题。
int main() {
int x = 5;
auto foo = [x = x + 1]{ return x; };
}
这个lambda表达式在C++11是无法编译通过的,但是在C++14可以。这个赋值表达式通过等号跨越了两个作用域,等号左边的变量存在于lambda表达式的作用域,等号右边的变量存在于main函数的作用域。还可以这样写
int main() {
int x = 5;
auto foo = [y = x + 1]{ return y; };
}
如果此时在lambda表达式函数体里使用变量x,则会出现编译错误。
初始化捕获在一些场景下可以起到减少运行开销的作用:比如下面这个例子:
int main() {
std::string x = "hello c++ ";
auto foo = [x = std::move(x)]{ return x + "world"; };
}
使用std::move
对捕获列表变量x进行初始化,这样避免了简单捕获的复制对象操作,代码运行效率得到了提升。
还有就是是在异步调用时复制this对象,防止lambda表达式被调用时因原始this对象被析构造成未定义的行为:
class Work {
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn() {
// 返回32766,因为this指针被析构,无定义行为
return std::async([=]() -> int { return value; });
}
std::future<int> spawn() {
// 返回42,this复制到tmp对象中,然后在函数体内返回tmp对象的value
return std::async([=, tmp = *this]() -> int { return tmp.value; });
}
};
std::future<int> foo() {
Work tmp;
return tmp.spawn();
}
int main() {
std::future<int> f = foo();
f.wait();
std::cout << "f.get() = " << f.get() << std::endl;
}
三、无状态lambda表达式
C++标准对于无状态的lambda表达式有着特殊的照顾,即它可以隐式转换为函数指针
void f(void(*)()) {}
void g() { f([] {}); } // 编译成功
在上面的代码中,lambda表达式[] {}
隐式转换为void(*)()
类型的函数指针。同样,看下面的代码:
void f(void(&)()) {}
void g() { f(*[] {}); }
这段代码也可以顺利地通过编译。我们经常会在STL的代码中遇到lambda表达式的这种应用
四、在STL中使用lambda表达式
在STL中常常会有这样一些函数,它们的形参需要传入一个函数指针或函数对象从而完成整个算法,例如std::sort
、std::find_if
等,在C++11标准之前,通常这个函数需要在外部定义。而lambda表达式可以直接在STL算法函数的参数列表内实现辅助函数
int main() {
std::vector<int> x = {1, 2, 3, 4, 5};
std::cout << *std::find_if(x.cbegin(), x.cend(), [](int i) { return (i % 3) == 0; }) << std::endl;
}
五、泛型lambda表达式
C++14标准让lambda表达式具备了模版函数的能力,我们称它为泛型lambda表达式。虽然具备模版函数的能力,但是它的定义方式却用不到template关键字。实际上泛型lambda表达式语法要简单很多,我们只需要使用auto占位符即可:
int main() {
auto foo = [](auto a) { return a; };
int three = foo(3);
char const* hello = foo("hello");
}
六、捕获[=,*this]
在广义捕获中,我们在捕获列表内复制了一份this指向的对象到tmp,然后使用tmp的value。这样并不优美,如果在lambda表达式中用到了大量this指向的对象,那么必须将所有对象全部修改。为了更方便地复制和使用this对象,C++17增加了捕获列表的语法来简化这个操作,具体来说就是在捕获列表中直接添加[*this],然后在lambda表达式函数体内直接使用this指向对象的成员
class Work{
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn() {
return std::async([=, *this]() -> int { return value; });
}
};
简化了操作,可以看做是语法糖
七、捕获[=,this]
[=]
可以捕获this指针,相似的,[=,\*this]
会捕获this对象的副本。但是在代码中大量出现[=]和[=,\*this]
的时候我们可能很容易忘记前者与后者的区别。为了解决这个问题,在C++20标准中引入了[=, this]
捕获this指针的语法,它实际上表达的意思和[=]
相同,目的是为了方便区分。
[=, this]{}; // C++17 编译报错或者报警告, C++20成功编译
同时用两种语法捕获this指针是不允许的,比如:
[this, *this]{};