「前言」文章内容大致是 function 和 bind 包装器。

// 普通函数
int add(int a, int b) { return a + b; }
// 函数指针
int (*funcPtr)(int, int);
funcPtr = add;
int result = funcPtr(2, 4);
// 函数对象(仿函数),重载了()运算符
 class AddFunctor 
 {
 public:
     int operator()(int a, int b) const { return a + b; }
 };

一、function 包装器

1.1 介绍

std::function 包装器也叫作适配器。C++中的 function 本质是一个类模板,也是一个包装器。
std::function是一个通用的多态函数包装器。它可以存储、复制和调用任何可调用的目标,包括函数、函数指针、成员函数指针和函数对象(仿函数)。

function 类模板的原型如下:

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

 包装器使用了可变参数模板,模板参数介绍如下:

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

代码解释如下:

  • 这段代码定义了一个模板类函数,该类接受一个或多个类型参数。
  • 第一行是一个不完整的声明,因为没有指定函数类型参数的具体类型,因此它是未定义的。
  • 第二行定义了一个模板类 function,它有一个类型参数 Ret 和一个变长模板参数 Args,表示返回值类型和函数参数类型。
  • 这个模板类的具体实现是针对函数类型 Ret(Args...) 的。也就是说,这个模板类可以用来存储任意类型为 Ret(Args..) 的函数指针、函数对象等。

function 包装器可以对可调用对象进行包装,包括函数指针(函数名)、仿函数(函数对象)、lambda 表达式、类的成员函数等。

使用 function包装器需要包含头文件:

#include <functional>

1.2 使用

对于以下函数模板 useF:

  • 传入该函数模板的第一个参数可以是任意的可调用对象,比如函数指针、仿函数、lambda 表达式等。
  • useF 中定义了静态变量 count,并在每次调用时将 count 的值和地址进行了打印,可判断多次调用时调用的是否是同一个 useF 函数。
  • 在传入第二个参数类型相同的情况下,如果传入的可调用对象的类型是不同的,那么在编译阶段该函数模板就会被实例化多次。

代码如下:

#include <iostream>
using namespace std;

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);
}

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

	return 0;
}

 运行结果,三者的 count 都是不同的,也就意味着该函数模板实例化了三份

由于函数指针、仿函数、lambda表达式是不同的类型,因此 useF 函数会被实例化出三份,三次调用 useF 函数所打印 count 的地址也是不同的。但实际这里根本没有必要实例化出三份 useF 函数,因为三次调用 useF 函数时传入的可调用对象虽然是不同类型的,但这三个可调用对象的返回值和形参类型都是相同的。

这时就可以用包装器分别对着三个可调用对象进行包装,然后再用这三个包装后的可调用对象来调用 useF 函数,这时就只会实例化出一份 useF 函数。

根本原因就是因为包装后,这三个可调用对象都是相同的 function 类型,因此最终只会实例化出一份 useF 函数,该函数的第一个模板参数的类型就是 function 类型的。

测试代码:

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);
}

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

int main()
{
	//函数名
	function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;

	//函数对象
	function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;

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

 运行结果,这时三次调用 useF 函数所打印 count 的地址就是相同的,说明类模板实例化只生成了一份,并且 count 在三次调用后会被累加到 3,表示 useF 函数被调用了三次。

function的包装器使用如下:

对于是函数、函数对象、lambda用法如下:

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

int main()
{
	// 函数名(函数指针)
	//int(int, int) 第一个int返回类型,(int, int)是参数列表
	function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;

	//也可以这样
	function<int(int, int)> func2(f);
	cout << func2(1, 2) << endl;
	cout << endl;

	// 函数对象
	function<int(int, int)> func3 = Functor();
	cout << func3(1, 2) << endl;

	//这种匿名对象的写法,VS系列不支持,可能是编译器识别的问题
	/*function<int(int, int)> func4(Functor());
	cout << func4(1, 2) << endl;*/

	//有名对象可以
	Functor ft;
	function<int(int, int)> func5 = ft;
	cout << func5(1, 2) << endl;
	cout << endl;

	// lambda表达式
	function<int(int, int)> func6 = [](const int a, const int b) {return a + b; };
	cout << func6(1, 2) << endl;

	return 0;
}

如果是类成员函数,使用有点区别:

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;//类静态成员函数指针
	cout << func1(1, 2) << endl;
	//也可以不取地址
	function<int(int, int)> func2 = Plus::plusi;//类静态成员函数指针
	cout << func2(1, 3) << endl;

	//类普通成员函数
	//对于类普通成员函数,必须进行&,没有直接报错
	//参数列表还需要加上一个参数:类名,这个参数代表的是 this指针
    //this指针不能显示去传,所以需要用Plus替代
	function<double(Plus, double, double)> func8 = &Plus::plusd;//类普通成员函数指针
	//调用的时候必须要传多一个匿名对象,不传直接报错
	cout << func8(Plus(), 1.1, 2.2) << endl;

	return 0;
}

运行结果

3
4
3.3

function 包装器其实就是对函数、函数对象、lambda 表达式进行包装,包装的目的是: 将可调用对象的类型进行统一,但其本质上还是调用函数、函数对象、lambda 表达式。

1.3 function 包装器的意义

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

1.3 function包装器的一个例子

链接:逆波兰表达式

 

  • 定义一个栈,依次遍历所给字符串。
  • 如果遍历到的字符串是数字则直接入栈。
  • 如果遍历到的字符串是加减乘除运算符,则从栈定抛出两个数字进行对应的运算,并将运算后得到的结果压入栈中。
  • 所给字符串遍历完毕后,栈顶的数字就是逆波兰表达式的计算结果。

没有使用 function包装器,只能暴力判断,代码如下:

class Solution 
{
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> st;
        for(auto str : tokens)
        {
            if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                
                //暴力判断
                if(str == "+")  st.push(left + right);
                if(str == "-")  st.push(left - right);
                if(str == "*")  st.push(left * right);
                if(str == "/")  st.push(left / right);
            }
           else
           {
               st.push(stoi(str));
           }
        }
        return st.top();
    }
};

这种情况可以用包装器来简化代码。

  • 建立各个运算符与其对应需要执行的函数之间的映射关系,当需要执行某一运算时就可以直接通过运算符找到对应的函数进行执行。
  • 当运算类型增加时,就只需要建立新增运算符与其对应函数之间的映射关系即可

代码如下:

class Solution 
{
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> st;
        map<string, function<int(int, int)>> opFuncMap =
        {
            { "+", [](int i, int j) {return i + j; } },
            { "-", [](int i, int j) {return i - j; } },
            { "*", [](int i, int j) {return i * j; } },
            { "/", [](int i, int j) {return i / j; } }
        };
        
        for (auto& str : tokens)
        {
            if (opFuncMap.find(str) != opFuncMap.end())
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                st.push(opFuncMap[str](left, right));
            }
            else
            {
                //stoi to_string C++11
                st.push(stoi(str));
            }
        }
        return st.top();
    }
};

二、bind 包装器

2.1 介绍 

std::bind 函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。
std::bind是 C++ 标准库 中的一个函数模板,它用于绑定函数的参数,生成一个新的可调用对象。

bind 函数模板的原型如下:

// 原型如下:
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);

模板参数说明:

  • fn:可调用对象
  • Args:参数列表
  • Ret:返回值类型
  • args...:要绑定的参数列表:值或占位符

代码解释:

  • 这是 C++11 标准库中的 bind 函数的两个重载版本。它们的作用是创建一个函数对象,该函数对象可以将给定的函数对象和参数绑定在一起,以便稍后调用该函数对象时自动传递这些参数。
  • 第一个重载版本的返回类型是未指定的,它将返回一个可调用对象,该对象可以调用 fn 函数对象,并用 Args 参数列表调用它。
  • 第二个重载版本的返回类型是未指定的,它将返回一个可调用对象,该对象可以调用 fn 函数对象,并用 Args 参数列表调用它,然后将结果转换为 Ret 类型并返回。

可以将 bind 函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

调用 bind 的一般形式:auto newCallable = bind(callable,arg_list)

  • 其中,newCallable本身是一个可调用对象,
  • arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。
  • 当我们调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。

arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是“占位符”,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的 “位置”。

数值 n 表示生成的可调用对象中参数的位置,比如 _1为 newCallable的第一个参数,_2为第二个参数,以此类推。

占位符说明:

placeholders_1 是一个占位符,placeholders 其实是一个命名空间。

 

 此外,除了用 auto 接收包装后的可调用对象,也可以用 function 类型指明返回值和形参类型后接收包装后的可调用对象。

2.2 使用

bind包装器绑定固定参数。绑定固定参数,测试代码如下:

int Plus(int a, int b)
{
	return a + b;
}

int main()
{
	//表示绑定函数Plus 参数分别由调用 f1 的第一,二个参数为指定
	auto f1 = bind(Plus, placeholders::_1, placeholders::_2);
	cout << f1(1, 2) << endl;
	cout << f1(2, 2) << endl;

	//表示绑定函数 Plus 的第一个参数,第二个参数为: 10, 20
	auto f2 = bind(Plus, 10, 20);
	cout << f2() << endl;

	//表示绑定函数 Plus 的第一个参数为指定,第二个参数固定为:20
	auto f3 = bind(Plus, placeholders::_1, 20);
	cout << f3(1) << endl;

	//可以用 function类型指明返回值和形参类型后接收 bind包装后的可调用对象
	//f4 的类型为 function<void(int, int, int)> 与 f1类型一样
	function<int(int, int)> f4 = bind(Plus, placeholders::_1, placeholders::_2);
	cout << f4(1, 2) << endl;
	cout << f4(2, 2) << endl;

	return 0;
}

运行结果

3
4
30
21
3
4

还有一种无意义的绑定,下面这种绑定就是无意义的绑定: 

int Plus(int a, int b)
{
	return a + b;
}
int main()
{
	//无意义的绑定
	function<int(int, int)> func = bind(Plus, placeholders::_1, placeholders::_2);
	cout << func(1, 2) << endl; //3
	return 0;
}

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

bind包装器调整传参顺序。测试代码:

int Sub(int a, int b)
{
	return a - b;
}

int main()
{
	function<int(int, int)> f1 = bind(Sub, placeholders::_1, placeholders::_2);
	cout << f1(2, 1) << endl;

	//调整传参顺序,只需更改占位符的位置即可
	function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
	cout << f2(2, 1) << endl;

	return 0;
}

运行结果:

1
-1

根本原因就是因为,后续调用新生成的可调用对象时,传入的第一个参数会传给placeholders::_1,传入的第二个参数会传给placeholders::_2,因此可以在绑定时通过控制placeholders::_n的位置,来控制第 n 个参数的传递位置。

对于 bind 绑定的是类成员函数时:

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};

int main()
{
	// 绑定类成员函数
	function<int(int, int)> f1 = std::bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
	cout << f1(2, 1) << endl;
	return 0;
}

运行结果

1

对于 Sub 类中的 sub 成员函数,sub 成员函数的第一个参数是隐藏的 this 指针,如果想要在调用 sub 成员函数时不用对象进行调用,那么可以将 sub 成员函数的第一个参数固定绑定为一个 Sub 的对象,上面绑定的是匿名对象

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

2.3 bind包装器的意义

  • 将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用传递某些参数。
  • 可以对函数参数的顺序进行灵活调整。

实际上,不怎么使用 bind包装器,function包装器使用是比较多一些。

--------------- END ---------------

「 作者 」 枫叶先生
「 更新 」 2023.4.22
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。