【C++11】C++11——包装器

26 阅读4分钟

我正在参加「掘金·启航计划」

1.function包装器概念

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

其实这些都是可调用对象:C语言的函数指针、仿函数/函数对象、lambda表达式、今天说的是包装器,主要是function包装器与bind包装器。

类模板原型:

template <class T> function;   
template <class Ret, class... Args>
class function<Ret(Args...)>;

模板参数说明:Ret:被包装的可调用对象的返回值类型。Args...:被包装的可调用对象的形参类型。

废话不多说,我们先来简单地使用一下是把,分别包装函数指针、仿函数、Lambda表达式、成员函数:

包装函数指针、仿函数、Lambda表达式:

#include <functional>
int f(int a, int b)
{
	return a + b;
}

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

int main()
{
	//1.包装函数指针
	function<int(int, int)> f1;
	f1 = f;
	cout << f1(10, 20) << endl;
	function<int(int, int)> f2(f);
	cout << f2(10, 20) << endl;

	//2.包装仿函数
	/*Functor f1();
	function<int(int, int)> f3(f1);*/
	function<int(int, int)> f3 = Functor();
	cout << f3(10, 20) << endl;

	//3.包装Lambda表达式
	function<int(int, int)> f4 = [](const int a, const int b) {return a + b; };
	cout << f4(10, 20) << endl;
 	return 0;
}

注意头文件#include

包装成员函数:(成员函数分为静态和非静态,本质还是函数指针)

class P
{
public:
	static int P1(int a, int b)
	{
		return a + b;
	}

	int P2(int a, int d)
	{
		return a + d;
	}
};
int main()
{	
    //静态成员函数
    function<int(int, int)> f5 = &P::P1;//成员函数要加上类域限制
	cout << f5(10, 20) << endl;
	//非静态成员函数,this指针不允许显式传递
	function<int(P, int, int)> f6 = &P::P2;
	cout << f6(P(), 10, 20) << endl;
}

取静态成员函数的地址可以不用"&",但是取非静态成员函数的地址就必须带上"&"

包装非静态成员函数是需要注意:非静态成员函数的第一个参数是隐藏this指针,所以在包装的时候需要指明第一个形参的类型为类的类型

包装器本质就是对各种可调用对象进行类型的统一。

2.function包装器统一类型

我们提供一个函数模板useF:

传入该函数模板的第一个参数可以是任意的可调用对象:如我们上面所说的函数指针、仿函数、lambda表达式等。

useF中定义了静态变量count,每次调用时将count的值和地址进行了打印,可判断多次调用时调用的是否是同一个useF函数。

函数模板useF代码:

template<class F,class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}

现在我们传入第二个参数相同的类型,但是传入的可调用对象的类型是不同的,那么在编译阶段该函数模板就会被实例化多次:

struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};

double f(double i)
{
	return i / 2;
}
int main()
{
	//函数名
	cout << useF(f, 9.9) << endl;
	//函数对象
	cout << useF(Functor(), 9.9) << endl;
	//lambda表达式
	cout << useF([](double d)->double {return d / 3; },9.9) << endl;
	return 0;
}

image-20230322181857536

由于函数指针、仿函数、lambda表达式是不同的类型,那么函数模板useF也会实例化出三份,结果打印出来也是不同的。

但是如果有需求:不实例化出三份,因为虽然三次调用传入可调用对象类型不同,但是调用对象的返回值与形参类型相同

此时我们就可以使用包装器对这三个不同的调用对象进行包装了,分别包装这三个可调用对象来调用useF函数,这时候就值实例化出一份useF函数。

#include <functional>
template<class F,class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;

	return f(x);
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
double f(double i)
{
	return i / 2;
}
int main()
{
	//函数名
	function<double(double)> f1 = f;
	cout << useF(f1, 9.9) << endl;
	//函数对象
	function<double(double)> f2 = Functor();
	cout << useF(f2, 9.9) << endl;

	//lambda表达式
	function<double(double)> f3 = [](double d)->double {return d / 4; };
	cout << useF(f3,9.9) << endl;
	return 0;
}

这时三次调用useF函数所打印count的地址就是相同的,并且count在三次调用后会被累加到3,表示这一个useF函数被调用了三次。

3.function包装器优化代码

还记得我们写过的题目:逆波兰表达式求值吗?

解题过程:

定义一个栈,遍历字符串,如果是数字直接入栈(字符串转化成数字stoi),如果遍历到的字符是加减乘除运算符,则从栈顶取出两个数字,进行相关的运算,最后在将结果入栈。遍历完字符串之后,栈顶的数字就是最终的计算结果

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(int i = 0;i<tokens.size();i++)
        {
            if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/")
            {
                long long num1 = st.top();
                st.pop();
                long long num2 = st.top();
                st.pop(); 
                if(tokens[i]=="+") st.push(num2+num1);
                if(tokens[i]=="-") st.push(num2-num1);
                if(tokens[i]=="*") st.push(num2*num1);
                if(tokens[i]=="/") st.push(num2/num1);
            }
            else
            {
                st.push(stoi(tokens[i]));
            }
        }
        return st.top();
    }
};

对于上面的代码,通过多个if语句判断进行哪一个运算,如果运行增加,那么我们就需要继续添加if判断语句了。

这时候,我们可以利用function包装器来优化上面的代码:

建立各个运算符与其对应需要执行的函数间的映射关系,当执行某一个运算时就可以直接通过运算符找到对应的函数执行;

当运算符类型增加时,我们只需要建立新增运算符与其对应函数间的映射关系即可。

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        //命令与动作的映射
        map<string,function<int(int,int)>> opFuncMap = 
        {
            {"+",[](int x,int y)->int{return x+y;}},
            {"-",[](int x,int y)->int{return x-y;}},
            {"*",[](int x,int y)->int{return x*y;}},
            {"/",[](int x,int y)->int{return x/y;}},
        };
        for(auto&e:tokens)
        {
            //操作数进栈
            if(opFuncMap.count(e)==0)
            {
                st.push(stoi(e));
            }
            //操作符
            else
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();

                st.push(opFuncMap[e](left,right));
            }
        }
        return st.top();
    }
};

function包装器可以将可调用对象的类型进行统一,便于我们对其进行统一化;包装后明确8可调用对象的返回值和形参类型,更加方便使用者使用。


bind包装器

1.bind包装器概念

bind函数定义在头文件中,也是一个函数模板,就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。

bind函数模板原型:

template <class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);

参数说明:fn:可调用的对象,...args:要绑定的参数列表。

2.bind包装器绑定固定参数

bind绑定可以与function结合,placeholders:命名空间,_1,_2:占位对象

  • 无意义绑定
int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//表示绑定函数plus参数分别由调用func1的第一,二个参数指定
	std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
	cout << func1(10, 20) << endl;
	return 0;
}

第一个参数传入函数指针,后面传入绑定的参数列表依次是placeholders::_1和placeholders::_2,表示后续调用新生成的可调用对象时,第一个参数传给placeholders::_1,第二个参数传给placeholders::_2。此时绑定后生成的新的可调用对象的传参方式,和原来没有绑定的可调用对象是一样的,所以说这是一个无意义的绑定。

  • 固定绑定参数

参数是可以进行固定绑定的:比如我们把Plus函数的第二个参数固定绑定为10,可以在绑定时将参数列表的placeholders::_2设置为20:

#include <functional>
int Plus(int a, int b)
{
	return a + b;
}
int main()
{
    function<int(int)> func = bind(Plus, placeholders::_1,20);
	cout << func(2) << endl;//22
    return 0;
}

3.bind包装器调整传参顺序

我们以Sub类为例子,对于下面代码中的Sub类的成员函数sub,第一个参数是this指针,所以如果想要调用sub时不用对象进行调用,那么我们就可以通过上面所说的固定绑定参数,把sub成员函数的第一个参数固定绑定为Sub对象:

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
    function<int(Sub, int, int)> func4 = &Sub::sub;
	cout << func4(Sub(), 10, 20) << endl;//-10
    //绑定固定参数,不需要传Sub()
	function<int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
	cout << func5(10,20) << endl;//-10
	return 0;
}

此时调用绑定后生成的可调用对象时,就只需要传入用于相减的两个参数了,因为在调用时会固定帮我们传入一个匿名对象给this指针.

bind包装器的传参是可以进行调整的,将sub成员函数用于相减的两个参数的顺序交换,那么直接在绑定时将placeholders::_1和placeholders::_2的位置交换一下就行了:

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
    function<int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
	cout << func5(10,20) << endl;//10
    return 0;
}

bind包装器可以将一个函数的某些参数绑定为固定值,让我们在调用时可以不传递某些参数,也可以对函数参数的顺序进行调整