C++复习(十二):C++11之函数式编程——lambda表达式、函数指针、函数对象、function、bind

一、lambda表达式

本文lambda表达式的使用主要参考https://www.cnblogs.com/pzhfei/archive/2013/01/14/lambda_expression.html,后面添加了一些其他内容。

简介

C++11引入了和python一样的lambda表达式,也叫匿名函数、lambda函数,能够实现在函数内定义函数。

Lambda表达式具体形式如下:

[capture](parameters)->return-type{body}

具体使用

lambda表达式能够省略部分内容:

[](int x, int y) -> int { int z = x + y; return z; }        //显示指定返回类型
[](int x, int y) { return x + y; }    // 隐式返回类型
[](int& x) { ++x; }                   // 没有return语句 -> lambda 函数的返回类型是'void'
[]() { ++global_x; }                  // 没有参数,仅访问某个全局变量
[]{ ++global_x; }                     // 与上一个相同,省略了()

 Lambda函数可以引用在它之外声明的变量,这些变量的集合叫做一个闭包,闭包被定义在Lambda表达式声明中的方括号[]内。这个机制允许这些变量被按值或按引用捕获。下面为具体写法。

[]        //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[x, &y]   //x 按值捕获, y 按引用捕获.
[&]       //用到的任何外部变量都隐式按引用捕获
[=]       //用到的任何外部变量都隐式按值捕获
[&, x]    //x显式地按值捕获. 其它变量按引用捕获
[=, &z]   //z按引用捕获. 其它变量按值捕获

特殊情况:

默认情况下,lambda函数总是一个const函数,但mutable可以取消其常量性。使用该修饰符时,参数列表不可省略。

int a = 3, b = 4;
auto fun2 = [=](int c)mutable{return b = a + c;};

 

举两个使用的例子

第一个:将total按引用捕获。

std::vector<int> some_list;
int total = 0;
for (int i = 0; i < 5; ++i)
    some_list.push_back(i);
std::for_each(begin(some_list), end(some_list), [&total](int x) 
{
    total += x;
});

第二个:total按引用捕获,value按值捕获。对this的捕获比较特殊,只能按值捕获。this只有当包含它的最靠近它的函数不是静态成员函数时才能被捕获。对protect和priviate成员来说,lambda函数与创建它的成员函数有相同的访问控制。如果this被捕获了,不管是显式还隐式的,那么它的类的作用域对Lambda函数就是可见的。访问this的成员不必使用this->语法,可以直接访问。 

std::vector<int> some_list;
int total = 0;
int value = 5;
std::for_each(begin(some_list), end(some_list), [&, value, this](int x) 
{
  total += x * value * this->some_func();
});

不同编译器的具体实现可以有所不同,但期望的结果是:按引用捕获的任何变量,lambda函数实际存储的应该是这些变量在创建这个lambda函数的函数的栈指针,而不是lambda函数本身栈变量的引用。不管怎样,因为大数lambda函数都很小且在局部作用中,与候选的内联函数很类似,所以按引用捕获的那些变量不需要额外的存储空间。
  如果一个闭包含有局部变量的引用,在超出创建它的作用域之外的地方被使用的话,这种行为是未定义的!(我找不出有这种情况出现的例子)
  lambda函数是一个依赖于实现的函数对象类型,这个类型的名字只有编译器知道。如果用户想把lambda函数做为一个参数来传递,那么形参的类型必须是模板类型或者必须能创建一个std::function类似的对象去捕获lambda函数。使用 auto关键字可以帮助存储lambda函数。

auto my_lambda_func = [&](int x) { /*...*/ };
auto my_onheap_lambda_func = new auto([=](int x) { /*...*/ });

 

一个没有指定任何捕获的lambda函数,可以显式转换成一个具有相同声明形式函数指针。所以,像下面这样做是合法的:

auto a_lambda_func = [](int x) { /*...*/ };
void(*func_ptr)(int) = a_lambda_func;
func_ptr(4); //calls the lambda.

 

 

lambda表达式优缺点

优点:

0、不用起名字

1. 相比于匿名内部类和函数指针,更加简洁。(如果无须捕捉变量,它就可以说是等于普通的函数指针)

2. 使得代码块可以像其他数据类型一样作为参数来传递,或者作为函数的返回值。(作为函数返回值我还不了解)

3. 非常容易并行计算。(这个说法我还不理解)

缺点:

1. 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(这个说法更不理解了)

2. 相比于其他语言,C++的lambda显得有些冗长,参数类型也不能自动推导出来,必须手写,要么只能用auto。

 

C++中lambda表达式的底层实现

参考文献:https://blog.csdn.net/yiuechen/article/details/105331534

对于下面的代码段

class Add{
public:
	Add(int n):_a(n){
	}

	int operator()(int n){
		return _a + n;
	}
private:
	int _a;
};

int main(){
	int n = 2;
	Add a(n);
	a(4);

	auto a2 = [=](int m)->int{return n + m; };
	a2(4);
	return 0;
} 

查看它的汇编代码

在这里插入图片描述

从汇编代码中可以看到,当我们编写了一个lambda表达式之后,编译器将该表达式翻译成一个未命名类的未命名对象,且该类含有一个operator(),即是一个匿名函数对象(图片中的仿函数)。

 

二、函数指针

定义方式:

//函数返回值类型 (* 指针变量名) (函数参数列表);
int(*p)(int, int);

调用方式:

int Add(int x, int y){
    return x + y;
}

int Minus(int x, int y){
    return x - y;
}

int mymethod(int(*p2)(int,int), int x, int y){    //将函数指针作为参数
    return p2(x,y);
}

int main(){
    int(*p)(int, int);  //定义一个函数指针
    p = Add;  //把函数Add赋给指针变量p, 使p指向Add函数
    int a = 1, b = 2, c;
    //通过函数指针调用Add函数
    c = (*p)(a, b);
    
    //多种相同形参类型及返回值的函数作为参数传入
    cout<<mymethod(Add,a,b)<<endl;
    cout<<mymethod(Minus,a,b)<<endl;
    return 0;
}

注意:函数指针没有++和--运算。

 

与lambda表达式的对比:

从汇编的角度来看,lambda表达式是直接一个代码起始地址,而函数指针调用的是指针指向的地址,指针指向地址的值才是代码开头地址,实际上两者在定义上并无本质区别。

实现上lambda表达式应该和函数对象是一样的。

 

三、函数对象/函数符/仿函数

参考文章:http://c.biancheng.net/view/354.html,文章中还有函数对象在accumulate和sort函数中的作用。

如果一个类将()运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象。函数对象是一个对象,但是使用的形式看起来像函数调用,实际上也执行了函数调用,因而得名。

class CAverage{
public:
    double operator()(int a1, int a2, int a3){  //重载()运算符
        return (double)(a1 + a2 + a3) / 3;
    }
};

int main(){
    CAverage average;  //能够求三个整数平均数的函数对象
    cout << average(3, 2, 3);  //等价于 cout << average.operator(3, 2, 3);
    return 0;
}

与lambda表达式对比:

lambda表达式也称匿名函数对象。

函数对象的使用方式和lambda表达式一样。

 

四、std::function

参考文章:https://blog.csdn.net/p942005405/article/details/84760715https://blog.csdn.net/qq_35865125/article/details/108182313

类模版std::function包含于头文件 #include<functional>中,是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、仿函数(functor 重载括号运算符实现)、类成员函数、静态成员函数等(这六个也被称为可调用对象)。std::function对象是对C++中现有的可调用对象的一种类型安全的包装(解决了前面提到的函数指针的类型不安全的问题)。

std:function原型

template< class R, class... Args >
class function<R(Args...)>

定义

std::function<void()> f1;
std::function<int (int , int)> f2;

使用

#include <iostream>
#include <functional>
using namespace std;

std::function<bool(int, int)> fun;

//普通函数
bool compare_com(int a, int b) {
    return a > b;
}

//lambda表达式
auto compare_lambda = [](int a, int b) { return a > b;};

//仿函数
class compare_class{
public:
    bool operator()(int a, int b) {
        return a > b;
    }   
};

//类成员函数
class Compare {
public:
    bool compare_member(int a, int b) {
        return a > b;
    }
    static bool compare_static_member(int a, int b) {
        return a > b;
    }
};

int main() {
    bool result;
    fun = compare_com;
    result = fun(10, 1);
    cout << "普通函数输出, result is " << result << endl;
 
    fun = compare_lambda;
    result = fun(10, 1);
    cout << "lambda表达式输出, result is " << result << endl;
 
    fun = compare_class();
    result = fun(10, 1);
    cout << "仿函数输出, result is " << result << endl;
 
    fun = Compare::compare_static_member;
    result = fun(10, 1);
    cout << "类静态成员函数输出, result is " << result << endl;
 
    类普通成员函数比较特殊,需要使用bind函数,并且需要实例化对象,成员函数要加取地址符
    Compare temp;
    fun = std::bind(&Compare::compare_member, temp, std::placeholders::_1, std::placeholders::_2);
    result = fun(10, 1);
    cout << "类普通成员函数输出, result is " << result << endl;
}

注意事项

对于std::function的使用只要创建一个模板类对象,并传入相应的模板参数就可以存储任何具有相同返回值和参数的可调用对象,在调用的时候直接将std::function对象名当做普通的函数名就可以调用存储在其中的可调用实体。

  • 关于可调用对象转换为std::function对象需要遵守以下两条原则:
    • 转换后的std::function对象的参数能转换为可调用对象的参数;
    • 可调用对象的返回值能转换为std::function对象的返回值。
  • std::function对象最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等,但是可以与NULL或者nullptr进行比较。
  • 需要注意的是创建的std::function对象中存储的可调用实体不能为空,若对空的std::function进行调用将抛出 std::bad_function_异常。

引入std::function的意义

1、统一处理,体现多态

std::function实现了一套类型消除机制,可以统一处理不同的函数对象类型,一定意义上体现了多态。

程序设计,特别是程序库设计时,经常需要涉及到回调,如果针对每种不同的callable object单独进行声明类型,代码将会非常散乱,也不灵活,而通过std::function<>可以定义一个回调列表,而列表的元素可接受的可调用物类型并不相同。

#include <iostream>
#include <functional>
#include <list>
using namespace std;
 
// 普通函数
int c_function(int a, int b) {
    return a + b;
}
 
// 函数对象
class Functor {
public:
    int operator()(int a, int b) {
        return a + b;
    }
};
 
int main(int argc, char** argv) {
    Functor functor;
    std::list<std::function<int(int, int)>> callables;
 
    callables.push_back(c_function);    //装载普通函数
    callables.push_back(functor);    //装载函数对象
    callables.push_back([](int x, int y)->int{return x + y;});     //装载lambda表达式   
 
    for (const auto& e : callables) {
        cout << e(3, 4) << endl;
    }
}

2、更加类型安全

以前我们使用函数指针来完成这些,现在我们可以使用更安全的std::function来完成这些任务。

所以写代码时应该尽量使用std::function而不是函数指针,或者把函数指针转化成std::function。

 

五、bind

参考文章:https://blog.csdn.net/p942005405/article/details/84760715

std::bind函数将可调用对象(用法中所述6类)和可调用对象的参数进行绑定,返回新的可调用对象(std::function类型,参数列表可能改变),返回的新的std::function可调用对象的参数列表根据bind函数实参中std::placeholders::_x从小到大对应的参数确定。

void fun(int x,int y,int z) {
    cout << x << " " << y << " " << z << endl;
}
 
void fun_2(int &a,int &b) {
    a++;
    b++;
    cout << a << " " << b << endl;
}
class A {
public:
    void fun_3(int k,int m) {
        cout << k << " " << m << endl;
    }
};
 
int main() {
    auto f1 = std::bind(fun, 1, 2, 3);    //表示绑定函数fun的第一,二,三个参数值为: 1 2 3
    f1();    //print:1  2  3
 
    auto f2 = std::bind(fun, placeholders::_1, placeholders::_2,3);    //表示绑定函数fun的第三个参数为3,而fun的第一,二个参数分别有调用f2的第一,第二个参数指定
    f2(1, 2);    //print:1  2  3
 
    auto f3 = std::bind(fun, placeholders::_2, placeholders::_1,3);    //表示绑定函数fun的第三个参数为3,而fun的第一,二个参数分别有调用f3的第二,第一个参数指定
    f3(1, 2);    //print:2  1  3
 
 
    int n = 2;
    int m = 3;
    auto f4 = std::bind(fun_2, n,placeholders::_1);
    f4(m);    //print:3  4 
    cout << m << endl;    //print:4  说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的
    cout << n << endl;    //print:2  说明:bind对于预先绑定的函数参数是通过值传递的
 
 
    A a;
    auto f5 = std::bind(&A::fun_3, a, placeholders::_1, placeholders::_2);
    f5(10, 20);    //print:10 20
 
    std::function<void(int,int)> fc = std::bind(&A::fun_3, a, std::placeholders::_1, std::placeholders::_2);
    fc(10, 20);    //print:10 20
 
    return 0;
}

 

总结

综合了许多博客的内容写了这篇文章,写之前的本意是想知道C++中如何才能将函数作为参数来传递,以及把之前LeetCode别人相关的代码写法写进来,只想写lambda表达式、function和bind的,后来查资料的过程中加入了和lambda表达式类似的函数指针、函数对象的内容。现在此篇文章中有些东西关于底层的东西没有自己求证过,仍然有可能是错的,如有错误,望大佬指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值