1、介绍
在 C++ 中,"包装器"(Wrapper)通常指的是一类技术,用于对其他编程接口提供更一致或更适合的接口。C++ 标准库中有几种重要的包装器,function、bind、mem_fn、reference_wrapper。
1. std::function
std::function
是一个类型擦除的包装器,可以封装任何可调用对象(如函数、函数对象、lambda 表达式等)。它是在 C++11 标准中引入的。
2. std::bind
std::bind
是一个函数适配器,可以将一个可调用对象与特定的参数绑定,生成一个新的可调用对象。它也在 C++11 中引入。
3. std::mem_fn
std::mem_fn
是一个函数适配器,用于将成员函数指针包装为可调用对象。它也在 C++11 中引入。
4. std::reference_wrapper
std::reference_wrapper
是一个简单的包装器,用于封装对象引用,使其可以存储在需要复制对象的容器中。它也在 C++11 中引入。
本章将介绍function包装器和bind包装器:
2、function包装器
模版形式
template <class T> function; // undefined
template <class Ret, class... Args> class function<Ret(Args...)>;
泛型模板声明:
template <class T> function;
表示定义了一个模板类 function
,但没有给出具体的实现。
模板类的具体定义,其中:
Ret
:被包装的可调用对象的返回值类型。Args...
:被包装的可调用对象的形参类型。
function<int(int, int)> op;
如果将函数、函数对象、lambda表达式等赋值给包装器对象op,那么就相当于把函数、函数对象、lambda表达式等装进了一个名为op的盒子,通过这个盒子就能以一个统一的方式使用函数、函数对象、lambda表达式等。
首先需要添加头文件 <functional>
2.1 函数指针、函数对象(仿函数)、lambda表达式的包装。
int f(int a, int b)
{return a + b;
}
class Add
{
public:int operator ()(int a, int b){return a + b;}
};
function<int(int, int)> f1 = f; // 函数指针
function<int(int, int)> f2 = Add(); // 仿函数
function<int(int, int)> f3 = [](int a, int b) { return a + b; };// lambda表达式
cout << f1(1, 2) << endl;
cout << f1(1, 2) << endl;
cout << f1(1, 2) << endl;
输出:
3
3
3
2.2 静态和非静态成员函数的包装
class Plus
{
public:static int Plusi(int a, int b) // 静态成员函数,参数列表中没有this指针{return a + b;}double Plusd(double a, double b)// 成员函数,参数列表中有this指针{return a + b;}
};
2.2.1 包装静态成员函数
function<int(int, int)> f4 = &Plus::Plusi;
// function<int(int, int)> f4 = Plus::Plusi; // 静态成员函数Plus::Plusi前可以不加&
cout << f4(1, 2) << endl; // 输出3
2.2.2 包装非静态成员函数
function<double(Plus*, double, double)> f5 = &Plus::Plusd;
2.2.2.1 注意:非静态成员函数取函数指针时,必须要加&
cout << f5(new Plus(), 1.1, 2.2) << endl; // 输出3.3
Plus pd;
cout << f5(&pd, 1.1, 2.2) << endl; // 输出3.3
2.2.2.2 也可以使用对象(有名或者匿名)
// 也可以使用对象(有名或者匿名):
function<double(Plus, double, double)> f6 = &Plus::Plusd;
Plus pd;
cout << f6(pd, 1.1, 2.2) << endl; // 有名对象,输出3.3
cout << f6(Plus(), 1.1, 2.2) << endl; // 匿名对象,输出3.3
2.2.2.3 使用引用
Plus& _pd = pd;
function<double(Plus&, double, double)> f7 = &Plus::Plusd;
cout << f7(_pd, 1.1, 2.2) << endl; // 输出3.3
到这里,可能有人会有疑问,为什么非静态成员函数的第一个参数可以传this指针又可以传对象又或是引用?
这是因为底层是先将这个参数以成员变量的形式存储起来,然后用该成员变量调用函数。
2.2.3 只包装对象的引用,为了返回数据成员的引用
#include <iostream>
#include <functional>class MyClass
{
public:int value;MyClass(int v) : value(v) {}
};int main()
{MyClass obj(42);// 使用 std::function 包装非静态数据成员指针std::function<int& (MyClass&)> func = [](MyClass& obj) -> int& { return obj.value; };// 获取并修改成员值int& val = func(obj); // obj 是指向包含数据成员的对象的引用std::cout << "原始value值: " << val << std::endl;val = 100; // 修改成员值std::cout << "修改后value值: " << obj.value << std::endl; return 0;
}
2.2.4 function
对象没有目标
#include <iostream>
#include <functional>int main()
{try {// 创建一个空的 std::function 对象std::function<void()> func;// 尝试调用空的 std::function 对象func(); // 这将引发 std::bad_function_call 异常} catch (const std::bad_function_call& e) {// 捕获并处理 std::bad_function_call 异常std::cerr << "Exception caught: " << e.what() << std::endl;}return 0;
}
输出:
Exception caught: bad function call
对于function实例化和调用总结:
公共成员函数
std::function::operator()
Ret operator()(Args... args) const;
调用目标
调用目标可调用对象,将参数转发给它。
其效果取决于
std::function
对象所指向的可调用对象的类型:
如果目标是一个函数指针或函数对象,则直接调用它,并将参数转发给该调用。
如果目标是一个非静态成员函数指针,则使用第一个参数作为调用该成员函数的对象(这个对象可以是一个对象、引用或指向该对象的指针),其余的参数则作为成员函数的参数进行转发。
如果目标是一个非静态数据成员指针,则应该只接受一个参数,并返回该参数的对应成员的引用(参数可以是对象、引用或指向对象的指针)。
如果对象没有目标(即它是一个空函数),则会抛出
std::bad_function_call
异常。
包装器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);
}
其中:
-
F
:表示任意的可调用对象,比如函数指针、仿函数、lambda表达式等; -
T
:表示(包装器)传入的参数。
在函数模板内部定义了静态变量count,通过打印它的地址来验证每次调用的是否是同一个函数。
useF函数会有几份取决于编译器如何实例化模板函数。实例化是指编译器根据模板函数和具体的类型参数生成一个特定的函数。一般来说,每种类型参数都会生成一个不同的实例,所以如果你用了三种类型参数,就会有三份useF函数。
// 普通函数
double f(double d)
{return d / 2;
}
// 函数对象
struct Functor
{double operator()(double d){return d / 4;}
};
// lambda表达式
auto l = [](double d)
{return d / 8;
};
int main()
{cout << useF(f, 12.12) << endl; // 函数指针cout << useF(Functor(), 12.12) << endl; // 函数对象cout << useF(l, 12.12) << endl; // lambda表达式return 0;
}
输出:
count: 1
&count: 00007FF63B4301C4
6.06
count: 1
&count: 00007FF63B4301C8
3.03
count: 1
&count: 00007FF63B4301CC
1.515
结果表明,编译器通过不同的模板参数实例化出了3份不同的函数。因为代码中使用了函数模板useF,并且用三种不同类型的参数(函数指针、函数对象和lambda表达式)调用了它。这意味着编译器会为每种参数类型生成一个useF的特化版本。
模板实例化是从一个模板声明和一个或多个模板参数创建一个函数、类或类成员的新定义的过程。创建的定义称为特化。
然而,我们使用包装器的目的是能以一种统一的方式使用函数指针、函数对象和lambda表达式等功能。三次调用useF函数时传入的可调用对象虽然是不同类型的,但这三个可调用对象的返回值和形参类型都是相同的,显然这会对效率造成一定影响。
如果想让编译器只实现一份useF,从而调用函数指针、函数对象和lambda表达式,你可以使用显式实例化。显式实例化是指在代码中明确地指定要生成哪些类型的模板函数,而不是让编译器自动推断。这样可以避免重复或冗余的实例化,并提高编译效率。
不过,因为三个可调用对象的类型不同,显示实例化对应的签名也是不同的,可以使用包装器,将它们都放进一个盒子里,然后通过这个唯一的盒子实例化出唯一的函数。
int main()
{function<double(double)> op; // 实例化包装器op = f; // 函数指针cout << useF(op, 8.88) << endl;op = Functor(); // 函数对象cout << useF(op, 8.88) << endl;op = l; // lambda表达式cout << useF(op, 8.88) << endl;return 0;
}
输出:
count: 1
&count: 00007FF6EA6925D4
4.44
count: 2
&count: 00007FF6EA6925D4
2.22
count: 3
&count: 00007FF6EA6925D4
1.11
以上三个函数因为参数类型一致,所以没有实例化多余的对象。
2.3 意义
function包装器是一种特殊的类模板,它可以包装一个可调用的目标,并提供一个统一的接口(明确了可调用对象的返回值和形参类型)。它的意义在于可以让你使用不同类型的函数或函数对象,而不需要关心它们的具体实现或参数类型。它也可以让你更容易地复用和组合现有的函数或函数对象。
3、bind包装器
bind包装器是一种函数对象,它可以将一个可调用对象和一些参数绑定在一起,形成一个新的可调用对象,本质是一个函数模板。可以使用std::bind或std::bind_front和std::bind_back来创建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...
:要绑定的参数列表:值或占位符。
形式:auto newCallable = bind(callable, arg_list);
其中:
callable
:需要包装的可调用对象。newCallable
:生成的新的可调用对象。arg_list
:逗号分隔的参数列表,对应给定的callable的参数。当调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
bind绑定会将处理后的可调用对象返回成另一个仿函数,并重载了operator()
占位符:参数列表中可能有形如_1
、_2
…_n
这样的参数,分别表示对应调用对象的第一个实参、第二个实参……第n个实参,这些参数被称为占位符。
使用时需要指定命名空间,或者展开命名空间:
using namespace std::placeholders;
调整参数顺序
可以使用不同编号的占位符来改变参数的顺序或者重复使用某些参数。
但是占位符一定是从_1开始的,因为实参的数量也是从第1个开始的 。
auto sub1 = bind(std::minus<int>(), std::placeholders::_1, std::placeholders::_2);
cout << sub1(10, 20) << endl;
auto sub2 = bind(std::minus<int>(), std::placeholders::_2, std::placeholders::_1);
cout << sub2(10, 20) << endl;
-10
10
sub1是无意义的,和调用原对象是一样的 。
sub2是将调用对象中的第一个和第二个实参换了顺序,并传递给std::minus<int>()继续调用。
然而这种特性并不常用。
绑定固定参数
现在我们要写一个只提供本金就可以计算出收益的函数:
auto func = [=](double rate, double money, int year) -> double{double ret = money;for (int i = 0; i < year; ++i){ret += ret * rate;}return ret - money;};// bind 用于对函数进行参数绑定
function<double(double)> func3_1_5 = bind(func, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func, 0.035, _1, 20);cout << func3_1_5(10000) << endl;
cout << func5_1_5(10000) << endl;
cout << func10_2_5(10000) << endl;
cout << func20_3_5(10000) << endl;
输出:
456.784
772.84
2800.85
9897.89
现在来分析这段代码:
class Plus
{
public:int add(int a, int b){return a + b;}
};auto func3 = bind<int>(&Plus::add, Plus(), _1, _2);
Plus p;
Plus* ptr = &p;
auto func4 = bind<int>(&Plus::add, ptr, _1, _2);
前面说过,用function包装非静态成员函数,参数中需要传递对象或对象的指针,这里同样如此,
只不过是将参数 (Plus(), _1, _2) 或 (ptr, _1, _2)进一步传递给可调用对象add。
bind作用
bind包装器的意义是用来绑定函数调用的某些参数,将可调用对象保存起来,然后在需要的时候再调用。它可以支持普通函数、函数对象和成员函数,并且可以使用占位符来灵活地指定参数。
bind包装器最大的作用就是将某些固定的参数和可调用对象绑定在一起,也可以在某些情况下赋予可调用对象以默认值。
总结
std::function
提供了一种统一的方法来存储和调用任何可调用对象,是现代 C++ 编程中广泛使用的工具。std::bind
则通过参数绑定来创建新函数对象,简化了函数调用的复杂性,尤其在处理成员函数和预先设置部分参数时非常有用。
这两个工具可以单独使用,也可以结合起来,用于构建灵活、简洁的代码。