Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
nine-point-eight-p opened this issue Apr 5, 2022 · 0 comments
Open

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

nine-point-eight-p opened this issue Apr 5, 2022 · 0 comments

Comments

@nine-point-eight-p
Copy link

运算符重载拓展: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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant