今天介绍一些在实际 C++ 编程中非常常用的工具:
Lambda、std::function、bind。
同时结合前面介绍的可变参数模板和完美转发,可以看到 C++11 是怎样一步一步增强泛型编程能力的。
一、Lambda 是什么?
如果以前想定义一个简单的函数对象,通常有两种办法:
函数指针
cpp
int (*func)(int, int);
仿函数
自己定义一个类:
cpp
struct Add
{
int operator()(int a, int b)
{
return a + b;
}
};
这些方法都可以,但是写起来比较麻烦。
C++11 提供了:
Lambda 表达式
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。
二、Lambda 的基本语法
基本形式:
cpp
[capture-list](parameters) -> return_type
{
function_body
};
最简单的例子:
cpp
auto add = [](int x, int y)
{
return x + y;
};
然后:
cpp
cout << add(1, 2);
就可以得到:
text
3
三、为什么 Lambda 的类型不好写?
Lambda 本质上会生成一个独特的匿名函数对象类型。
但是这个类型的名字不是我们自己定义的。
所以实际开发中通常使用:
cpp
auto
来接收:
cpp
auto func = []()
{
cout << "hello";
};
而不是尝试写出它真正的类型名称。
四、捕捉列表
lambda 表达式中默认只能用 lambda 函数体和
参数中的变量,如果想用外层作用域中的变量
就需要进行捕捉
例如:
cpp
int a = 10;
int b = 20;
auto func = [a, &b]()
{
cout << a << endl;
cout << b << endl;
};
这里:
text
[a, &b]
就是:捕捉列表。
五、值捕捉和引用捕捉
例如:
cpp
[a, &b]
表示:
text
a
→ 值捕捉
b
→ 引用捕捉
值捕捉本质上可以理解成:
把外部对象的一份值保存下来。
而引用捕捉则是:
让 Lambda 继续引用外部对象。
六、隐式捕捉
除了:
cpp
[a, &b]
还可以:
cpp
[=]
表示:
需要使用的外部变量都采用值捕捉。
或者:
cpp
[&]
表示:
需要使用的外部变量都采用引用捕捉。
例如:
cpp
auto func = [&]()
{
a++;
b++;
c++;
};
这样写起来会非常方便。
七、mutable
默认情况下,Lambda 的值捕捉具有 const 属性。
如果我们希望修改值捕捉得到的那一份副本,可以使用:
cpp
[=]() mutable
{
...
};
这里修改的是:
Lambda 内部自己的那份副本。
并不会修改外面的原始变量。
八、Lambda 最实用的场景:STL 算法
例如现在有一个商品结构:
cpp
struct Goods
{
string _name;
double _price;
int _evaluate;
};
如果想按照价格排序,以前可能需要专门写:
cpp
struct ComparePriceLess
{
bool operator()(const Goods& g1,
const Goods& g2)
{
return g1._price < g2._price;
}
};
现在直接:
cpp
sort(v.begin(), v.end(),
[](const Goods& g1,
const Goods& g2)
{
return g1._price < g2._price;
});
就可以了。
如果下一次想按照评价排序:
cpp
sort(v.begin(), v.end(),
[](const Goods& g1,
const Goods& g2)
{
return g1._evaluate > g2._evaluate;
});
这就是 Lambda 非常实用的地方:
需要一个临时的可调用对象时,不需要专门定义一个类。
九、Lambda 的底层原理
Lambda 看起来像一个特殊语法,但实际上:
编译器最终会把 Lambda 转化成一个类似仿函数的类。
例如:
cpp
double rate = 0.49;
auto r = [rate](double money, int year)
{
return money * rate * year;
};
本质上可以近似理解成:
cpp
class Lambda
{
public:
Lambda(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
也就是说:
text
Lambda捕捉的数据
↓
变成类成员变量
Lambda函数体
↓
变成 operator()
这也是为什么我们可以说:
Lambda 本质上仍然是一个函数对象。
十、std::function
有了 Lambda 后,又出现了一个新的问题:
不同的可调用对象,类型都不一样。
例如:
text
函数指针
仿函数
Lambda
bind返回对象
这些类型本身很难统一。
C++11 提供:
cpp
std::function
std::function 是一个可调用对象包装器,可以统一包装函数指针、仿函数、Lambda、bind 表达式等。
十一、为什么 function 很方便?
例如:
cpp
function<int(int, int)> f;
这个类型表示:
接收两个 int,返回一个 int 的可调用对象。
于是:
cpp
int Add(int a, int b)
{
return a + b;
}
可以放进去:
cpp
function<int(int, int)> f = Add;
Lambda 也可以:
cpp
f = [](int a, int b)
{
return a + b;
};
仿函数同样可以。
所以:
text
不同类型的可调用对象
↓
std::function
↓
统一类型
这对于构造:
text
回调
函数表
操作符映射
事件系统
等结构非常方便。
十二、bind
除了:
cpp
lambda
function
C++11 还提供:
cpp
std::bind
函数适配器。
它可以把一个已有的可调用对象包装成另一个新的可调用对象,同时:
- 调整参数顺序
- 固定部分参数
- 改变调用方式
十三、bind 最简单的例子
假设:
cpp
int Sub(int a, int b)
{
return (a - b) * 10;
}
可以:
cpp
auto sub1 = bind(Sub, _1, _2);
那么:
cpp
sub1(10, 5);
实际就是:
cpp
Sub(10, 5);
十四、调整参数顺序
甚至可以:
cpp
auto sub2 = bind(Sub, _2, _1);
然后:
cpp
sub2(10, 5);
实际调用:
cpp
Sub(5, 10);
所以 _1 表示:
新函数的第一个参数。
_2 表示:
新函数的第二个参数。
十五、bind 还可以固定参数
例如:
cpp
auto sub3 = bind(Sub, 100, _1);
那么:
cpp
sub3(5);
实际上执行:
cpp
Sub(100, 5);
也就是说:
text
原函数
↓
bind固定部分参数
↓
生成一个新的可调用对象
这在需要大量"半固定参数的函数"时会比较方便。
cpp
#include <functional>
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
using namespace std::placeholders;
// 把 add 的第一个参数固定为 10,第二个参数留给调用者
auto f1 = std::bind(add, 10, _1);
std::cout << f1(5) << '\n'; // 输出 15,相当于 add(10, 5)
// 全部固定,调用时不需要参数
auto f2 = std::bind(add, 1, 2);
std::cout << f2() << '\n'; // 输出 3
// 参数重排:_2 作为第一个参数,_1 作为第二个参数
auto f3 = std::bind(add, _2, _1);
std::cout << f3(3, 4) << '\n'; // 输出 7,相当于 add(4, 3)
// 同一个参数使用多次
auto f4 = std::bind(add, _1, _1);
std::cout << f4(7) << '\n'; // 输出 14,相当于 add(7, 7)
}