【C++】C++11的新特性 — function 包装器 , bind包装器

2024-08-13 10:58:26 浏览数 (1)

1 function包装器

1.1 function的底层

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

在C 中有一个可调用对象的概念,其中有几个奇葩:函数指针,仿函数对象,lambda表达式。祖师爷看这几个玩意儿很难受:

  1. 函数指针 — 类型定义复杂
  2. 仿函数对象 — 要定义一个类,用的时候很,麻烦,不适合统一类型
  3. lambda表达式 — 没有类型概念

所以包装器就来包装上面的复杂东西,可以做到统一类型,就像秦王统一度量衡一样!我们想来看包装器的底层是什么样子的:

代码语言:javascript复制
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

模板参数说明:

  1. Ret: 被调用函数的返回类型
  2. Args…:参数包 ,被调用函数的形参

支持非常多的构造:

我们继续看最底层是什么:

看到里面重载了operator(),所以其实包装器的底层是仿函数!

1.2 开始使用function

包装器不是用来定义可调用对象的,是用来包装可调用对象的。也就是可以包装所有的可调用对象,尤其是这仨货:函数指针,仿函数对象,lambda表达式。我们先来练练手: 假如有这样一个仿函数,要如何来进行包装呢?

代码语言:javascript复制
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a   b;
	}
};

包装器的包装方式很不一样,我们上面看到过包装器的底层,其中的模版参数是

返回值类型(参数类型)这样的,很好理解!

代码语言:javascript复制
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a   b;
	}
};

int main()
{
	//看起来很好理解!
	function<int(int, int)> func = Functor();
	
	return 0;
}

同样的也可以包装函数指针,lambda表达式!:

代码语言:javascript复制
#include <functional>

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a   b;
	}
};
int add(int a, int b)
{
	return a   b;
}

int main()
{
	function<int(int, int)> func = Functor();
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = add;
	cout << func1(1, 2) << endl;
	// 仿函数对象
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;
	// lambda表达式
	std::function<int(int, int)> func3 = [](const int a, const int b) { return a   b; };
	cout << func3(1, 2) << endl;

	return 0;
}

非常好用,调用起来非常丝滑:

假设包装的时候类型不匹配怎么办?肯定就会报错了!包装器内部将可调用对象进行储存起来,封装了一层来进行调用。但是为什么不直接来进行调用,而是进行包装呢?进行一个统一,让代码更加优雅,让代码更加好用,我们来看一个经典的题:

对于这个题目,之前我们解法是使用一个栈,依次存入数字,取到运算符时就进行运算。对于这个运算符的判断,可以通过多重if语句进行判断或者Switch语句。我们学习了包装器就可以进行进行一个优雅的方式了:

代码语言:javascript复制
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        //一般解法是调用一个栈来做到取用数字
        //可以通过包装器来实现代码优雅化
    map<string , function<int(int, int)> > math 
    {
        { " " , [](int a, int b) {return b   a; } },
        { "-" , [](int a, int b) {return b - a; } }, 
        { "*" , [](int a, int b) {return b * a; } },
        { "/" , [](int a, int b) {return b / a; } }
    };
    
        
        //调用一个stack
        stack<int> tmp;
        //从头开始遍历
        for(int i = 0 ; i < tokens.size() ; i  )
        {
            string t = tokens[i];

            if( t.size() == 1 && !('0' <= t[0] && t[0] <= '9'))
            {
                int a = tmp.top(); tmp.pop();
                int b = tmp.top(); tmp.pop();

                if(t[0] == ' ') tmp.push(math[t](a , b));
                if(t[0] == '-') tmp.push(math[t](a , b));
                if(t[0] == '*') tmp.push(math[t](a , b));
                if(t[0] == '/') tmp.push(math[t](a , b));

            }
            //是数字就进行插入
            else
            {
                tmp.push(stoi(t));
            }
           
        }
        return tmp.top();
    }
};

这样就优雅的进行了解决!

1.3 包装成员函数指针

我们来看一个特别的:对于对象里面的函数如何进行包装呢? 对象里的函数可以分为两种:静态成员函数,普通成员函数 对于静态函数指针直接进行包装就可以,普通函数指针需要添加&,并且要注意普通成员函数有默认参数``。所以为了统一就都加上&

代码语言:javascript复制
class Plus
{
public:
    static int plusi(int a, int b)
    {
        return a   b;
    }
    double plusd(double a, double b)
    {
        return a   b;
    }
};

int main()
{
	//需要指明类域
	//对于静态的成员函数直接进行调用就可以
    //function<int(int, int)> func1 = Plus::plusi;
    function<int(int, int)> func1 = &Plus::plusi;
    //普通成员函数不可以这样进行 , 和上面一样会出现报错
    //需要&类::函数名 , 隐藏的参数
    function<double(Plus* , double, double)> func2 = &Plus::plusd;

    return 0;
}

使用的时候,,静态成员函数可以直接拿来使用,但是对于类的普通函数需要实例化一个类,一并传入才可以:

代码语言:javascript复制
func1(1 , 2);
//实例化一个类
Plus plus;
func2(&plus , 1.1 , 2.2);

当然肯定有简单的方法,我们可以在包装的时候,做一下处理:将第一个参数改成类,而不是类指针!这样让人很不理解,怎么看怎么不对!确实可以用

代码语言:javascript复制
function<double(Plus , double, double)> func2 = &Plus::plusd; 
//...
//可以直接传入匿名对象了
func2(Plus() , 1.1 , 2.2);

其中的原因我们来分析一下:function<double(Plus , double, double)> func2 = &Plus::plusd; 中的模版参数会不会直接传入到类对象中呢?不会的!因为this不会进行直接的显示调用,我们可以猜测包装器内部应该是通过这个对象来进行调用!

2 bind包装器

2.1 bind的底层

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

其实和function的工作很像,多增加了一下模版参数,支持了参数的包装!可以称作绑定!

我们先来看原型:

代码语言:javascript复制
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 带返回值可以进行一个显示修改!
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

其返回值是/* unspecified */ 不明确的,认为没有返回值!很神奇奥! 对于这个底层,我们稍微了解就可以!

2.2 开始使用bind

bind 的用途是用来调整可调用对象的参数个数或者顺序,就是我们可以把一个可调用对象包装起来,我们可以在包装器这层调整其参数的顺序!里面有一个placehodlers命名空间

_n代表第几个参数

来看一个最直观的:

代码语言:javascript复制
int sub(int a , int b)
{
	return a - b;
}

int main()
{
	//正常顺序调用
	auto func1 = sub;
	cout << func1(10, 5) << endl;
	
	auto func2 = bind(sub, placeholders::_1, placeholders::_2);
	cout << func2(10, 5) << endl;
	//调整参数顺序调用
	auto func3 = bind(sub, placeholders::_2, placeholders::_1);
	cout << func3(10, 5) << endl;

	return 0;
}

我们运行:

就可以理解这个调整参数顺序是什么意思了。_n代表的是新产生的包装器的参数的顺序!

这样我们就可以继续来了解:假如我们要对类函数进行包装:

代码语言:javascript复制
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};

int main()
{
	//这里其实是是有三个参数,bind(&Sub::sub, placeholders::_1, placeholders::_2 , placeholders::_3)
	//这里和function一样有两种写法 传指针和穿对象都可以
	auto func4 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
	//这样就省略了一个参数
	cout << func4(10, 5) << endl;
	return 0;
}

这样就调整了参数个数。

通过对参数的个数和顺序的调整就可以实现了对可调用对象参数的调整!

2.3 bind绑定的实际应用

我们设想一个游戏场景,每个英雄都有一定血量和蓝量。我们设计一个英雄类来记录这些基本信息。 现在遇到了群体debuff,每个英雄都会受到影响,但是效果不同。我们可以通过:

  1. 在类对象中加入特定函数来实现对特定对象的修改,但是这样会是我们的代码很不优雅!
  2. 所以可以设计一个特定函数,通过一个bind绑定到对应对象中,方便调用!

我们在类外实现一个debuff函数,然后通过bind绑定到对象上,为保证可以修改到,一定注意是使用引用!!!

代码语言:javascript复制
#include<vector>
#include<string>
#include<functional>

class Hero
{
public:
	Hero(string name , int blood , int blue):
		_name(name) ,
		_blood(blood),
		_blue(blue)
	{	
	}


public:
	int _blood;
	int _blue;
private:
	string _name;
};

void hurt(Hero& hr , int blood_n , int blue_n)
{
	hr._blood -= blood_n;
	hr._blue -= blue_n;
}


int main()
{
	Hero WK("孙悟空", 100, 100);
	Hero SZ("唐三藏", 80, 150);
	Hero BJ("八戒", 120, 80);
	Hero SS("沙僧", 80, 120);

	//假如有一个场景需要对英雄造成伤害,但伤害不同
	//可以写一个可调用对象来统一绑定
	//ref(WK)注意一定要加入ref,保证是进行的引用!
	auto func_WK = bind(hurt , ref(WK), placeholders::_1 , placeholders::_2);
	auto func_SZ = bind(hurt , ref(SZ), placeholders::_1 , placeholders::_2);

	func_WK( 20, 10);
	func_SZ( 10, 20);

	return 0;
}

这样就可以调用专门的函数来对每个对象进行处理了!

0 人点赞