包装器
在 C++ 当中可能会有各种各样的可调用类型,比如 函数指针,仿函数,lambda 等等,那么这么多的可调用类型,我们在使用的时候就会犯迷糊,那可不可以统一控制一下呢?
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()
{
// 函数名(函数名就相当于是函数指针)
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 静态变量输出的地址是不一样的:
通过上面的程序验证,我们会发现useF函数模板实例化了三份。
在主函数当中,三种方式都可以调用,也就是说:
ret = func(x);
上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率下!
那么,有没有方式和把上述的三种方式都统一下呢?
答案是有的。
就是使用包装器。
包装器语法
std::function 在头文件 <functional> 当中,其实 function 包装器本质上是一个 类,在这个类当中存储了 各种函数,有上述的 仿函数,lambda,函数指针都都可以存储其中。
function 类模板原型如下所示:
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
- 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<double(double)> f1 = f;
// 包装lambda表达式
function<double(double)> f2 = [](double d)->double { return d / 4; };
// 包装仿函数(传入仿函数类的匿名对象)
function<double(double)> f3 = Functor();
那么,此时就包装好了 这三个函数。
在没有使用包装类的时候,我们要想把这个些 可调用对象 存入到一个 容器,比如像 vector 当中是不可能的。
但是现在这三个函数已经被 function 包装类,包装成一个对象了,那么此时,我们就可以往vector 当中存储 一个个 类型为 function<double(double)> 的一个个 包装类对象(如上述的 f1 f2 f3 )。
如下所示,我们拿 vector 容器把上述的三个对象存入其中:
vector<function<double(double)> v = {f1 , f2 , f3 };
包装类本质上解决是什么问题呢?
在以前,我们想把 仿函数的可调用对象存储到一个容器当中,那么,这个容器的类型就只能是这个仿函数的类型,那么就只能存储这个这种类型的仿函数,想函数指针就不能一起存储了。
而且,像lambda表达式 的 可调用对象的类型我们根本不好控制,他是 lambda + uuid 的方式来命名的类型,基本上每一个 lambda表达式的可调用对象的类型都是不一样的。那么对于 同返回值类型 和 同参数列表的 lambda表达式 都不能存储在一起。
而像上述 一样 ,通过 function 包装类 包装过后,这些 可调用对象 就都被包装成了一个 function<返回值参数类型(参数包)> 这样类型的 function 对象,那么在vector 当中只需要存储 每一个 可调用对象包装成的 function 对象,就可以把 不同 类型的 可调用对象 存储在一起了:
vector<function<double(double)> v = {f1 , f2 , f3 };
double n = 1.1;
for(auto f : v)
{
cout << f(n++) << endl;
}
输出:
而且,我们都不用 像 上述的方式来写,不同多定义出 f1 f2 这些 function 的对象,可以直接 把函数的可调用对象传入进去:
vector<function<double(double)> v = {
f
, [](double d)->double { return d / 4; }
,Functor()
};
上述的 vector 只是举一个例子,意思就是可以把 一个函数 利用包装类,包装成一个 function 对象,把 函数的可调用对象存储到一个 容器当中。
在使用 包装类, 包装两三个函数之后,这个三个可调用对象都被包装成 同一个 类型的 对象了,那么我们接下来在传入这三个包装之后的对象,因为这个三个对象的类型是一样 ,所以使用的是一个实例化出来的函数,也就意味着这三个函数就用的是一个 useF 的实例化函数了:
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b){return a + b; };
cout << func3(1, 2) << endl;
输出:
如上述,我们发现 count 的地址都是一样的,而且在三个函数当中对 count 的修改都能延伸。
证明了 三个 可调用对象 的类型是一样的。
比如下述的引用场景:
150. 逆波兰表达式求值 - 力扣(LeetCode)
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
如果不用 上述包装器的话,我们是使用 if - else 或者是 switch 的方式来手动判断 当前是哪一个 操作符,然后在调用这个 操作符对应的 算法:
可以发现,非常的麻烦。
我们可以使用键值对的方式来把 ,上述的 操作符 和 这个操作符对应的算法给 匹配起来。但是,算法是一行一行的代码,我们要想把代码存储到 一个 容器当中,就需要 像上述一样,写成一个函数,然后把这个函数的可调用对象包装成 一个 function 的对象,map 才能存储这个对象。
那么 ,map 当中就可以实现了, 一个 运算符 对应 一个 函数的算法(可调用对象包装出的 function 对像):
向上述一样,我们使用 看起来最简便的 lambda 表达式,不使用 仿函数 这种使用起来 相对 lambda 来说比较臃肿。但是是因为上述的代码都比较少,如果是 代码量比较多的话,仿函数的方式分开写也是对于 可读性是有提高的。
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<int(int, int)> rSub2 = bind(func, placeholders::_2, placeholders::_1);
cout << rSub2(10, 5) << endl;
输出:
-5
我们看到,如果传入 10 和 5 的话,在func()函数返回结果是 5,但是 经过 bind 修改后的 返回的 rSub 对象的返回结果是 -5。
而且,bind()函数只是对 func()原函数的基础之上,重新创建了一个新的函数,对这个新的函数进行修改,并不会影响到 func()原函数:
function<int(int, int)> rSub1 = bind(func, placeholders::_1, placeholders::_2);
cout << func(10, 5) << endl;
输出:
5
bind语法
我们看到,上述使用了 placeholders ,这个其实是一个命名空间,这个命名空间声明了未指定数量的对象:_1,_2,_3,…,用于在调用bind函数时指定占位符:
我们拿上述例子来说明语法:
_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<double(int, int) > plus1 = bind(func, placeholders::_1, placeholders::_2, 4.0);
function<double(int, int) > plus2 = bind(func, placeholders::_1, placeholders::_2, 4.2);
function<double(int, int) > 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
上述 的 4.0 4.2 4.4 位置就是我们在 bind函数 当中的设置的缺省参数值,设置的缺省参数对应 func 当中的 rate 这个参数。
当然,如果 要缺省的参数位置改变,对应的 palceholders::_? 也需要改变吗?
如下所示:
double func(double rate ,int a, int b)
{
return (a - b) * rate;
}
function<double(int, int) > plus1 = bind(func, 4.0, placeholders::_2, placeholders::_3); // 编译报错
function<double(int, int) > 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;
在调用函数的角度看来,不就是第一个参数和第二个参数吗?
而且,如果我们想要缺省的参数是在 参数列表的中间位置,左右两边都有参数的话,从调用函数的角度来说更好一点:
总结一下:如果不管 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<int(int, int) > 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<int(int, int) > plus2 = bind(&SubType::Sub, &st, placeholders::_1,placeholders::_2);
return 0;
}
或者是传入一个匿名对象也行; 不传入一个对象指针,传入一个对戏那个也行。
之所以支持这样的操作,是因为,不管是 lambda,bind()其实底层都是仿函数,在仿函数类当中要重写 operator()这个函数,上述不管是 &st ,st , 还是匿名对象,都是传给这个 operator()函数的。
function<int(int, int) > plus2 = bind(&SubType::Sub, Subtype() , placeholders::_1,placeholders::_2);
function<int(int, int) > plus2 = bind(&SubType::Sub, st , placeholders::_1,placeholders::_2);
静态的可以不加,但是也可以加上"&"。