lambda
C++11支持lambda表达式。lambda表达式是C++一个非常重要的内容。
lambda表达式本质是一个匿名的函数对象 。由于lambda表达式语法上没有类型,我们就用auto或模板参数定义的对象接收lambda表达式。
与普通函数不同的是,lambda表达式可以定义在函数内部。
1、语法
cpp
[capture-list] (parameters)-> return type {function body}
其中,
- capture-list:捕捉列表 。捕捉列表不能省略。(捕捉列表具体内容下文会讲解)
- parameters:参数列表 。lambda表达式的参数列表可以类比普通函数的参数列表。当lambda表达式设计成无参数需传入时,参数列表可以省略。
- return type:返回类型 。lambda表达式将返回类型放在函数声明的尾部,当函数的返回类型比较长的时候我们就可以这么做,使得函数名不会被遮挡。返回类型可以省略,因为编译器可以根据返回值进行推导。
- function body:函数体 。类比普通函数的函数体。函数体不可省略。
2、应用
lambda表达式出现之前,我们只能显式编写仿函数(或者使用函数指针),然后传递仿函数对象达成我们的预期:
cpp
struct Goods
{
string _name;
double _price;
int _evaluate;
Goods(const string& name, double price, int evaluate)
:_name(name)
,_price(price)
,_evaluate(evaluate)
{}
};
struct ComparePriceLess // 价格升序
{
bool operator() (const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ComparePriceGreater // 价格降序
{
bool operator() (const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
struct CompareEvaluateLess // 评价升序
{
bool operator() (const Goods& g1, const Goods& g2)
{
return g1._evaluate < g2._evaluate;
}
};
struct CompareEvaluateGreater // 评价降序
{
bool operator() (const Goods& g1, const Goods& g2)
{
return g1._evaluate > g2._evaluate;
}
};
void test2()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 },
{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
// 显式定义仿函数并传入
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), CompareEvaluateLess());
sort(v.begin(), v.end(), CompareEvaluateGreater());
}
这样一来,每次增加一个新需求就要添加一个新的仿函数,并且想搞清楚当前仿函数的实际功能就必须向上寻找仿函数,那么代码的设计就不那么简洁,代码的可读性就没有那么好。
更有甚者,如果有人对仿函数取的名字是Compare1, Compare2...那么对于后来维护代码的人来说,想要弄懂Compare1, Compare2...到底是什么意思,就必须不厌其烦地向上查找定义。
这时我们就可以利用lambda表达式:
cpp
void test2()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 },
{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
// 显式定义仿函数并传入
//sort(v.begin(), v.end(), ComparePriceLess()); // 价格升序
//sort(v.begin(), v.end(), ComparePriceGreater()); // 价格降序
//sort(v.begin(), v.end(), CompareEvaluateLess()); // 评价升序
//sort(v.begin(), v.end(), CompareEvaluateGreater()); // 评价降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 价格升序
{
return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 价格降序
{
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 评价升序
{
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 评价降序
{
return g1._evaluate > g2._evaluate;
});
}
其实上面代码对于lambda的使用,体现了模板参数定义的对象接收lambda表达式这一点。
用lambda表达式代替显式定义的仿函数,既能使代码变得更加简洁,又增加了代码的阅读性。
lambda表达式的应用很广泛,我们在接下来的学习中也会碰到。
3、捕捉列表
lambda表达式可以使用传入的参数对象,也可以使用lambda表达式所在函数域内的局部对象,这时局部对象就需要用捕捉列表进行捕捉:

捕捉列表捕捉对象的方式有值捕捉 和引用捕捉:
- 值捕捉的方式是直接在捕捉列表中放上对象 。值捕捉而来的对象不能直接在lambda表达式的函数体内修改。
- 引用捕捉的方式相比值捕捉添加了一个& 。引用捕捉而来的对象可以在函数体内修改,然而这种修改会在lambda函数体外造成影响。
cpp
void test4()
{
int i = 1;
double d = 1.1;
auto sum = [i, &d](int a, int b)
{
//++i; // 值捕捉对象,不能修改,报错
++d; // 引用捕捉对象,可以修改
return i + d + a + b;
};
cout << sum(1, 2) << endl; // 结果是:6.1
cout << d << endl; // 结果是2.1
// 说明lambda函数体内引用捕捉对象的修改会影响到函数体外
}
要想在lambda函数体内,修改值捕捉对象,也不是不行,只需在lambda表达式后加上关键字mutable。然而对于值捕捉对象的修改,并不会影响到lambda函数体外。
cpp
void test5()
{
int i = 1;
double d = 1.1;
auto sum = [i, &d](int a, int b) mutable
{
++i;
return i + d + a + b;
};
cout << sum(1, 2) << endl; // 结果是:6.1
cout << i << endl; // 结果是1
}
捕捉列表捕捉对象,还可以使用隐式捕捉的方式:
- 捕捉列表中只给=,是对lambda外的所有对象进行值捕捉。
- 捕捉列表中只给&,是对lambda外的所有对象进行引用捕捉。
cpp
void test6()
{
int i = 1;
double d = 1.1;
auto sum = [=]
{
//++i; // 值捕捉对象不能修改
//++d; // 值捕捉对象不能修改
return i + d;
};
cout << sum() << endl; // 结果是2.1
}
cpp
void test7()
{
int i = 1;
double d = 1.1;
auto sum = [&]
{
++i;
++d;
return i + d;
};
cout << sum() << endl; // 结果是4.1
cout << i << endl; // 结果是2
cout << d << endl; // 结果是2.1
}
实际上,隐式捕捉并不是捕捉所有对象,而是lambda使用了哪些对象,就捕捉哪些对象。
捕捉列表捕捉对象的第三种方式,是值/引用捕捉与隐式捕捉的混合:
- =与&只能放在捕捉列表的前端。
- =只能与引用捕捉组合,&只能与值捕捉组合
- =与引用捕捉组合,引用捕捉对象之外的所有对象都是值捕捉对象;&与值捕捉组合,值捕捉对象之外的所有对象都是引用捕捉对象
cpp
void test8()
{
int a = 1, b = 2, c = 3, d = 4;
auto func1 = [&, c, d]
{
++a, ++b;
return a + b + c + d;
};
cout << func1() << endl; // 12
}
cpp
void test9()
{
int a = 1, b = 2, c = 3, d = 4;
auto func1 = [=, &c, &d]
{
++c, d += 2;
return a + b + c + d;
};
cout << func1() << endl; // 13
}
对于全局对象和静态对象,lambda捕捉列表不能捕捉它们,捕捉了也会报错;由于全局对象和静态对象在所有的作用域内都能被使用,lambda也就没有必要捕捉这些对象,因为lambda函数体内可以直接使用这些对象。
cpp
int e = 100; // 全局对象
void test10()
{
static int f = 1000; // 静态对象
int a = 1, b = 2, c = 3, d = 4;
auto func1 = [=]
{
return a + b + c + d + e + f;
};
cout << func1() << endl; // 1110
}
还有一点,lambda如果实现在全局域,捕捉列表必须为空。
4、原理
开头曾提及lambda表达式本质上是一个匿名函数对象。其实,编译器在编译阶段根据lambda表达式生成了一个仿函数 。仿函数的名称是编译阶段确定的(比如lambda_1, lambda_2...),代码编写阶段是不知道名称的,所以我们说lambda表达式本质上是一个匿名函数对象。
lambda表达式各部分与仿函数各部分的对应关系:
| 部分 | lambda | 仿函数 |
|---|---|---|
| 函数体 | lambda函数体 | operator()函数体 |
| 返回类型 | lambda返回类型 | operator()返回类型 |
| 参数列表 | lambda参数列表 | operator()参数列表 |
捕捉列表,本质上是编译器生成的仿函数类下的成员变量。于是编译器可以利用捕捉列表里对应的对象,完成仿函数对象的初始化;再通过仿函数对象的调用、传参,完成仿函数对象的使用,即lambda的使用。
cpp
void test11()
{
int i = 1;
double d = 1.1;
auto sum = [i, d](int a, int b)
{
return i + d + a + b;
};
cout << sum(1, 2) << endl;
cout << i << endl;
}
struct lambda_1
{
public:
auto operator() (int a, int b)
{
return i + d + a + b;
}
private:
int i = 1;
double d = 1.1;
};
void test12()
{
lambda_1 ld1;
cout << ld1(1, 2) << endl; // sum(1, 2)的底层实现,可以认为是ld1(1, 2)
}
更进一步,我们可以认为:
- 对于值捕捉对象,编译器生成对应仿函数类的成员变量时,赋予的是const普通类型,所以值捕捉对象在lambda函数体内不能被修改;而加上mutable修饰,可以认为是类型去掉了const,此时lambda仿函数内的值捕捉对象是外面对象的拷贝,所以lambda内对值捕捉对象的修改影响不到外面。
- 对于引用捕捉对象,编译器赋予的是引用类型,即lambda内外的引用捕捉对象共用同一块空间,所以lambda内的修改能影响到lambda外。
lambda原理的验证,可以观察汇编代码:

