函数对象

函数对象:重载了调用操作符()的类对象。当用该类对象调用此操作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。如:

class A
{
public:
    int operator() (int val)
    {
        return val > 0 ? val : -val;
    }
};

int main()
{
    A a;
    cout << a(-10);  // 10
    return 0;
}

✏️ 1、不同函数复用相同处理代码

🖋️ 1.1、C语言的处理方式

使用函数指针和回调函数来实现代码复用。例如qsort()

🖋️ 1.2、C++语言的处理方式

🐹 1.2.1、函数指针方式

🐹 1.2.2、函数模板方式

🐹 1.2.3、仿函数方式

🐹 1.2.4、仿函数模板方式

✏️ 2、优势

🖋️ 2.1、函数对象和普通函数

与普通函数相比,函数对象比函数更加灵活,函数对象有以下的优势:

  1. 函数对象可以有自己的状态。我们可以在类中定义状态变量,这样一个函数对象在多次的调用中可以共享这个状态。但是函数调用没这种优势,除非它使用全局变量来保存状态。

  2. 函数对象有自己特有的类型,而普通函数无类型可言。这种特性对于使用C++标准库来说是至关重要的。这样我们在使用STL中的函数时,可以传递相应的类型作为参数来实例化相应的模板,从而实现我们自己定义的规则。比如自定义容器的排序规则。

  3. 函数对象一般快于普通函数:因为函数对象一般用于模板参数,模板一般会在编译时会做一些优化。

🖋️ 2.2、函数对象与函数指针

尽管函数指针被广泛用于实现函数回调,但C++还提供了一个重要的实现回调函数的方法,那就是函数对象。函数对象(也称“函数符”)是重载了“()”操作符的普通类的对象。

用函数对象代替函数指针有几个优点:

  • 首先,因为对象可以在内部修改而不用改动外部接口,因此设计更灵活,更富有弹性。函数对象也具备有存储先前调用结果的数据成员。在使用普通函数时需要将先前调用的结果存储在全程或者本地静态变量中,但是全程或者本地静态变量有某些我们不愿意看到的缺陷。

  • 其次,在函数对象中编译器能实现内联调用,从而更进一步增强了性能。这在函数指针中几乎是不可能实现的。

  • C++11还提供了lambda表达式来实现函数的灵活调用。

🖋️ 2.3、函数对象与Lambda表达式

定义一个可以拥有状态的函数对象,其用于生成连续序列:

可以看到,函数对象可以拥有一个私有数据成员,每次调用时递增,从而产生连续序列。同样地,你可以用lambda表达式实现类似的效果,但是必须采用引用捕捉方式。但是,函数对象可以实现更复杂的功能,而用lambda表达式则需要复杂的引用捕捉。当需要捕捉的外部变量很多时,函数对象就比较有优势。

可以看到Print<int>的实例可以传入std::for_each,其表现可以像函数一样,因此我们称这个实例为函数对象。大家可能会想,for_each为什么可以既接收lambda表达式,也可以接收函数对象,其实STL算法是泛型实现的,其不关心接收的对象到底是什么类型,但是必须要支持函数调用运算:

泛型提供了高级抽象,不论是lambda表达式、函数对象,还是函数指针,都可以传入for_each算法中。

✏️ 3、使用场景

🖋️ 3.1、自定义排序规则

std::sort()函数的排序规则和关联式容器的排序规则都是可以自定义的,一种方式就是使用函数对象。如:

🖋️ 3.2、谓词函数

谓词函数是一个返回布尔值的函数。

一元谓词函数就是只釆用一个实参的函数。使用一元谓词函数可以确定一个给定对象是否具有某些特征。

二元谓词函数就是釆用两个形参的谓词函数。使用二元谓词函数可以确定两个对象是否以某种方式相关联。

在调用用到函数对象的标准库算法时,除非显式地指定模板类型为传引用,否则默认情况下函数对象是按值传递的,因此,如果传递一个具有内部状态的函数对象,则被改变状态的是函数内部被复制的临时对象,函数结束后随之消失。真正传进来的函数对象状态并为改变。

原则:

不是所有的返回布尔值的函数对象都适合作为谓词函数,因此用作谓词函数的函数对象,最好不要依赖其内部状态的改变。

最后更新于

这有帮助吗?