【C++】函数对象以及常见的std::function包装器
引言1 函数对象
1.1 自定义函数对象1.2 常见内置函数对象
1.2.1 运算符函数对象1.2.2 搜索器 2 函数包装器与部分函数应用
2.1 函数包装器——std::function2.2 std::bind
2.2.1 bind原型与基本用法2.2.2 bind与占位符2.2.3 bind用于成员函数与成员变量 3 实际运用参考文档 引言
本文介绍了函数对象,以及在诞生于 C++11 新标准下的函数包装器及其涉及到的部分函数应用。大多数介绍的内容均可在 cppreference 上找得到,感兴趣的小伙伴也可以直接跳转至参考文档的第 3 条内容。本文对这些常用的工具做了个汇总,并且展示了在实战代码中是如何用到这些内容的。
1 函数对象凡是重载 operator() 运算符的对象都属于函数对象。
1.1 自定义函数对象一般自定义函数对象需要在类中重载函数调用运算符 operator(),示例代码如下:
struct MyPlus{ int operator()(int a, int b) { return a + b; }};
首先需要根据自定义类实例化一个对象,也可以是匿名对象(临时变量),在 main 函数中调用
int main(int argc, char *argv[]){ MyPlus plus; // 1 + 2 3 + 4 cout << plus(1, 2) << ' ' << MyPlus()(3, 4) << endl; return 0;}
运行结果如下:
3 7
1.2 常见内置函数对象 1.2.1 运算符函数对象 这些函数对象在使用前需要包含相应的头文件 #include
常见的算数运算符函数对象有std::plus(+)、std::multiplies(*)……
常见的逻辑运算符函数对象有std::equal_to(==)、std::greater(>)……
std::plus
C++17 提供了若干种搜索器,它们是适合用于 std::search2 的 搜索器 重载的类,它将搜索操作委派到 C++17 前标准库的 std::search。这里介绍默认搜索器:std::default_searcher3,参考代码如下:
int main(){ // 定义串和待搜索的子串 string sentence = "You should have gone for the head."; string word = "have"; auto word_it = default_searcher(word.begin(), word.end()); // 直接使用 std::default_searcher auto res = word_it(sentence.begin(), sentence.end()); cout << '"' << string(sentence.begin(), res.first) << '"' << endl; // 传统 std::search auto it_1 = search(sentence.begin(), sentence.end(), word.begin(), word.end()); cout << '"' << string(sentence.begin(), it_1) << '"' << endl; // std::default_searcher 配合 std::search 使用 auto it_2 = search(sentence.begin(), sentence.end(), word_it); auto it_3 = search(sentence.begin(), sentence.end(), default_searcher(word.begin(), word.end())); cout << '"' << string(sentence.begin(), it_2) << '"' << endl; cout << '"' << string(sentence.begin(), it_3) << '"' << endl;}
it_3 一行创建的是一个匿名的临时对象,这也是配合 std::search 使用时最标准的使用方法。运行结果如下:
"You should ""You should ""You should ""You should "
2 函数包装器与部分函数应用 2.1 函数包装器——std::functionC++11提供了一个通用多态函数包装器std::function<>,使其能够存储、复制、调用任何可复制构造的可调用对象1,包括全局函数、成员函数、函数对象、lambda表达式、bind表达式。以下示例代码为std::function<>对前4种可调用对象进行了包装。
void foo(int a, double b, string c){ cout << a << ' ' << b << ' ' << c << endl;}class MyFoo{ int _a;public: MyFoo(int a) : _a(a) {} void myFoo(string b) const { cout << _a << ' ' << b << endl; }};struct MyFoo2{ inline void operator()(float a) { cout << a << endl; }};int main(){ cout << "<<<<<<<<<<< 全局函数 >>>>>>>>>>>" << endl; function
运行结果如下:
<<<<<<<<<<< 全局函数 >>>>>>>>>>>1 3.14 abc<<<<<<<<<<< 成员函数 >>>>>>>>>>>123 hello666 hello<<<<<<<<<<< 函数对象 >>>>>>>>>>>0.123<<<<<<<<< lambda表达式 >>>>>>>>>294
注意:
此处若使用auto关键字进行自动类型推导,会被当做普通的函数指针,但不影响结果。不过值得注意的是,被推导的函数不得有重载版本,否则编译不通过
使用包装器对成员函数进行操作时,第一个参数需要传入调用该成员函数的对象,因为成员函数和成员变量的内存是分开存储的(这等效于一个结构体struct和若干函数的内存表示)。而平常在访问成员函数时,编译器会自动将所调用对象地址赋值给 this 指针,因此这里需要指定调用的对象。
2.2 std::bind 2.2.1 bind原型与基本用法函数原型:此处传参使用变参模板4支持任意个数、任意类型,使用转发引用获取实参
template
函数模板 std::bind 生成 f 的转发调用包装器5。调用此包装器等价于以一些绑定到 args 的参数调用 f,示例代码如下:
void foo(int a, double b, string c){ cout << a << ' ' << b << ' ' << c << endl;}int main(){ auto f = bind(foo, 123, 3.14, "hello"); f(); // 运行结果为:123 3.14 hello return 0;}
2.2.2 bind与占位符除此之外,我们也可以使用_1, _2, _3, ...作为args的占位参数,这将很大提高了bind使用的灵活性,这些占位参数被定义在命名空间std::placeholders下5,示例代码如下:
inline void foo(int a, string b, int c, double d){ cout << a << ' ' << b << ' ' << c << ' ' << d << endl;}int main(){ function
运行结果如下:
3 hello 3 2
实际上,_1绑定在第一个实参3上,_2绑定在第二个实参2,_3绑定在第二个实参4,在调用中,只对_1和_2进行了访问,因此形参a和c都为3,形参d为2。
2.2.3 bind用于成员函数与成员变量用于成员函数
bind 的第一个参数永远是可调用对象所在地址,对成员函数进行操作时,使用std::function传参的第一个参数是调用该成员函数的对象,在这种情况下 bind 的第二个参数同样也是调用该成员函数的对象,并且该对象也可使用占位符,示例代码如下:
struct MyFunction{ void foo(int a, string b, double c, bool d) { cout << a << b << c << (d ? "True" : "False") << endl; }};int main(){ MyFunction fun; auto f = bind(&MyFunction::foo, fun, _1, "hello", 3.14, _2); f(10, true); auto g = bind(&MyFunction::foo, _3, 20, "world", 1.23, false); g(100, "my_fun", fun); return 0;}
在运行 15 行代码时,仅fun被绑定在了_3上,因此前两个参数均无效。运行结果如下:
10hello3.14True20world1.23False
用于成员变量
前两个参数的调用规则与操作成员函数的一致,由于获取成员变量时无其余操作,因此无需其余参数。示例代码如下:
struct A{ int num1; double num2;};int main(){ A a = {100, 3.14}; auto f = bind(&A::num1, a); cout << f() << endl; auto g = bind(&A::num2, _1); cout << g(a) << endl; // _1 绑定 a return 0;}
运行结果如下
1003.14
3 实际运用前不久学了一下线程、进程的相关内容,在开源代码的指导下,摸索着搭了一个线程池库ThreadPool,这里对我遇到过的有关函数包装器的内容做个简单的分享。其中在这个线程池库中有一个添加任务的函数addTask,函数原型如下:
template
该函数要求传入一个无返回值的函数作为任务,若具有通用性,那么这个函数支持任意个数的参数,这点与要求相悖。并且实际上,每个线程在从队列中取用任务时执行的都是相同的操作,如以下代码片段:
// 以下代码为每个线程运行的 loop() 中获取任务以及执行任务的部分Task task = takeTask(); // 取用任务if (task){ task(); // 执行任务}
因此在执行每个task时,需要保证其参数列表一致。
如果每个任务的函数不一样,则可以使用 bind 绑定参数到可调用对象上,最终可以得到一个无参数的包装器,可参考本文 2.2.1 节示例代码。最终作为addTask的参数进行添加任务的步骤,便可成功打入任务队列,可参考以下代码:
// 创建一个 vectorvector
我们一步一步把调用内容拆解开,首先bind的可调用对象为一个lambda表达式,该可调用对象经过std::function包装后可以得到std::function
因此bind若要实现零参数,在使用时需传入两个无占位符的参数,第一个就是该lambda表达式,第二个就是传入lambda函数的待处理值:mat,值得注意的是,若在函数包装器中使用引用作为参数,直接传入mat是不行的:在参数传入bind进行绑定时会创建一份新的内存空间副本,实际运行该可调用对象时(此处为lambda函数)才会取该副本的引用,导致真正的值不会被修改。因此需要用到std::ref来得到其左值引用6,若传入的值不需要内部进行修改,则不需要使用std::ref。
到这一步,bind 将 ref(mat) 绑定在了 lambda 表达式上,其返回值则可以被function
std::search ↩︎ ↩︎
默认搜索器 ↩︎
function多态函数包装器 ↩︎
变参模板 Variadic Templates ↩︎
std::bind 可调用对象参数绑定工具 ↩︎ ↩︎
std::ref ↩︎