现代C++:函数式编程(lambda、函数对象包装器)
# 现代C++:函数式编程(lambda、函数对象包装器)
# 函数作为另一个函数的参数
函数其实也可以作为另一个函数的参数,而且,这个作为参数的函数也可以有参数。
void print_number(int n)
{
std::cout << n << '\n';
}
void call_func(void func(int))
{
func(0);
func(1);
}
int main()
{
call_func(print_number);
return 0;
}
因为传入的 print_number
与定义的 void func(int)
类型匹配,因而可以作为参数传入。
为了简化,甚至可以将 func
的类型作为一个模板参数,从而无需写 void(int)
,同时允许函数的参数为其他类型,更好地进行自动适配与优化。
template<typename Func>
void call_func(Func func)
{
func(0);
func(1);
}
# Lambda 表达式
Lambda表达式提供了类似匿名函数的特性,其具体语法如下:
// [捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
// }
Lambda 表达式内部函数体在默认情况下是不能使用函数体外部的变量的,这时候捕获列表可以起到传递外部数据的作用。函数可以引用定义位置所有的变量,这一特性在函数式编程中称为闭包(closure)。根据传递的行为,捕获列表也分为以下四种:
值捕获、引用捕获、隐式捕获、表达式捕获
小贴士:lambda表达式,传常引用以避免拷贝开销
当一个函数需要lambda表达式作为函数参数时,最好把模板参数的
Func
声明为Func const &
以避免不必要的拷贝。这是由于捕获了变量之后,Func的大小将会变大。
# lambda表达式作为返回值
既然函数可以作为参数,当然也可以作为返回值,由于lambda表达式永远是个匿名类型,因此需要使用 auto
让其自动推导。
auto make_twice(int fac)
{
return [=] (int n)
{
return n*fac;
};
}
注意,此处捕获列表不可写成
[&](){};
因为退出函数后,fac在栈上已被销毁,所以引用失效。
总之,要是使用 [&]
,需要保证 lambda 对象的生命周期不超过他的捕获的所有引用的寿命。
# lambda 用途举例
# yield模式
可以延时求值。
template <typename Func>
void fetch_data(Func const &func) {
for (int i = 0; i < 32; i++) {
func(i);
func(i + 0.5f);
}
}
int main() {
std::vector<int> res_i;
std::vector<float> res_f;
fetch_data([&](auto const &x) {
using T = std::decay_t<decltype(x)>;
if constexpr(std::is_same_v<T, int>) {
res_i.push_back(x);
} else if constexpr(std::is_same_v<T, float>){
res_f.push_back(x);
}
});
return EXIT_SUCCESS;
}
这里用 std::decay_t<>
去掉类型中的const与&,即:
decay_t<int const &> = int
is_same_v<int,int> = true
is_same_v<float,int> = false
# 用于立即求值
以下程序实现查找数字下标的功能。
std::vector<int> vec = {1,3,2,4,5,6};
int tofind = 5;
int index = [&]{
for (int i = 0; i < vec.size(); i++)
if (vec[i] == tofind)
return i;
return -1;
}();
std::cout << index << '\n';
# 实现局部递归
需要注意的是:
- lambda需要把自己作为参数传入;
- 返回类型需要自己定义,否则无法推断
auto fibonacci = [&](auto const &func, int index) {
if (index == 1 || index == 2)
return 1;
return func(func, index-1)+func(func, index-2);
};
std::cout << fibonacci(fibonacci, 7); // 1 1 2 3 5 8 [13] 2
# 关于lambda表达式的新特性说明
另外,将lambda表达式的参数声明为 auto
时,基本上与 template<typaname T>
等价,和模板函数一样,具有惰性、多次编译的特性。缺点是前者无法进行特化。
auto twice = [](auto n){return n * 2;};
// 等价于
template <typename T>
auto twice(T n)
{
return n * 2;
}
另另外,在C++20后,lambda 表达式中可以使用模板类型参数。例如,下面的 lambda 表达式使用了一个模板类型参数。
auto lambda = []<typename T>(std::vector<T> const &vec) -> std::vector<T>
{
std::vector<T> result;
return result;
};
# 函数对象包装器
在使用lambda表达式作为函数参数的时候,通常要借助模板来声明定义函数,这就会造成无法将声明与定义分离或者加快编译的问题,为了灵活性,可以使用函数对象包装器 std::function
容器,只需在尖括号里写函数的返回类型和参数列表即可:
#include <functional>
std::function<int(float, char *)>;
另外,当没有捕获任何局部变量,那么不需要使用 std::function<int(int)>
,直接用函数指针类型 int (int)
即可,[](int n)->int{};
会退化为 int (int)
。
# std::bind
绑定
有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过 placeholder
函数,我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用
int foo3(int a, int b, int c)
{
std::cout << "a = " << a
<< "\tb = " << b
<< "\tc = " << c << '\n';
};
void test_bind()
{
using std::placeholders::_1;
auto binded_foo3 = std::bind(foo3, _1, 2, 3);
binded_foo3(10);
}