函数调用运算符重载与仿函数
运算符重载拓展:operator()与仿函数
我们已经在课上学习了C++运算符重载。然而乍看上去,有一些重载未免有些奇怪:我们到底为什么需要重载它们呢?重载new、 delete,或者[]、()又有什么用呢?这里就operator()所拓展出的功能——仿函数(functor)做一点简单的介绍。
从重载operator()到仿函数
首先复习一下operator()的重载。一个常见的operator()重载可能长成这样:
class Add {
int operator()(int a, int b)
{
return a + b;
}
};
于是一个Add类的对象可以这样使用:
#include <iostream>
using namespace std;
int main()
{
Add t;
cout << t(2,3) << endl; // 输出2+3的运算结果
}
经过重载,Test类的对象表现得就像个函数,可以像函数一样被调用,也做着函数该做的事。可以看出,operator()为我们提供了一种方法,使我们可以利用对象模拟函数的行为。自然,这样的对象就被称为仿函数(functor)。
为什么要用仿函数?
当我们有了仿函数的概念,也就自然产生了这样的问题。毕竟,有好好的函数不用,何必要大费周章去写一个类和重载呢?不妨设想这样一个例子。假如有一个int数组,我们想要统计其中能被5整除的数据的个数。我们可以写一个函数来完成:
int count_if_mod_5(int* arr, int size)
{
int count = 0;
for(int i = 0; i < size; ++i)
{
if(arr[i] % 5 == 0)
count++;
}
return count;
}
写成函数的好处是把功能模块化,方便复用。比如可以在另外一个相对更主要的函数里调用这个count_if_mod_5函数:
void do_something(int* arr, int size)
{
// do something
int cnt = count_if_mod_5(arr, size);
// do something
}
更一般地,为了更方便地调用不同的模块,我们可以用函数指针来调用其他函数:
void do_something(int* arr, int size, int (*fp)(int*, int))
{
// do something
int whatever = fp(arr, size);
// do something
}
函数指针给予了我们更大的灵活性,使得这个函数不局限于对整除5的数据计数——只要满足函数指针相应的接口就行。但是这足够了吗?回到count_if_mod_5。假如我们的需求发生了改变,现在改成要统计整除10的数据个数了。怎么办?最直接的办法当然是再写一个count_if_mod_10。但是追求~~lazy~~简洁和优雅的程序员怎么能满足于此呢?显然k应该被处理为变量。使用全局变量?虽然可以实现功能,但是可能产生的各种bug使之并不是最佳选择。k应该作为函数的一个参数会更好。于是可能想这样写:
int count_if_mod_k(int* arr, int size, int k)
{
int count = 0;
for(int i = 0; i < size; ++i)
{
if(arr[i] % k == 0)
count++;
}
return count;
}
但是这样会有一个问题:原来采用指针调用函数的do_something不再和这个count_if_mod_k适配了,因为count_if_mod_k有三个参数,而do_something的函数指针只能有两个参数(数组及其大小)。难道要为此去修改do_something吗?首先,这一部分代码你未必能修改(比如在一个工程里,那不是你负责的部分,你只是负责搬一搬像count_if_mod_k这样的砖,~~摸鱼划水~~);其次,即便能修改,do_something并不只调用count_if_mod_k这一个函数,如果修改了do_something里函数指针的相应接口,可能导致采取原接口的函数也不能使用了,正是难以两全的局面。在这时,仿函数便可以来救场了。我们使用仿函数改写do_something和count_if_mod_k:
class CountIfModK {
private:
int mod;
public:
CoundIfModK(int k) // 用k初始化除数
: mod(k) {}
int operator()(int* arr, int size) // 完成相同的功能
{
for(int i = 0; i < size; ++i)
{
if(arr[i] % k == 0)
count++;
}
return count;
}
}
void do_something(int* arr, int size, CountIfModK mod_functor)
{
// do something
int cnt = mod_functor(arr, size);
// do something
}
在do_something看来,无论mod_functor具体的除数是什么,都具有相同的类型,也就可以以完全相同的方式调用。就mod_functor的角度来看,除数被妥善地保存、封装、隐藏起来,使得相同的签名(即CountIfModK)也可以产生“不同”的函数。这也正是仿函数的特点:可以在同一签名下拥有不同的实际功能,并且可以拥有自己的状态(甚至可以记录状态)。
你或许会问:这个do_something似乎并不能像函数指针一样调用不同的处理函数?事实上,采用我们在后续课程将会学习的泛型编程的方法,就可以做到了:
template <typename T> // template,即模板,定义一个“模板函数”
void do_something(int* arr, int size, T general_functor) // 需要一个类型为T的仿函数
{
// do something
int whatever = general_functor(arr, size); // 要保证重载了相应接口的operator()
// do something
}
这样,不论是对数组的内容进行修改、判断、统计,都可以用仿函数来实现。它们提供了一致的接口,而它们各自所需要的特别的参数则作为成员数据封装起来,do_something就越来越flexible了。
关于仿函数的杂谈
-
仿函数还可以这样调用:
int cnt = CountIfModK(5)(arr, size);结合仿函数对象的构造方法即可理解。看上去仿佛又多了一个参数。
-
仿函数实际上是一个类及其实例化而得到的对象,因此除了拥有数据以外,甚至还可以拥有组合、继承等关系。可见相较于模板函数(即上文中的
template),仿函数有其自身的独特之处。 -
(将要学习的)C++的STL中大量使用了仿函数,并且也内置了很多的仿函数,定义在
<functional>头文件中。<functional>中还有std::function,将C++中的几种具有函数形式及功能的“可调用对象”抽象成同一种事物,可谓九九归一。 -
(~~逐渐深入到本人也一知半解的东西了,放弃治疗~~)