OOP-THU icon indicating copy to clipboard operation
OOP-THU copied to clipboard

函数调用运算符重载与仿函数

Open nine-point-eight-p opened this issue 4 years ago • 0 comments

运算符重载拓展: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_somethingcount_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++中的几种具有函数形式及功能的“可调用对象”抽象成同一种事物,可谓九九归一。

  • (~~逐渐深入到本人也一知半解的东西了,放弃治疗~~)

参考资料

  1. C++ 仿函数_恋喵大鲤鱼的博客-CSDN博客_c++仿函数
  2. 仿函数(functors)_JUAN425的博客-CSDN博客_仿函数
  3. Standard library header - cppreference.com (codingdict.com)

nine-point-eight-p avatar Apr 05 '22 16:04 nine-point-eight-p