【C++】函数对象以及常见的std::function包装器
目录
引言
本文介绍了函数对象,以及在诞生于 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 <functional>
1
常见的算数运算符函数对象有std::plus
(+)、std::multiplies
(*)……
常见的逻辑运算符函数对象有std::equal_to
(==)、std::greater
(>)……
std::plus<float> p;
std::multiplies<float> mu;
// 5 + 2 5 * 2
std::cout << p(5, 2) << ' ' << mu(5, 2) << std::endl;
std::equal_to<float> e;
std::greater<uint8_t> g;
// 3.1 == 3.2 16 > 2
std::cout << e(3.1f, 3.2f) << ' ' << g(0x10, 0b10) << std::endl;
1.2.2 搜索器
C++17 提供了若干种搜索器,它们是适合用于 std::search
2 的 搜索器 重载的类,它将搜索操作委派到 C++17 前标准库的 std::search
。这里介绍默认搜索器:std::default_searcher
3,参考代码如下:
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:
/* explicit */ 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<void(int, double, string)> f1 = foo;
f1(1, 3.14, "abc");
cout << "<<<<<<<<<<< 成员函数 >>>>>>>>>>>" << endl;
function<void(const MyFoo &, string)> f2 = &MyFoo::myFoo;
MyFoo my_foo(123);
f2(my_foo, "hello");
f2(666, "hello"); // 调用隐式转换构造,注意不要使用 explicit 关键字
cout << "<<<<<<<<<<< 函数对象 >>>>>>>>>>>" << endl;
function<void(float)> f3 = MyFoo2();
f3(0.123f);
cout << "<<<<<<<<< lambda表达式 >>>>>>>>>" << endl;
function<int(int)> f4 = [](int a) -> int { return 2 * a; };
cout << f4(147) << endl;
return 0;
}
运行结果如下:
<<<<<<<<<<< 全局函数 >>>>>>>>>>>
1 3.14 abc
<<<<<<<<<<< 成员函数 >>>>>>>>>>>
123 hello
666 hello
<<<<<<<<<<< 函数对象 >>>>>>>>>>>
0.123
<<<<<<<<< lambda表达式 >>>>>>>>>
294
注意:
-
此处若使用
auto
关键字进行自动类型推导,会被当做普通的函数指针,但不影响结果。不过值得注意的是,被推导的函数不得有重载版本,否则编译不通过
-
使用包装器对成员函数进行操作时,第一个参数需要传入调用该成员函数的对象,因为成员函数和成员变量的内存是分开存储的(这等效于一个结构体
struct
和若干函数的内存表示)。而平常在访问成员函数时,编译器会自动将所调用对象地址赋值给this
指针,因此这里需要指定调用的对象。
2.2 std::bind
2.2.1 bind原型与基本用法
函数原型:此处传参使用变参模板4支持任意个数、任意类型,使用转发引用获取实参
template<typename F, typename... Args>
/*unspecified*/ 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<void(int, string, int, double)> 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.14True 20world1.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; }
运行结果如下
100 3.14
3 实际运用
前不久学了一下线程、进程的相关内容,在开源代码的指导下,摸索着搭了一个线程池库ThreadPool
,这里对我遇到过的有关函数包装器的内容做个简单的分享。其中在这个线程池库中有一个添加任务的函数addTask
,函数原型如下:
template <typename _Task>
void addTask(_Task &&task);
// 这里使用了转发引用,但任务队列真正需要的 Task 类型定义如下
using Task = std::function<void()>;
该函数要求传入一个无返回值的函数作为任务,若具有通用性,那么这个函数支持任意个数的参数,这点与要求相悖。并且实际上,每个线程在从队列中取用任务时执行的都是相同的操作,如以下代码片段:
// 以下代码为每个线程运行的 loop() 中获取任务以及执行任务的部分
Task task = takeTask(); // 取用任务
if (task)
{
task(); // 执行任务
}
因此在执行每个task
时,需要保证其参数列表一致。
如果每个任务的函数不一样,则可以使用 bind
绑定参数到可调用对象上,最终可以得到一个无参数的包装器,可参考本文 2.2.1 节示例代码。最终作为addTask
的参数进行添加任务的步骤,便可成功打入任务队列,可参考以下代码:
// 创建一个 vector
vector<int> mat(100);
// 为线程池添加任务
tp.addTask(bind(
[](vector<int> &m)
{
for (auto &element : m)
element += 10;
}, ref(mat)));
// 线程池待所有任务执行完毕后停用
tp.stop(false);
我们一步一步把调用内容拆解开,首先bind
的可调用对象为一个lambda表达式,该可调用对象经过std::function
包装后可以得到std::function<void(vector<int> &)>
类型。
因此bind
若要实现零参数,在使用时需传入两个无占位符的参数,第一个就是该lambda表达式,第二个就是传入lambda函数的待处理值:mat
,值得注意的是,若在函数包装器中使用引用作为参数,直接传入mat
是不行的:在参数传入bind
进行绑定时会创建一份新的内存空间副本,实际运行该可调用对象时(此处为lambda函数)才会取该副本的引用,导致真正的值不会被修改。因此需要用到std::ref
来得到其左值引用6,若传入的值不需要内部进行修改,则不需要使用std::ref
。
到这一步,bind
将 ref(mat)
绑定在了 lambda 表达式上,其返回值则可以被function<void()>
所转换,则可以成功加入任务队列。