欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

【C++】C++11——函数对象、function函数包装器及其相关工具

时间:2023-05-31
【C++】函数对象以及常见的std::function包装器 目录

【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 1

常见的算数运算符函数对象有std::plus(+)、std::multiplies(*)……

常见的逻辑运算符函数对象有std::equal_to(==)、std::greater(>)……

std::plus p;std::multiplies mu;// 5 + 2 5 * 2std::cout << p(5, 2) << ' ' << mu(5, 2) << std::endl;std::equal_to e;std::greater g;// 3.1 == 3.2 16 > 2std::cout << e(3.1f, 3.2f) << ' ' << g(0x10, 0b10) << std::endl;

1.2.2 搜索器

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::function

C++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 f1 = foo; f1(1, 3.14, "abc"); cout << "<<<<<<<<<<< 成员函数 >>>>>>>>>>>" << endl; function f2 = &MyFoo::myFoo; MyFoo my_foo(123); f2(my_foo, "hello"); f2(666, "hello"); // 调用隐式转换构造,注意不要使用 explicit 关键字 cout << "<<<<<<<<<<< 函数对象 >>>>>>>>>>>" << endl; function f3 = MyFoo2(); f3(0.123f); cout << "<<<<<<<<< lambda表达式 >>>>>>>>>" << endl; function f4 = [](int a) -> int { return 2 * a; }; cout << f4(147) << endl; return 0;}

运行结果如下:

<<<<<<<<<<< 全局函数 >>>>>>>>>>>1 3.14 abc<<<<<<<<<<< 成员函数 >>>>>>>>>>>123 hello666 hello<<<<<<<<<<< 函数对象 >>>>>>>>>>>0.123<<<<<<<<< lambda表达式 >>>>>>>>>294

注意:

此处若使用auto关键字进行自动类型推导,会被当做普通的函数指针,但不影响结果。不过值得注意的是,被推导的函数不得有重载版本,否则编译不通过

使用包装器对成员函数进行操作时,第一个参数需要传入调用该成员函数的对象,因为成员函数和成员变量的内存是分开存储的(这等效于一个结构体struct和若干函数的内存表示)。而平常在访问成员函数时,编译器会自动将所调用对象地址赋值给 this 指针,因此这里需要指定调用的对象。

2.2 std::bind 2.2.1 bind原型与基本用法

函数原型:此处传参使用变参模板4支持任意个数、任意类型,使用转发引用获取实参

template bind(F&& f, Args&&..、args);

函数模板 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 fun = foo; auto f = bind(fun, _1, "hello", _1, _2); f(3, 2, 4); return 0;}

运行结果如下:

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 void addTask(_Task &&task);// 这里使用了转发引用,但任务队列真正需要的 Task 类型定义如下using Task = std::function;

该函数要求传入一个无返回值的函数作为任务,若具有通用性,那么这个函数支持任意个数的参数,这点与要求相悖。并且实际上,每个线程在从队列中取用任务时执行的都是相同的操作,如以下代码片段:

// 以下代码为每个线程运行的 loop() 中获取任务以及执行任务的部分Task task = takeTask(); // 取用任务if (task){ task(); // 执行任务}

因此在执行每个task时,需要保证其参数列表一致。

如果每个任务的函数不一样,则可以使用 bind 绑定参数到可调用对象上,最终可以得到一个无参数的包装器,可参考本文 2.2.1 节示例代码。最终作为addTask的参数进行添加任务的步骤,便可成功打入任务队列,可参考以下代码:

// 创建一个 vectorvector mat(100);// 为线程池添加任务tp.addTask(bind( [](vector &m) { for (auto &element : m) element += 10; }, ref(mat)));// 线程池待所有任务执行完毕后停用tp.stop(false);

我们一步一步把调用内容拆解开,首先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 ↩︎

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。