前言

本文介绍的C++11新特性是:包装器


为什么需要包装器

在C++中,可能有各种各样的可调用类型,如函数、函数对象(仿函数)、lambda表达式。

那么如果我们将这些可调用的类型,传给一个函数模板时:

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 << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}
//输出:
//count:1
//count : 00B1C198
//5.555
//count : 1
//count : 00B1C19C
//3.70333
//count : 1
//count : 00B1C1A0
//2.7775

func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!因为函数模板本身实例化了三份。

那么我们有什么方式可以统一他们的类型呢? 即让改函数模版只实例化为一份。还有一些情况,如果我们需要将一个可调用对象存到容器中,又如何实现呢?(存lambda拿不到类型,存函数指针很难看复杂)

此时就可以用包装器:

1. function包装器

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

1.1 使用方法

std::function在头文件<functional>
// 类模板原型如下
template <class Ret, class... Args>
class function<Ret(Args...)>;  //定义时需求是这样,这里是特殊语法,没有什么巧妙处。

模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

包装器本质可以理解为一种适配器模式,可以对你的可调用对象进行包装,成我们需要的东西。
其实就是用一个具体的类模版实例化一个类型,其对我们的可调用对象都进行了相应的适配(封装),让我们需要可调用对象的类型的时候可以有具体的类型写。

包装器解决的是----可调用对象的类型问题!!! 且完成了可以将不同的可调用类型统一类型的工作。

#include<functional>


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

void test()
{
	function<double(double)> f1 = f;
	function<double(double)> f2 = functor();
	function<double(double)> f3 = [](double d)->double { return d / 4; };

	vector<function<double(double)>> v = { f1,f2,f3 };
	// 还可以不定义包装器类型,直接将类型写入:
	//vector<function<double(double)>> v = { f,functor(),[](double d)->double { return d / 4; } };
	double n = 6.6;
	for (auto f : v)
	{
		cout << f(n) << endl;
	}
}
//输出:
//3.3
//2.2
//1.65

这样只要是我们返回值和接收参数相同的可调用类型,都被 “包装” 成了同一种类型,针对不同可调用类型函数模板实例化成不同函数的效率问题也就解决了。

#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);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	// lamber表达式
	std::function<double(double)> func3 = [](double d)->double { return d /4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}
//输出
//count:1
//count : 00C1E568
//5.555
//count : 2
//count : 00C1E568
//3.70333
//count : 3
//count : 00C1E568
//2.7775

注意:
我们的auto,推断的是基础的类型,而推断不出function包装器类型,所以我们尽量不要使用auto。


2. bind包装器

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

我们的function虽也是包装器,但是其是类模版,它可以封装我们的所有可调用类型,以解决需要使用可调用类型的场景;
但是这里bind它是一个函数模版,它接收一个可调用对象,并可通过一些规则绑定它的参数,从而实现一些对参数的操作:


(1)实现一个可调用对象的参数顺序调整
(2)接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数等操作。

2.1 使用方法

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

调用bind的一般形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。
当我们调用newCallable时,newCallable会调用传入的可调用对象callable,并将arg_list中的参数传给他,然后将其参数传出。
arg_list中的参数可能包含形如 placeholders::_n的名字,n是一个整数, placeholders是一个命名空间域,这些参数是“占位符”,表示newCallable的参数,它们占据了传递newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

bind返回的也是一个可调用对象,即仿函数,所以我们可以使用function接收它。

2.1.1 绑定可调用对象(仿函数)

class DivisionType
{
public:
	double operator()(int s, double a, double b)
	{
		return a / b - s;
	}
};

void bindFunctionObject()
{
	function<double(double,double)> f = bind(DivisionType(),1, placeholders::_1, placeholders::_2);  
	cout << f(1, 2) << endl;    //-0.5
}

上面我们传入的是一个匿名函数对象,即没有创建对象的,我们创建一个对象之后再传入也可以:

DivisionType d;
function<double(double, double)> f = bind(d, 1, placeholders::_1, placeholders::_2);

2.1.2 绑定lambda

void bindLambda()
{
	auto lambda = [](int a, int b, int c) {return a + b + c; };
	function<int(int, int)> f = bind(lambda, 1, placeholders::_1, placeholders::_2);
	//function<double(double, double)> f = bind([](int a, int b, int c) {return a + b + c; }, 1, placeholders::_1, placeholders::_2);
	cout << f(1, 2) << endl;    //4
}

2.1.3 绑定函数

(1)绑定普通函数
int add(int a,int b,int rate)
{
	return (a + b) * rate;
}

void bindFunc()
{
	function<int(int,int)> f = bind(add, placeholders::_1, placeholders::_2, 6);
	cout << f(2, 3) << endl;    //30
}
(2)绑定静态成员函数

静态成员函数不需要对象指针。

class MultiType
{
public:
	static int Multi(int a,int b,int rate)
	{
		return a * b * rate;
	}
};

void bindStaticMemberFunc()
{
	function<int(int, int)> f = bind(&MultiType::Multi, placeholders::_1, placeholders::_2, 6);  //“SubType::subtract”: 非标准语法;请使用 "&" 来创建指向成员的指针
	cout << f(1, 2) << endl;    //12
}
(3)绑定成员函数

成员函数也是函数,也是可以调用的类型,所以它也可以绑定,但是我们知道成员函数是专门针对一个对象的,因为其有this指针,所以我们需要额外传入对象的this指针。

class SubType
{
public:
	int subtract(int a, int b, int rate)
	{
		return (a - b) * rate;
	}
};

void bindMemberFunc()
{
	SubType s;
	function<int(int,int)> f = bind(&SubType::subtract, &s, placeholders::_1, placeholders::_2, 6);  //“SubType::subtract”: 非标准语法;请使用 "&" 来创建指向成员的指针
	cout << f(6, 4) << endl;  //12
}

注意:
我们的传入的可调用对象成员函数,必须加&,如:&SubType::subtract,否则报错:“SubType::subtract”: 非标准语法;请使用 “&” 来创建指向成员的指针。
因为C++中成员函数指针的语法必须使用 &运算符,因为成员函数是操作对象的,必须要指明一个对象this。

而后面传入的对象指针this,我们可以取地址&s,也可以直接传对象s都可以,这涉及到bind的底层原理:
bind绑定的底层和lambda类似,也是生成了一个仿函数,该仿函数内部去调用我们需要绑定的函数,其调用方式有几种:
①指针调用;②对象调用。

2.2 浅谈bind原理

我们不用function包装器包装我们bind生成的可调用对象,直接用auto去接收,看看bind生成的真正可调用对象的类型:

void test()
{
	auto func = bind(add, placeholders::_1, placeholders::_2, 3);
	cout << typeid(func).name() << endl;   //输出类型: class std::_Binder<struct std::_Unforced,int (__cdecl&)(int,int,int),struct std::_Ph<1> const &,struct std::_Ph<2> const &,int>
}

bind内部使用我们传入的类型参数给到Binder模版类,实例化了一个Binder类,然后传给了我们的Binder类中的operator()(没错该类中也实现了一个operator()).

在实例化出来的Binder类中,又使用我们传入的可调用类型和参数作为operator()函数的参数,在operator()函数中使用参数去调用可调用类型,在这个过程中,Binder类中有保存和绑定参数位置相关的结构和成员实现参数位置的匹配。

总结

function包装器是类模板,其目的是:对我们的可调用对象都进行了相应的适配(封装),让我们需要可调用对象的类型的时候可以有具体的类型写。
bind包装器是函数模板,但是其内部原理类似lambda表达式,通过在内部创建一个仿函数,并通过期间的操作,控制参数的匹配。


本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点 😃。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐