• 【C++】lamada表达式和包装器



    可调用对象的类型包括

    • 仿函数
    • 函数指针
    • function包装器
    • lamada表达式

    lamada表达式

    lamada表达式其实就是一个匿名的函数,可以在参数传参的时候构造这个可调用对象

    lamada表达式语法

    lambda表达式书写格式:

    [capture-list] (parameters) mutable -> return-type { statement }
    
    • 1
    • [capture-list]

      捕捉列表,该列表总是出现在lambda函数的开始位置编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用

    • (parameters)

      参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略

    • mutable

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

    • ->returntype

      返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略返回值类型明确情况下,也可省略,由编译器对返回类型进行推导

    • {statement}

      函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

    使用示例:

    int main() {
    	//最简单的lamada表达式,表示什么都不做
    	[] {};
    	
    	//实现一个add lamada表达式
    	int a = 3, b = 4;
    	auto add1 = [](int x, int y)->int {return x + y; };
    	std::cout << add1(a, b) << std::endl;
    
    	//通过捕捉列表实现add lamada表达式
    	auto add2 = [=]{return a + b; };
    	std::cout << add2() << std::endl;
    
    	//实现一个swap lamaba表达式
    	a = 1, b = 2;
    	auto swap1 = [](int& x, int& y) {
    		int tmp = x;
    		x = y;
    		y = tmp;
    	};
    	swap1(a, b);
    
    	//通过捕捉列表实现swap lamada表达式
    	/*auto swap2 = [&a, &b] {
    		int tmp = a;
    		a = b;
    		b = tmp;
    	};*/
    	auto swap2 = [&] {
    		int tmp = a;
    		a = b;
    		b = tmp;
    	};
    	swap2();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。


    捕捉列表说明

    捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用

    [var]//表示值传递方式捕捉变量var
    [=]//表示值传递方式捕获所有父作用域中的变量(包括this)
    [&var]//表示引用传递捕捉变量var
    [&]//表示引用传递捕捉所有父作用域中的变量(包括this)
    
    • 1
    • 2
    • 3
    • 4

    注意

    • 父作用域指包含lambda函数的语句块
    • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
    • 捕捉列表不允许变量重复传递,否则就会导致编译错误,比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
    • 在块作用域以外的lambda函数捕捉列表必须为空
    • 块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
    • lambda表达式之间不能相互赋值,即使看起来类型相同

    函数对象和lambda表达式

    函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象

    class Rate{
    public:
    	Rate(double rate) : _rate(rate)
    	{}
    	double operator()(double money, int year){
    		return money * _rate * year;
    	}
    private:
    	double _rate;
    };
    int main(){
    	// 函数对象
    	double rate = 0.49;
    	Rate r1(rate);
    	r1(10000, 2);
    	// lamber
    	auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
    	r2(10000, 2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    从使用方式上来看,函数对象与lambda表达式完全一样

    函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到

    在这里插入图片描述

    lambda底层是一个仿函数,无论是捕捉列表捕捉还是参数列表都是通过函数传参实现的

    每个lambda表达式都会生成唯一的uuid,所以仿函数的名称不一样,返回的类型也不一样

    实际在底层编译器对于lambda表达式的处理方式完全就是按照函数对象的方式处理的

    即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。


    包装器

    function包装器

    function包装器也叫作适配器,C++中的function本质是一个类模板,也是一个包装器

    上面提到了几种可调用对象,函数指针,仿函数,lambda表达式,这几种对象是完全不同的类型,而且使用起来不是很方便

    • 函数指针类型太复杂,不方便使用和理解
    • 仿函数类型是一个类名,没有指定调用参数和返回值,得去看operator()的实现才能看出来
    • lambda表达式在语法层,看不到类型,只能在底层看到其类型,基本都是lambda_uuid

    而且这几种对象是不同的类型,如果说这几个对象所作的事情完全相同参数返回值也都相同,这时把对象作为模板参数传递,尽管对象执行的操作都相同,但是对象是不同的类型, 需要实例化出多个类,浪费了资源

    为了解决上面多种可调用对象的类型问题,就引入了function包装器,function包装器其实是对可调用对象的一层封装,让参数和返回值相同的不同类型的可调用对象封装成一个类型,使用起来更加方便

    // 类模板原型如下
    template <class T> function; // undefined
    template <class Ret, class... Args>
    class function<Ret(Args...)>; 
    
    • 1
    • 2
    • 3
    • 4

    模板参数说明:

    • Ret: 被调用函数的返回类型
    • Args…:被调用函数的形参

    使用包装器包装多个不同类型的可调用对象:

    #include <functional>
    int f(int a, int b){
    	return a + b;
    }
    struct Functor{
    	int operator() (int a, int b)
    	{
    		return a + b;
    	}
    };
    class Plus{
    public:
    	static int plusi(int a, int b){
    		return a + b;
    	}
    	double plusd(double a, double b){
    		return a + b;
    	}
    };
    int main(){
    	//包装函数名(函数指针)
    	std::function<int(int, int)> func1 = f;
    	std::cout << func1(1, 2) << std::endl;
    
    	//包装函数对象(仿函数)
    	std::function<int(int, int)> func2 = Functor();
    	std::cout << func2(1, 2) << std::endl;
    
    	//包装lamber表达式
    	std::function<int(int, int)> func3 = [](const int a, const int b) {return a + b;};
    	std::cout << func3(1, 2) << std::endl;
    
    	//包装类的静态成员函数
    	std::function<int(int, int)> func4 = Plus::plusi;
    	std::cout << func4(1, 2) << std::endl;
    
    	//包装类的非静态成员函数
    	std::function<double(Plus, double, double)> func5 = &Plus::plusd;//对于普通成员的包装一定要加上&,需要通过指针进行调用成员函数
    	std::cout << func5(Plus(), 1.1, 2.2) << std::endl;//传入类对象,通过对象进行调用
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    bind

    bind是一个函数模板,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表,也就是调整可调用类型参数

    class A{
    public:
    	int  printf2(int b = 1){
    		return  b + 2;
    	}
    };
    int main(){
    	//把plus绑定成一个值+10																	
    	std::function<int(int)> f2 = std::bind([](int a, int b) { return a + b; }, 10, std::placeholders::_1);//表示传入的第一个参数
    	//绑定后的f2,只需要传入一个参数就可以调用,10也就是第二个参数已经被绑定好了
    	std::cout << f2(5) << std::endl;
    
    	//绑定固定的可调用对象 
    	//在非静态成员函数中需要传入对象,我们就绑定该对象,每次调用时就不用传入了
    	std::function<int(int)> f3 = std::bind(&A::printf2, A(), std::placeholders::_1);
    	std::cout << f3(10) << std::endl;
    
    	//调整参数顺序
    	std::function<int(int, int)> f4 = std::bind([](int a, int b) { return a - b; }, std::placeholders::_2, std::placeholders::_1);
    	std::cout << f4(1, 2) << std::endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    bind是对包装器的可调用类型的进一步封装,可以根据自己的需要进行调整参数的数据及位置,绑定类对象能有优化成员函数的包装使用更加符合使用习惯

  • 相关阅读:
    今天面试招了个18K的人,从腾讯出来的果然都有两把刷子···
    字符集导致19c集群安装故障
    基于Python的购物网站分析系统
    面向对象编程原则(08)——合成复用原则
    Java 8 之 Optional
    算法设计 - 分治法
    【Ubuntu】创建桌面快捷方式并固定到docker栏
    新华三的千亿企业梦,还得靠吃ICT老本来实现?
    FRED的光路和光路历史记录
    算法2:链表的逆转
  • 原文地址:https://blog.csdn.net/xiaomage1213888/article/details/125610990