C++ - 包装器 - bind()函数

包装器

在 C++ 当中可能会有各种各样的可调用类型,比如 函数指针,仿函数,lambda 等等,那么这么多的可调用类型,我们在使用的时候就会犯迷糊,那可不可以统一控制一下呢?

function包装器,也叫做适配器。

我们先来看一个例子:

template
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 << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

上述有三种调用方式,分别是:函数指针,仿函数 和 lambda表达式。

每一个 不同类型的 函数,在同一个 函数模版当中会实例化出 三份 函数,因为 f 这个变量,接收的是 可调用对象,三种不同的可调用对象的传入,可能是要实例化出 不同的函数出来的,所以,对于上述 static 的 count 静态变量输出的地址是不一样的

C++ - 包装器 - bind()函数_第1张图片

通过上面的程序验证,我们会发现useF函数模板实例化了三份

在主函数当中,三种方式都可以调用,也就是说:

ret = func(x);
上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率下!

那么,有没有方式和把上述的三种方式都统一下呢?

答案是有的。

就是使用包装器。

 包装器语法

 std::function  在头文件  <functional> 当中,其实 function 包装器本质上是一个 类,在这个类当中存储了 各种函数,有上述的 仿函数,lambda,函数指针都都可以存储其中。

function 类模板原型如下所示:

// 类模板原型如下
template  function; // undefined
template 
class function;

C++ - 包装器 - bind()函数_第2张图片

 模板参数说明

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

 使用包装类的话,就是把 上述的可调用对象存储到一个容器当中(function对象当中),通过这个包装类,把 这些可调用对象包装一遍,这样我们在调用这个些个 可调用对象的时候,就可以以统一的方式进行调用了。

比如,在上述我们写了三个函数,在返回值上就是都是 double 类型,而且函数的参数个数只有一一个:

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 << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;

    return 0;
}

那么包装类声明应该这样写:

// 包装函数指针
function f1 = f;

// 包装lambda表达式
function f2 = [](double d)->double { return d / 4; };

// 包装仿函数(传入仿函数类的匿名对象)
function f3 = Functor();

那么,此时就包装好了 这三个函数。

在没有使用包装类的时候,我们要想把这个些 可调用对象 存入到一个 容器,比如像 vector 当中是不可能的。

但是现在这三个函数已经被 function 包装类,包装成一个对象了,那么此时,我们就可以往vector 当中存储 一个个 类型为 function 的一个个 包装类对象(如上述的 f1  f2  f3 )。

如下所示,我们拿  vector 容器把上述的三个对象存入其中:

vector v = {f1 , f2  , f3 };

包装类本质上解决是什么问题呢?

在以前,我们想把 仿函数的可调用对象存储到一个容器当中,那么,这个容器的类型就只能是这个仿函数的类型,那么就只能存储这个这种类型的仿函数,想函数指针就不能一起存储了。

而且,像lambda表达式 的 可调用对象的类型我们根本不好控制,他是 lambda + uuid 的方式来命名的类型,基本上每一个 lambda表达式的可调用对象的类型都是不一样的。那么对于 同返回值类型 和 同参数列表的 lambda表达式 都不能存储在一起。

而像上述 一样 ,通过 function 包装类 包装过后,这些 可调用对象 就都被包装成了一个 function<返回值参数类型(参数包)> 这样类型的 function 对象,那么在vector 当中只需要存储 每一个 可调用对象包装成的 function 对象,就可以把 不同 类型的 可调用对象 存储在一起了:

vector v = {f1 , f2  , f3 };

double n = 1.1;
for(auto f : v)
{
    cout << f(n++) << endl;
}

输出:

 而且,我们都不用 像 上述的方式来写,不同多定义出 f1 f2 这些 function 的对象,可以直接 把函数的可调用对象传入进去

vector v = {
                                f 
                                , [](double d)->double { return d / 4; }
                                ,Functor()
                                };

上述的 vector 只是举一个例子,意思就是可以把 一个函数 利用包装类,包装成一个 function 对象,把 函数的可调用对象存储到一个 容器当中。

在使用 包装类, 包装两三个函数之后,这个三个可调用对象都被包装成 同一个 类型的 对象了,那么我们接下来在传入这三个包装之后的对象,因为这个三个对象的类型是一样 ,所以使用的是一个实例化出来的函数,也就意味着这三个函数就用的是一个 useF 的实例化函数了

// 函数名(函数指针)
std::function func1 = f;
cout << func1(1, 2) << endl;

// 函数对象
std::function func2 = Functor();
cout << func2(1, 2) << endl;

// lamber表达式
std::function func3 = [](const int a, const int b){return a + b; };
cout << func3(1, 2) << endl;

输出: 

C++ - 包装器 - bind()函数_第3张图片

 如上述,我们发现 count 的地址都是一样的,而且在三个函数当中对 count 的修改都能延伸。

 证明了 三个 可调用对象 的类型是一样的。


比如下述的引用场景:

150. 逆波兰表达式求值 - 力扣(LeetCode)

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 '+''-''*' 和 '/' 。
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断 。
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

 如果不用 上述包装器的话,我们是使用 if - else 或者是 switch 的方式来手动判断 当前是哪一个 操作符,然后在调用这个 操作符对应的 算法:

C++ - 包装器 - bind()函数_第4张图片

 可以发现,非常的麻烦。

我们可以使用键值对的方式来把 ,上述的 操作符 和 这个操作符对应的算法给 匹配起来。但是,算法是一行一行的代码,我们要想把代码存储到 一个 容器当中,就需要 像上述一样,写成一个函数,然后把这个函数的可调用对象包装成 一个 function 的对象,map 才能存储这个对象。

那么 ,map 当中就可以实现了, 一个 运算符 对应 一个 函数的算法(可调用对象包装出的 function 对像):

C++ - 包装器 - bind()函数_第5张图片

 向上述一样,我们使用 看起来最简便的 lambda 表达式,不使用 仿函数 这种使用起来 相对 lambda 来说比较臃肿。但是是因为上述的代码都比较少,如果是 代码量比较多的话,仿函数的方式分开写也是对于 可读性是有提高的。

C++ - 包装器 - bind()函数_第6张图片

bind

std::bind 

 是一个函数模版,它就像一个函数适配器(包装器)一样,接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

一般而言,我们可以用 bind 函数,传入一个 某一个函数 FN,这个FN函数当中有 N 个参数,通过绑定一些参数,返回一个接受 M 个 (M > N 是可以的,但是没有必要) 参数 的新函数(这个新函数是以 function 对象方式返回的),我们可以使用 bind 函数对这个新函数当中的 参数顺序进行调整

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

使用 bind 函数可以把上述 func函数 的 a 和 b 的参数位置进行交换。在函数当中相当于是 a 变成 b,b 变成a。

	function rSub2 = bind(func, placeholders::_2, placeholders::_1);
	cout << rSub2(10, 5) << endl;

输出:

-5

我们看到,如果传入 10 和 5 的话,在func()函数返回结果是 5,但是 经过 bind 修改后的 返回的 rSub 对象的返回结果是 -5。

而且,bind()函数只是对 func()原函数的基础之上,重新创建了一个新的函数,对这个新的函数进行修改,并不会影响到 func()原函数:
 

	function rSub1 = bind(func, placeholders::_1, placeholders::_2);
	cout << func(10, 5) << endl;

输出:
 

5

bind语法 

我们看到,上述使用了 placeholders ,这个其实是一个命名空间,这个命名空间声明了未指定数量的对象:_1,_2,_3,…,用于在调用bind函数时指定占位符:

C++ - 包装器 - bind()函数_第7张图片

 我们拿上述例子来说明语法:

C++ - 包装器 - bind()函数_第8张图片

C++ - 包装器 - bind()函数_第9张图片 _1 就代表的是 func 函数的第一个参数, _2 代表 func 的第二个参数·······以此类推。

 它相当于是 placeholders::_1 只 接受在调用函数时候传入的第一个参数,如上述的10; placeholders::_2 只 接受在调用函数时候传入的第二个参数,如上述的5·······


如果,我们使用的函数参数比较多的话,或者是同事,别人写的函数采纳数定义顺序我们写着不习惯,我们就可以选择调换参数位置,来符合我们的调用习惯。


 bind()函数的缺省参数写法

如果我们不想传某个参数,可以用 bind 函数给定缺省值。

如下面这个例子:

void func(int a, int b, double rate)
{
    return (a - b) * rate;
}

int main()
{
    // function对象的类型当中的模版参数不要写缺省参数的类型
    function plus1 = bind(func, placeholders::_1, placeholders::_2, 4.0);
    function plus2 = bind(func, placeholders::_1, placeholders::_2, 4.2);
    function plus3 = bind(func, placeholders::_1, placeholders::_2, 4.4);

    // 调用的时候也不用写 缺省参数的类型
    cout << plus1(10, 5) << endl;
    cout << plus2(10, 5) << endl;
    cout << plus3(10, 5) << endl;

    return 0;
}

 输出:
 

20
21
22

C++ - 包装器 - bind()函数_第10张图片

 上述 的 4.0  4.2  4.4  位置就是我们在 bind函数 当中的设置的缺省参数值,设置的缺省参数对应 func 当中的 rate 这个参数。 

 当然,如果 要缺省的参数位置改变,对应的 palceholders::_? 也需要改变吗?

如下所示:

double func(double rate ,int a, int b)
{
    return (a - b) * rate;
}

function plus1 = bind(func, 4.0, placeholders::_2, placeholders::_3); // 编译报错
function plus2 = bind(func, 4.2, placeholders::_1, placeholders::_2); // 编译通过

后序写 _1 和_2 的方式才是正确的,尽管 此时 的  placeholders::_1, 和 placeholders::_2 分别对应的是 第二个参数 和 第三个参数。其实可以理解为,bind函数当中缺省之后的参数就不计算在 参数列表当中了。

其实上述的理解要从调用 plus1 和 plus2 理解:

    cout << plus1(10, 5) << endl;
    cout << plus2(10, 5) << endl;

在调用函数的角度看来,不就是第一个参数和第二个参数吗?

 而且,如果我们想要缺省的参数是在 参数列表的中间位置,左右两边都有参数的话,从调用函数的角度来说更好一点:

C++ - 包装器 - bind()函数_第11张图片

总结一下:如果不管 bind() 出来的新函数其中有没有 bind 的缺省参数,对于 placeholders::_?指定哪一个参数,要看调用的时候是几个参数,那么对应的 _?  就是 调用函数时候对应的 第 ? 个参数。


看到这个你应该明白了bind 函数是如何使用 缺省参数的,其实bind ()函数实现的缺省参数在大体上看来要比 直接在函数参数列表当中写缺省值要好的。

因为,bind()函数生成的新函数的缺省参数是在 原本函数当中直接传值,也就是在原本的函数当中的被 bind()修改的成缺省参数的参数,本身不是缺省参数。

而且我们可以使用多次 bind 函数()对不同的参数进行缺省;还可以对同一个参数写出不同的缺省值;从而定义多个 function 对象。

 从上述观点来看,bind()的缺省参数是要更灵活一些的。


如果是重载函数想要 使用 bind ()函数的话区分 重载函数,用的是 function的模版参数


如果是类当中 public 的 成员函数,比如是静态函数,那么可以通过 "类名::函数名()" 的方式在类外调用的,那么,在 类当中的 静态成员函数,也是差不多的,"类名::函数名"

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

    function plus2 = bind(SubType::Sub, placeholders::_1,placeholders::_2);

 如果是 public 的 非静态的成员函数的话,不能像 "类名::函数名" 一样访问了。需要在之前加一个 "&" ,变成:"&类名::函数名":

 而且,非静态的成员函数和 静态成员函数不一样的是,虽然我们在该函数的参数列表当中写的 是 N 的参数,但是实际上是 N+1 个参数,因为非静态的成员函数的参数列表当中的 第一个参数不是我们在函数参数列表当中写的第一个参数,而且是指向当中对象的 this 指针。

 其实也是和之前保持一致的,我们想要调用 类当中的非静态的成员方法,就需要一个对象作为媒介,所以,调用其中的成员函数需要创建一个对象之后,传入这个对象的指针。

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

int main()
{
    SubType st;
    function plus2 = bind(&SubType::Sub, &st, placeholders::_1,placeholders::_2);

    return 0;
}

或者是传入一个匿名对象也行;   不传入一个对象指针,传入一个对戏那个也行。

  之所以支持这样的操作,是因为,不管是 lambda,bind()其实底层都是仿函数,在仿函数类当中要重写 operator()这个函数,上述不管是 &st   ,st  , 还是匿名对象,都是传给这个 operator()函数的。

    function plus2 = bind(&SubType::Sub, Subtype() , placeholders::_1,placeholders::_2);

    function plus2 = bind(&SubType::Sub, st , placeholders::_1,placeholders::_2);

静态的可以不加,但是也可以加上"&"。

你可能感兴趣的:(C++,c++,算法,开发语言)